3

I m new at redux and I have integrated it with my react app , but I have a note about a small test .

At the next example I see that the value of user added on my second click.

the reducer:

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;
    }
}

action :

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

export default adminAction; 

changing state in store.


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)
}

the click void associated to a button .

when doing the first click this is what happens :
the value has changed in the store but the admin.user value is still empty.

when clicking a second time : The store values have updated The admin value has been added .

My question is why does only the second click trigger the retrieval of the value from store?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
azdeviz
  • 597
  • 2
  • 9
  • 20

2 Answers2

4

The reason why you are seeing the old value of admin in the click function is due to closures which cause the stale props and state problem. How do JavaScript closures work?

Every function in JavaScript maintains a reference to its outer lexical environment. This reference is used to configure the execution context created when a function is invoked. This reference enables code inside the function to "see" variables declared outside the function, regardless of when and where the function is called.

In a React function component, each time the component re-renders, the closures are re-created, so values from one render don't leak into a different render. It's for this same reason why any state you want should be within useState or useReducer hooks because they will remain past re-renders.

More information on the react side of it in React's FAQ


In addition to the main answer for the question, you should useState for the values of user and pass in your component. Otherwise the values don't stay between re-renders.

You could otherwise go without using controlled components, but not use a weird mix of both.

Working example below:

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"/>

Example with original vars instead of useState. You'll notice in this case by using the vars and keeping the value prop on the inputs, it prevents entry to the password input. The reason why it "works" on the user input is because the user variable started off as undefined rather than an empty string.

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"/>

Example with fully uncontrolled inputs. This doesn't need the vars at all:

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"/>

Why closures are relevant to the post

Because of the synchronous nature of the original code, it's possible to say that the reason the admin.user value that's logged isn't the new value is simply because the component hadn't re-rendered yet. However, the point remains that the admin value for that render is set in stone and will never change due to a closure. Even if React rendered synchronously with the redux state updating, admin wouldn't change.

It's for reasons like this that it's important to think about the closures and how everything fully works even in the simpler cases so you don't mess up in the more complicated ones.

For example of how incorrect mental models can hinder you, imagine that you assume that the reason the console.log(admin.user) doesn't show the correct value in the original example is solely because the component hadn't re-rendered yet. You might assume that putting a timeout to wait for the re-render would let you see the new value.

Imagine trying to add an auto-save functionality where you want to save the current value of admin to localstorage or an API every 10 seconds. You might put an effect with an interval of 10 seconds that logs admin with an empty dependency array because you want it to not reset until the component unmounts. This is clearly incorrect because it doesn't include the admin value in the dependencies, but it will highlight that no matter how many times you submit the values and change admin in the redux state, the value in this effect never changes because this effect will always be running from the initial value of admin on the first render.

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

Closures are the overall issue and you can easily get in trouble if you have the wrong mental model for how things work.


More information on closures:

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

As mentioned in the MDN web docs, every time a function is created, the function gains permanent access to the variables in the surrounding code. Essentially, the functions get access to everything within scope for them.

In react function components, each render creates new functions.

In this example, when App is rendered:

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. the admin variable is initialized into the scope of App based on the current state of the redux store (i.e. { user: '', password: ''}).
  2. The dispatch variable is initialized to be redux's store.dispatch
  3. user is initialized to undefined
  4. pass is initialized to ''
  5. the adminChangeUsername and adminChangePassword functions are initialized. They have access to (and use) user and pass from the closure that they create.
  6. click is initialized. It has access to (and uses) user, pass, store, and admin from it's closure.

click always only has access to the admin value from the current re-render when it was created because the click function created a closure around it when it was created. Once the click event occurs, and adminAction is dispatched, click won't change which render it was from, so it will have access to the admin variable that was initialized when it was. And each render, the admin variable is re-initialized and is thus a different variable.

Also the main issue here not just the asynchronous nature but the fact that state values are used by functions based on their current closures and state updates will reflect in the next re-render by which the existing closures are not affected but new ones are created. Now in the current state the values within hooks are obtained by existing closures and when a re-render happens the closures are updated based on whether function is recreated again or not

No matter if you put a timeout in the click handler, the admin value will never be the new value because of that closure. Even if React happens to re-render immediately after calling dispatch.

You can use a useRef to create a mutatable value who's current property will remain the same between re-renders, and at that point, the asynchronous nature of React's re-renders actually comes into play because you'd still have the old value for a few more miliseconds before react re-renders the component.


In this example https://codesandbox.io/s/peaceful-neumann-0t9nw?file=/src/App.js, you can see that the admin value logged in the timeout still has the old value, but the adminRef.current value logged in the timeout has the new value. The reason for that is because admin points to the variable initialized in the previous render, while adminRef.current stays the same variable between re-renders. Example of the logs from the code sandbox below:

console logs from CodeSandbox example run

The only thing that might really surprise you about the order of events in this is that useSelector is called with the new data before the re-render occurs. The reason for this is that react-redux calls the function whenever the redux store is changed in order to determine if the component needs to be re-rendered due to the change. It would be entirely possible to set the adminRef.current property within the useSelector to always have access to the latest value of admin without store.getState().

In this case, it's easy enough to just use store.getState() to get the current redux state if you need access to the latest state outside of the hook's closure. I often tend to use things like that in longer-running effects because store doesn't trigger re-renders when changed and can help performance in performance critical locations. Though, it should be used carefully for the same reason of it being able to always get you access to the latest state.

I used a timeout in this example because that's the best way to highlight the actual nature of the closure's effect in this. Imagine if you will that the timeout is some asynchronous data call that takes a bit of time to accomplish.

Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
  • thanks bro for your explain , I did the same example before with useState hook , but it s not neccessar for question I want just to try see the same values at admin and store.getState() at the same time , and you have explained now why , thanks you again. – azdeviz May 24 '20 at 18:02
  • `useState` has strictly no relation to this question, nor does it solve something here. Even if those values are empty the second re-rendering, the problem would remain the same. I really think my answer reveals the real issue. – Mik378 May 24 '20 at 18:04
  • thanks so much I understand now . can you mention if there a method to let the admin's value changed when click happen ? , instead of using `store.getState().Admin` ? because when using `useState()` hook with `onChange` method to pass the values of `password and user ` to `Admin` via `Dispatch ` everytime last entred char will not be included at the same time , for example entred `ABCD` the value that will be taked is `ABC` at the last change. – azdeviz May 24 '20 at 18:12
  • The admin does change when the click happens. The issue is that you won't see the update in the click event due to closures. If you want something to happen whenever admin changes, I'd suggest using either `store.getState().admin` within the click event, or just using `useEffect` with `[admin]` as the dependency array. – Zachary Haber May 24 '20 at 18:17
  • @ZacharyHaber It must have taken time, great ;) I did and do master closures principles but it is always good to read on this subject. So, do you confirm (are you agree?) that the only way that the author could see an update after the second click was because we logically deduce that an arrow function was used within the onClick attribute: onClick=`{() => click()}`, thus recreating a new instance of the function reaching the current values. And not `onClick={click}` where in this case, the `admin` value won't never be updated because dealing with the initial closure. – Mik378 May 25 '20 at 23:45
  • @Mik378, `onClick={()=>click()}` and `onClick={click}` are the same, there's no difference between the two (in this case) except that the first doesn't get the event argument. The reason the second click gets the new `admin` value is because every time react re-renders it also re-renders the children. And the `click` function in this example is re-created on every render, so the button will have the new function which has access to the current scope. If we wanted to purposely not have the new `admin` value, we'd need `useCallback(click,[])` or similar to prevent it from updating. – Zachary Haber May 25 '20 at 23:53
  • @ZacharyHaber with `onClick={click}` and a proper way where click always reference the same instance of the function across re-rendering, the second click will still show `admin.user` as empty. – Mik378 May 26 '20 at 00:06
  • 1
    Yes, I agree. That's what I was referring to when I mentioned the use of `useCallback` to keep the function from updating on each render :) – Zachary Haber May 26 '20 at 00:09
  • yep ;) I agree too. – Mik378 May 26 '20 at 00:10
1

The admin object will be fed after the next component rerendering process, after the whole click callback has been executed.

Indeed, it is based on the value provided by useSelector that is triggered ONCE during a rendering.

admin.user being checked in the click callback, the component has not been reredenring yet, thus showing the current empty string value the first crossing.

1) Component is rendered for the first time.
2) useSelector is called.
3) Click is made.
4) Action is dispatched.
5) admin.user is still empty since 2) is has not been run again yet.
6) After click callback finishes, a rerendering is about to be made so useSelector is triggered again, grabbing the admin value!

Moral of the story:
Don't expect value provided from useSelector to be immediately synchronised within the click callback. It needs rerendering process to happen.

Besides, and just to improve your code, you can replace:

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

by this:

return {
         user: action.user,
         password: action.password
}
Mik378
  • 21,881
  • 15
  • 82
  • 180
  • thanks so much I understand now . can you mention if there a method to let the admin's value changed when click happen ? , instead of using `store.getState().Admin` ? because when using `useState()` hook with `onChange` method to pass the values of `password and user ` to `Admin` via `Dispatch ` everytime last entred char will not be included at the same time , for example entred `ABCD` the value that will be taked is `ABC` at the last change. – azdeviz May 24 '20 at 18:12
  • You're welcome. No you can't, if you want to grab a just-changed value from the store, you have to wait for a rerendering, so unreachable within the callback. You can use: `useEffect(() => {doSomethingWithAdminUser}, [admin])`, if you want to do something with `admin.user` just after re-rendering happens. – Mik378 May 24 '20 at 18:15
  • 1
    thanks for the info , I got the idea prefectly now. – azdeviz May 24 '20 at 18:22
  • Thanks, don't hesitate to validate the answer if it has helped you;) The other answer mentions the need for `useState` that doesn't answer the issue at all. Nor "closure" that is not linked AT ALL to the issue. – Mik378 May 24 '20 at 18:28
  • @Mik378 it absolutely has to do with closures. Here's an example proving that. I set a timeout within the click handler. The timer finishes after the component re-renders and it still shows the old admin value. If the issue wasn't due to closures, you'd see the new value after the setTimeout based on your point #5. https://codesandbox.io/s/delicate-darkness-trlku?file=/src/App.js – Zachary Haber May 24 '20 at 18:56
  • @ZacharyHaber There's no timeout in the posted code. With a timeout, it would involve the closure concept in the answer but it's not the case here. The fact that the author sees an empty value the first time is due to the fact that the useSelector hasn't been triggered again yet to grab the updated value ; that's all. – Mik378 May 24 '20 at 19:03
  • @Mik378 There is a timeout in the codesandbox I posted, also, all JS functions have closures applied to them regardless of if they are involved in a setTimeout call. Here are some resources discussing closures in regards to react hooks: https://dmitripavlutin.com/react-hooks-stale-closures https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately/54069332#54069332 https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/ – Zachary Haber May 24 '20 at 19:27
  • @ZacharHaber In your posted code yes there's a timeout, but NOT in THIS post. I ensure you that the real answer does not deal with "closure specificity" at all. – Mik378 May 24 '20 at 19:35
  • @Mik378, I have never disagreed with your assessment, but I do disagree with your dismissal of the importance of closures. I have updated my post, please read the section `Why closures are relevant to the post`. – Zachary Haber May 25 '20 at 20:20