Confusion with useState hook

Hi, I’m having some trouble. I thought I understood useState pretty well, but now it is behaving in an unexpected way, and I don’t quite know what to make of it. :slight_smile:

In addItem(), When I update arr, then set the selected item, it selects the second-to-last item. I assume this is because arr has not yet been updated when I call setSelected()? I don’t know the best way to fix this. I thought about using useEffect(), and selecting the last item whenever arr.length changes. Any suggestions? Thank you so much.

export default function App() {
  const [arr, setArr] = useState([])
  const [selected, setSelected] = useState(null) 
  useEffect(() => {
    setArr([1,2,3,4,5,6,7])
  }, [])
  async function addItem() {
    await setArr(prev => [...prev, prev.length + 1])
    setSelected(arr[arr.length - 1])
  }
  return (
    <div className="App">
      {
        arr.map(item => {
            return (
               <Item 
                  key={item} 
                  num={item} 
                  selected={selected === item} 
                  onSelect={() => setSelected(item)} 
               />
             )
         })
      }
      <div 
        className='item'
        onClick={addItem}>
          New Item
      </div>
    </div>
  );
}

Link to the program:

/**
 * @type {{ selected: number | null, arr: number[] }}
 */
const initialState = {
  selected: null,
  arr: [],
};

/**
 * @param {typeof initialState}
 * I assume this won't actually just be like ADD_ITEM with no payload, but anyway:
 * @param {{ type: "ADD_ITEM" }}
 * @returns {typeof initialState}
 */
function reducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM":
      // I assume this will not be quite like this:
      return {
        arr: state.arr.concat(state.arr.length + 1),
        selected: state.arr.length,
      };
    // I assume there will be other actions...
    // case "REMOVE_ITEM":
    //   return altered state
    default:
      throw new Error();
  }
}


const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const addItem = async () => {
    dispatch({ type: "ADD_ITEM" });
  };

  // Return component code here:
}

Wow, thanks! I hadn’t thought of using redux.

I’m still confused about useState, however. I feel like there is something important I am not understanding. Do you think you (or anyone) could explain why arr has not updated by the time I call setSelected?

Thanks.

It’s not redux, it’s just react. If you have even slightly complex state that’s interdependent, then you would generally use useReducer instead of useState, that’s what it’s there for.

1 Like

I’m still confused about useState, however. … Do you think you (or anyone) could explain why arr has not updated by the time I call setSelected ?

Dan’s answers are good. But trying to answer your question more directly … I am no expert here, but …

  async function addItem() {
    await setArr(prev => [...prev, prev.length + 1])
    setSelected(arr[arr.length - 1])
  }

I assume that the setter returned by useState is similar to how setState works. It is asynchronous. You’ve tried to apply await, but that works on promises. Those state setters do not return a promise, they don’t return anything. So, your await is just telling it to wait until that code is run, not to wait for its asynchronous actions to be finished.

In addition to Dan’s suggestion, you could use setSelected before calling, based on the current state of arr.

Furthermore, if selected is always the last element or arr, does it need to be kept in state? Can you just grab the last element when you need it?

Also, another way would be to have a useEffect that listens to arr and is triggered by changes to it, so you set arr in addItem and then setSelected is run based on that effect.

Another option would be to put setSelected inside the callback for setArr, so you are basing it on prev. I don’t know if that is kosher or not, but conceptually it seems to make sense.

1 Like

Wow, how have I not heard of that yet? There is a lot to learn. Thanks, that’s very helpful.

Thanks, I didn’t know that.

Haha I also thought of doing that, but didn’t know how ‘proper’ it was.

Yeah that seems to be what’s happening atm. You’ve introduced a race condition. Async/await isn’t just for making things happen in order, it’s specifically for handling async code that returns promises, which as @kevinSmith says, is not what the setter function returns (it’s async, but returns void).

Without the async/await the two updates should be batched, which will again cause similar behaviour. React will try to optimise things, and because you’re setting it to the length of the variable arr, it’s going to use that value, as it is, rather than the value in next render.

The obvious way to do this is to just give the set[Arr|Selected] functions the correct data at first. Example:

function addItem() {
    const newArr = [...arr, arr.length + 1];
    setArr(newArr);
    setSelected(newArr.length - 1);
}

link here: practical-smoke-p3ls2 - CodeSandbox

Edit I modified a few of things while I was trying to get stuff running – apologies if there’s anything slightly confusing there. But use the index for the selection, it’s miles easier and will always work fine

2 Likes

So is it pointless to use async/await with useState?

The code I gave is just a stripped-down version of the problem I’m having with a project. In my project, I need selected to be pointing to an object, since I am accessing the data from it. Would you recommend I have one piece of state holding the object, and another one holding the index of the selected item? I didn’t do this because I thought it would just be unnecessary code.

Would there be any possible side-effects from doing this?

Thank you so much for all of your help!

Yes. It’s for running asynchronous logic, but not in this case.

I assumed that was the case. It depends entirely on what you’re doing. Is this a to-do app by any chance?

I would strongly suggest using useReducer for the object manipulation over useState.

Also, some of the issues will go away when you break it down into components and use props to pass values in.

Just using the variables existing in the scope of the function? No. The components are just functions. Props are just like normal arguments you pass to a function. State values are also like normal arguments, but effectively allow you to modify the arguments from within the function, forcing the function to run again with the new values.

Same idea, a note taking app.

Yes, I’m very excited to have discovered useReducer!

You still need to be comfortable with useState and there are cases where that makes sense, and it certainly could work here, but I also agree that useReducer would be a cleaner and more sophisticated solution. If you’ve done the FCC stuff on redux reducers, you’ll have a leg up.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.