再次单击时更改redux状态值

时间:2020-05-24 16:38:00

标签: javascript redux react-redux react-hooks

我是redux的新手,并且已将其与我的react应用程序集成在一起,但是我有一个关于小测试的说明。

在下一个示例中,我看到第二次点击增加了用户价值。

减速器

const initialState = {
    user: '',
    password: ''
}

export const admin = (state = initialState, action) => {
    switch (action.type) {
        case 'admin':
            return state = {
                user: action.user,
                password: action.password
            }
        default:
            return initialState;
    }
}

操作:

const adminAction = (user, password) => {
    return {
        type: 'admin',
        user: user,
        password: password
    }
}

export default adminAction; 

更改商店中的状态。


const admin = useSelector(state => state.admin);
const dispatch = useDispatch();

var user,pass = '';

const adminChangeUsername = (event) => {
  user = event.target.value;
}

const adminChangePassword = (event) => {
  pass = event.target.value;
}

const click= (event) => {
    event.preventDefault();
    dispatch(adminAction(user,pass));
    console.log(store.getState())
    console.log(admin.user)
}

与按钮相关的 点击 无效。

第一次点击时会发生以下情况:
商店 中的值已更改,但 admin.user 的值仍为空。

再次单击时: 商店 值已更新 已添加 admin 值。

我的问题是为什么只有第二次单击才能触发从商店中检索值?

2 个答案:

答案 0 :(得分:2)

admin函数中看到click的旧值的原因是由于闭包导致过时的道具和状态问题。 How do JavaScript closures work?

JavaScript中的每个函数都对其外部保持引用 词汇环境。该参考用于配置执行 调用函数时创建的上下文。此参考启用 函数内部的代码以“查看”在外部声明的变量 函数,无论何时何地调用该函数。

在React函数组件中,每次重新渲染该组件时,都会重新创建闭包,因此来自一个渲染的值不会泄漏到另一个渲染中。出于同样的原因,您想要的任何状态都应位于useStateuseReducer挂钩之内,因为它们将保留过去的重新渲染。

React's FAQ

中有更多关于反应方面的信息

除了该问题的主要答案外,您还应该useState来获取组件中userpass的值。否则,值不会停留在重新渲染之间。

否则,您可以不使用受控组件,而不能同时使用两者的怪异组合。

以下工作示例:

const initialstate = {
  user: '',
  password: '',
};

const adminReducer = (state = initialstate, action) => {
  switch (action.type) {
    case 'admin':
      return (state = {
        user: action.user,
        password: action.password,
      });
    default:
      return initialstate;
  }
};

const adminAction = (user, password) => {
  return {
    type: 'admin',
    user: user,
    password: password,
  };
};

const store = Redux.createStore(Redux.combineReducers({ admin: adminReducer }));

const App = () => {
  const store = ReactRedux.useStore();
  const admin = ReactRedux.useSelector((state) => state.admin);
  const dispatch = ReactRedux.useDispatch();
  const [user, setUser] = React.useState('');
  const [pass, setPass] = React.useState('');

  const adminChangeUsername = (event) => {
    setUser(event.target.value);
  };

  const adminChangePassword = (event) => {
    setPass(event.target.value);
  };

  const click = (event) => {
    event.preventDefault();
    dispatch(adminAction(user, pass));
    console.log('store has the correct value here', store.getState());
    console.log('admin is the previous value of admin due to closures:', admin);
  };

  return (
    <div>
      <input
        onChange={adminChangeUsername}
        value={user}
        placeholder="username"
      />
      <input
        onChange={adminChangePassword}
        value={pass}
        type="password"
        placeholder="password"
      />
      <button onClick={click}>submit</button>

      <p>This shows the current value of user and password in the store</p>
      <p>User: {admin.user}</p>
      <p>Pass: {admin.password}</p>
    </div>
  );
};

ReactDOM.render(
  <ReactRedux.Provider store={store}>
    <App />
  </ReactRedux.Provider>,
  document.querySelector('#root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>

<div id="root"/>

使用原始变量而不是useState的示例。在这种情况下,您会注意到使用vars并在输入中保留值pro​​p,这会阻止输入密码输入。它在用户输入上“起作用”的原因是因为用户变量开始时是未定义的,而不是空字符串。

const initialstate = {
  user: '',
  password: '',
};

const adminReducer = (state = initialstate, action) => {
  switch (action.type) {
    case 'admin':
      return (state = {
        user: action.user,
        password: action.password,
      });
    default:
      return initialstate;
  }
};

const adminAction = (user, password) => {
  return {
    type: 'admin',
    user: user,
    password: password,
  };
};

const store = Redux.createStore(Redux.combineReducers({ admin: adminReducer }));

const App = () => {
  const store = ReactRedux.useStore();
  const admin = ReactRedux.useSelector((state) => state.admin);
  const dispatch = ReactRedux.useDispatch();
  
  var user,pass='';

  const adminChangeUsername = (event) => {
    user = event.target.value;
  };

  const adminChangePassword = (event) => {
    pass = event.target.value;
  };
  const click = (event) => {
    event.preventDefault();
    dispatch(adminAction(user, pass));
    console.log('store has the correct value here', store.getState());
    console.log('admin is the previous value of admin due to closures:', admin);
  };

  return (
    <div>
      <input
        onChange={adminChangeUsername}
        value={user}
        placeholder="username"
      />
      <input
        onChange={adminChangePassword}
        value={pass}
        type="password"
        placeholder="password"
      />
      <button onClick={click}>submit</button>

      <p>This shows the current value of user and password in the store</p>
      <p>User: {admin.user}</p>
      <p>Pass: {admin.password}</p>
    </div>
  );
};

ReactDOM.render(
  <ReactRedux.Provider store={store}>
    <App />
  </ReactRedux.Provider>,
  document.querySelector('#root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>

<div id="root"/>

输入完全不受控制的示例。这根本不需要var:

const initialstate = {
  user: '',
  password: '',
};

const adminReducer = (state = initialstate, action) => {
  switch (action.type) {
    case 'admin':
      return (state = {
        user: action.user,
        password: action.password,
      });
    default:
      return initialstate;
  }
};

const adminAction = (user, password) => {
  return {
    type: 'admin',
    user: user,
    password: password,
  };
};

const store = Redux.createStore(Redux.combineReducers({ admin: adminReducer }));

const App = () => {
  const store = ReactRedux.useStore();
  const admin = ReactRedux.useSelector((state) => state.admin);
  const dispatch = ReactRedux.useDispatch();

  const click = (event) => {
    event.preventDefault();
    const user = event.currentTarget.user.value;
    const pass = event.currentTarget.pass.value;
    dispatch(adminAction(user, pass));
    console.log('store has the correct value here', store.getState());
    console.log('admin is the previous value of admin due to closures:', admin);
  };

  return (
    <div>
      <form onSubmit={click}>
        <input name="user" placeholder="username" />
        <input name="pass" type="password" placeholder="password" />
        <button type="submit">submit</button>
      </form>
      <p>This shows the current value of user and password in the store</p>
      <p>User: {admin.user}</p>
      <p>Pass: {admin.password}</p>
    </div>
  );
};

ReactDOM.render(
  <ReactRedux.Provider store={store}>
    <App />
  </ReactRedux.Provider>,
  document.querySelector('#root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js" integrity="sha256-7nQo8jg3+LLQfXy/aqP5D6XtqDQRODTO18xBdHhQow4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js" integrity="sha256-JuJho1zqwIX4ytqII+qIgEoCrGDVSaM3+Ul7AtHv2zY=" crossorigin="anonymous"></script>

<div id="root"/>


为什么关闭与帖子有关

由于原始代码具有同步性,因此有可能说,记录的admin.user值不是新值的原因仅仅是因为该组件没有重新渲染了。但是,重点仍然在于,该渲染的admin值设置为固定值,并且永远不会因关闭而改变。即使React与Redux状态更新同步呈现,admin也不会改变。

出于这样的原因,重要的是考虑闭包以及即使在较简单的情况下一切也能完全正常工作的情况,因此您不要弄乱较复杂的闭包。

例如关于不正确的心理模型如何阻碍您的示例,假设您假设console.log(admin.user)在原始示例中未显示正确值的原因仅仅是 ,因为该组件尚未重新渲染。您可能会假设超时等待重新渲染将使您看到新值。

想象一下,尝试添加自动保存功能,您希望每10秒将admin的当前值保存到localstorage或API。您可能会以10秒的间隔放置一个效果,该效果会使用空的依赖项数组记录admin,因为您希望在组件卸载之前不重置它。这显然是不正确的,因为它在依赖项中不包含admin值,但是它将突出显示,无论您提交值多少次并在redux状态下更改admin,该值都将此效果永远不会改变,因为该效果将始终从第一个渲染器上的admin的初始值开始运行。

useEffect(()=>{
  const intervalId = setInterval(()=>{console.log(admin)},10000);
  return ()=>clearInterval(intervalId);
},[])

关闭是整体问题,如果您对事物的工作方式有错误的思维模式,则很容易遇到麻烦。


有关闭包的更多信息:

闭包是捆绑在一起的功能的组合(闭包) 并参考其周围状态(词汇环境)。在 换句话说,闭包使您可以访问外部函数的作用域 从内部功能。在JavaScript中,每次都会创建闭包 在函数创建时就创建了一个函数。

如MDN Web文档中所述,每次创建函数时,该函数都会获得对周围代码中变量的永久访问权。本质上,这些函数可以访问它们范围内的所有内容。

在react函数组件中,每个渲染都会创建新函数。

在此示例中,当呈现App时:

const App = ()=>{
    const admin = useSelector(state => state.admin);
    const dispatch = useDispatch();

    var user,pass = '';

    const adminChangeUsername = (event) => {
      user = event.target.value;
    }

    const adminChangePassword = (event) => {
      pass = event.target.value;
    }

    const click= (event) => {
        event.preventDefault();
        dispatch(adminAction(user,pass));
        console.log(store.getState())
        console.log(admin.user)
    }

}
  1. 根据redux存储区的当前状态(即{ user: '', password: ''})将admin变量初始化为App的范围。
  2. 将调度变量初始化为redux的store.dispatch
  3. 用户被初始化为undefined
  4. 通行证已初始化为''
  5. 初始化adminChangeUsername和adminChangePassword函数。他们有权访问(并使用)用户,并从他们创建的闭包中进行传递。
  6. 点击已初始化。它可以从其关闭处访问(并使用)用户,传递,存储和管理。

click在创建时始终只能从当前重新渲染中访问admin值,因为click函数在创建时围绕它创建了一个闭包。一旦发生click事件并调度了adminAction,click将不会更改其来源,因此它可以访问在 it admin变量。 >是。并且每个渲染admin变量都会重新初始化,因此是一个不同的变量。

这里的主要问题不仅是异步性质,还有事实 状态值由函数根据其当前值使用 关闭和状态更新将反映在下一次重新渲染中 现有的关闭不受影响,但是会创建新的关闭。现在 在当前状态下,挂钩中的值是通过现有的 闭包,并且当重新渲染发生时,闭包将根据更新 关于是否重新创建功能

无论您是否在点击处理程序中添加了超时,admin值都不会因为关闭而成为新值。即使React在调用dispatch之后碰巧立即重新渲染了

您可以使用useRef创建一个可变值,其current属性 在重新渲染之间保持不变,这时,React的异步特性重新渲染实际上起了作用,因为在React重新渲染该组件之前,您还有几毫秒的旧值。


在此示例https://codesandbox.io/s/peaceful-neumann-0t9nw?file=/src/App.js中,您可以看到在超时中记录的admin值仍然具有旧值,但是在超时中记录的adminRef.current值具有新值。原因是admin指向在上一个渲染中初始化的变量,而adminRef.current在重新渲染之间保持相同的变量。下面的代码沙箱中的日志示例:

console logs from CodeSandbox example run

唯一令您感到惊讶的是,事件发生的顺序是在重新渲染发生之前,用新数据 调用useSelector。这样做的原因是,每当更改Redux存储时,react-redux都会调用该函数,以确定 if 是否由于更改而需要重新呈现组件。完全有可能将adminRef.current中的useSelector属性设置为始终不使用store.getState()就可以访问admin的最新值。

在这种情况下,如果需要访问钩子闭包之外的最新状态,只需使用store.getState()就可以获取当前的redux状态,这很容易。我经常倾向于在运行时间更长的效果中使用类似的方法,因为store不会在更改时触发重新渲染,并且可以在性能关键的位置帮助提高性能。不过,出于相同的原因,应谨慎使用它,因为它始终可以使您访问最新状态。

在此示例中,我使用了超时,因为这是突出显示闭包效果的实际性质的最佳方法。试想一下,如果您将超时称为某个异步数据调用,则需要花费一些时间才能完成。

答案 1 :(得分:1)

在执行完整个点击回调后,将在下一个组件重新呈现过程后馈送admin对象。

实际上,它基于useSelector提供的值,该值是在渲染过程中一次触发的。

admin.userclick回调中被检查,该组件尚未重新定义,因此在第一个交叉处显示当前空字符串值。

1)首次渲染组件。
2)useSelector被调用。
3)点击。
4)已调度动作。
5)admin.user仍然为空,因为2)尚未再次运行
6)click回调完成后,将要进行重新渲染,因此将再次触发useSelector,抓住管理员value

故事的道德感:
不要期望从useSelector提供的值会立即在点击回调中同步。 它需要重新渲染过程。

此外,为了改善您的代码,您可以替换:

return state = {
                user: action.user,
                password: action.password
}

通过:

return {
         user: action.user,
         password: action.password
}