386
votes

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me even with spread(...) or without spread operator. I have made an API on another PC that I am calling and fetching the data which I want to set into the state.

Here is my code:

<div id="root"></div>

<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant

const StateSelector = () => {
  const initialValue = [
    {
      category: "",
      photo: "",
      description: "",
      id: 0,
      name: "",
      rating: 0
    }
  ];

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {
        // const response = await fetch("http://192.168.1.164:5000/movies/display");
        // const json = await response.json();
        // const result = json.data.result;
        const result = [
          {
            category: "cat1",
            description: "desc1",
            id: "1546514491119",
            name: "randomname2",
            photo: null,
            rating: "3"
          },
          {
            category: "cat2",
            description: "desc1",
            id: "1546837819818",
            name: "randomname1",
            rating: "5"
          }
        ];
        console.log("result =", result);
        setMovies(result);
        console.log("movies =", movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>

<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>

The setMovies(result) as well as setMovies(...result) is not working. Could use some help here.

I expect the result variable to be pushed into the movies array.

14
Are you able to see the changes moving console.log("movies =", movies); outsite the useEffect hook?Domenico Ruggiano

14 Answers

471
votes

Much like setState in Class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.

Also, the main issue here is 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 the function is recreated again or not.

Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.

setMovies(result);
console.log(movies) // movies here will not be updated

If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern

useEffect(() => {
    // action on update of movies
}, [movies]);

As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.

However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like

setMovies(prevMovies => ([...prevMovies, ...result]));
303
votes

Additional details to the previous answer:

While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.

TLDR: The reason is a closure scope around an immutable const value.


Solutions:

  • read the value in render function (not inside nested functions):

      useEffect(() => { setMovies(result) }, [])
      console.log(movies)
    
  • add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):

      useEffect(() => { setMovies(result) }, [])
      useEffect(() => { console.log(movies) }, [movies])
    
  • use a mutable reference (when the above is not possible):

      const moviesRef = useRef(initialValue)
      useEffect(() => {
        moviesRef.current = result
        console.log(moviesRef.current)
      }, [])
    

Explanation why it happens:

If async was the only reason, it would be possible to await setState().

However, both props and state are assumed to be unchanging during 1 render.

Treat this.state as if it were immutable.

With hooks, this assumption is enhanced by using constant values with the const keyword:

const [state, setState] = useState('initial')

The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).

Consider following fake, but synchronous, React-like implementation:

// sync implementation:

let internalState
let renderAgain

const setState = (updateFn) => {
  internalState = updateFn(internalState)
  renderAgain()
}

const useState = (defaultState) => {
  if (!internalState) {
    internalState = defaultState
  }
  return [internalState, setState]
}

const render = (component, node) => {
  const {html, handleClick} = component()
  node.innerHTML = html
  renderAgain = () => render(component, node)
  return handleClick
}

// test:

const MyComponent = () => {
  const [x, setX] = useState(1)
  console.log('in render:', x) // ✅
  
  const handleClick = () => {
    setX(current => current + 1)
    console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
  }
  
  return {
    html: `<button>${x}</button>`,
    handleClick
  }
}

const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
10
votes

You can solve it by using the useRef hook but then it's will not re-render when it' updated. I have created a hooks called useStateRef, that give you the good from both worlds. It's like a state that when it's updated the Component re-render, and it's like a "ref" that always have the latest value.

See this example:

var [state,setState,ref]=useStateRef(0)

It works exactly like useState but in addition, it gives you the current state under ref.current

Learn more:

7
votes

I just finished a rewrite with useReducer, following @kentcdobs article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.

see: https://kentcdodds.com/blog/how-to-use-react-context-effectively

I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.

Enjoy, I know I am !!

import React from 'react'

// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively

const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()

function stateReducer(state, action) {
  if (state.hasOwnProperty(action.type)) {
    return { ...state, [action.type]: state[action.type] = action.newValue };
  }
  throw new Error(`Unhandled action type: ${action.type}`);
}

const initialState = {
  keyCode: '',
  testCode: '',
  testMode: false,
  phoneNumber: '',
  resultCode: null,
  mobileInfo: '',
  configName: '',
  appConfig: {},
};

function DispatchProvider({ children }) {
  const [state, dispatch] = React.useReducer(stateReducer, initialState);
  return (
    <ApplicationDispatch.Provider value={dispatch}>
      <ApplicationContext.Provider value={state}>
        {children}
      </ApplicationContext.Provider>
    </ApplicationDispatch.Provider>
  )
}

function useDispatchable(stateName) {
  const context = React.useContext(ApplicationContext);
  const dispatch = React.useContext(ApplicationDispatch);
  return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}

function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }

export {
  DispatchProvider,
  useKeyCode,
  useTestCode,
  useTestMode,
  usePhoneNumber,
  useResultCode,
  useMobileInfo,
  useConfigName,
  useAppConfig,
}

with a usage similar to this:

import { useHistory } from "react-router-dom";

// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';

import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';

import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';

export const AltIdPage = () => {
  const history = useHistory();
  const [keyCode, setKeyCode] = useKeyCode();
  const [phoneNumber, setPhoneNumber] = usePhoneNumber();
  const [appConfig, setAppConfig] = useAppConfig();

  const keyPressed = btn => {
    const maxLen = appConfig.phoneNumberEntry.entryLen;
    const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
    setPhoneNumber(newValue);
  }

  const doSubmit = () => {
    history.push('s');
  }

  const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;

  return (
    <Container fluid className="text-center">
      <Row>
        <Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
      </Row>
      <Row>
        <MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
      </Row>
      <Row>
        <SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
      </Row>
      <Row>
        <ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
      </Row>
    </Container>
  );
};

AltIdPage.propTypes = {};

Now everything persists smoothly everywhere across all my pages

Nice!

Thanks Kent!

3
votes

I found this to be good, instead of defining state(Approach 1) as, example,

const initialValue = 1;

const [state,setState] = useState(initialValue)

Try this approach(Approach 2),

const [state = initialValue,setState] = useState()

This resolved the re-render issue without using useEffect since we are not concerned with its internal closure approach with this case.

P.S. If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state so approach 1 shall be used in this situation.

2
votes

useEffect has its own state/lifecycle it's related to mutation of state, it will not update until you pass a function in parameters or effect destroyed.

just pass single argument in parameters state and it will work perfectly.

React.useEffect(() => {
    console.log("effect");
    (async () => {
        try {
            let result = await fetch("/query/countries");
            const res = await result.json();
            let result1 = await fetch("/query/projects");
            const res1 = await result1.json();
            let result11 = await fetch("/query/regions");
            const res11 = await result11.json();
            setData({
                countries: res,
                projects: res1,
                regions: res11
            });
        } catch {}
    })(data)
}, [])
# or use this
useEffect(() => {
    (async () => {
        try {
            await Promise.all([
                fetch("/query/countries").then((response) => response.json()),
                fetch("/query/projects").then((response) => response.json()),
                fetch("/query/regions").then((response) => response.json())
            ]).then(([country, project, region]) => {
                // console.log(country, project, region);
                setData({
                    countries: country,
                    projects: project,
                    regions: region
                });
            })
        } catch {
            console.log("data fetch error")
        }
    })()
}, []);
1
votes

There are many good answer that shows how to fix your code, but there is an NPM package that let you fix it just by changing the import. it's called react-useStateRef

In your case:

import useState from 'react-usestateref'
const [movies, setMovies,moviesRef] = useState(initialValue);
....
useEffect(() => {
   setMovies(...)
   console.log(moviesRef.current) // it will have the last value
})

As you can see. Using this library let you access the latest state.

0
votes

With custom hooks from my library, you can wait for the state values to update:

  1. useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.

(Live Demo)

    import React, { useState, useEffect, useCallback } from "react";
    import { useAsyncWatcher } from "use-async-effect2";
    
    function TestComponent(props) {
      const [counter, setCounter] = useState(0);
      const [text, setText] = useState("");
    
      const textWatcher = useAsyncWatcher(text);
    
      useEffect(() => {
        setText(`Counter: ${counter}`);
      }, [counter]);
    
      const inc = useCallback(() => {
        (async () => {
          await new Promise((resolve) => setTimeout(resolve, 1000));
          setCounter((counter) => counter + 1);
          const updatedText = await textWatcher();
          console.log(updatedText);
        })();
      }, []);
    
      return (
        <div className="component">
          <div className="caption">useAsyncEffect demo</div>
          <div>{counter}</div>
          <button onClick={inc}>Inc counter</button>
        </div>
      );
    }
    
    export default TestComponent;
  1. useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.

(Live Demo)

import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";

function TestComponent(props) {
  const [state, setState] = useAsyncDeepState({
    counter: 0,
    computedCounter: 0
  });

  useEffect(() => {
    setState(({ counter }) => ({
      computedCounter: counter * 2
    }));
  }, [state.counter]);

  const inc = useCallback(() => {
    (async () => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      await setState(({ counter }) => ({ counter: counter + 1 }));
      console.log("computedCounter=", state.computedCounter);
    })();
  });

  return (
    <div className="component">
      <div className="caption">useAsyncDeepState demo</div>
      <div>state.counter : {state.counter}</div>
      <div>state.computedCounter : {state.computedCounter}</div>
      <button onClick={() => inc()}>Inc counter</button>
    </div>
  );
}
0
votes

var [state,setState]=useState(defaultValue)

useEffect(()=>{ var updatedState setState(currentState=>{ // Do not change the state by get the updated state updateState=currentState return currentState }) alert(updateState) // the current state. })

0
votes

use Background Timer Library it solve my problem https://github.com/ocetnik/react-native-background-timer

const timeoutId = BackgroundTimer.setTimeout(() => { // this will be executed once after 1 seconds // even when app is the the background console.log('tac'); }, 1000);

0
votes

The closure is not the only reason.

Based on the source code of useState (simplified below). Seems to me the value is never assigned right away.

What happens is that an update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.

Which means even we don't have closure issue, react version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.

  function useState(initialState) {
    let hook;
    ...

    let baseState = hook.memoizedState;
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next;

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);            // setValue HERE
        firstUpdate = firstUpdate.next;
      } while (firstUpdate !== hook.queue.pending);

      hook.queue.pending = null;
    }
    hook.memoizedState = baseState;

    return [baseState, dispatchAction.bind(null, hook.queue)];
  }

function dispatchAction(queue, action) {
  const update = {
    action,
    next: null
  };
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;

  isMount = false;
  workInProgressHook = fiber.memoizedState;
  schedule();
}

There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8

-1
votes

I found a trick in setState hook. You must not use old variable. You must create new variable and pass it to hook. For example:

const [users, setUsers] = useState(['Ayşe', 'Fatma'])

useEffect(() => {
    setUsers((oldUsers) => {
        oldUsers.push(<div>Emir</div>)
        oldUsers.push(<div>Buğra</div>)
        oldUsers.push(<div>Emre</div>)
        return oldUsers
    })
}, [])

return (
    <Fragment>
        {users}
    </Fragment>
)

You will see only Ayşe and Fatma users. Becouse you're returning (or passing) oldUsers variable. This variable has SAME REFERENCE with old state's reference. You must return NEWLY CREATED variable. If you pass same referenced variable then Reactjs not updating state.

const [users, setUsers] = useState(['Ayşe', 'Fatma'])

useEffect(() => {
    setUsers((oldUsers) => {
        const newUsers = [] // Create new array. This is so important.
        
        // you must push every old item to our newly created array
        oldUsers.map((user, index) => {
            newUsers.push(user)
        })
        // NOTE: map() function is synchronous
        
        newUsers.push(<div>Emir</div>)
        newUsers.push(<div>Buğra</div>)
        newUsers.push(<div>Emre</div>)
        return newUsers
    })
}, [])

return (
    <Fragment>
        {users}
    </Fragment>
)
-2
votes

There’s a special syntax to work with promises in a more comfortable fashion, called “async/await”. It’s surprisingly easy to understand and use.

There can be multiple solutions on this case of setState. One of the simplest and comfortable solution I found is as follows: In functional component, make useEffect() as async/await. i.e. Here in above example useEffect() is already made as async function. Now do make setMovies(results) as await like:

await setMovies(results);

This will definitely solve problem of immediate change. The keyword await makes JavaScript wait until that promise settles and returns its result. You also need not have to set initial value as written in question above. You can only declare variable movies like

const [movies, setMovies] = useState([]);

more info: https://javascript.info/async-await

-10
votes
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.