0

I created a hook, whose purpose is adding cherries to an array:

import { useReducer, useRef } from "react";

const useCherry = () => {
    const myRef = useRef(0);

    const [state, dispatch] = useReducer(
        (state, action) => {
            if (action.type === "add") {
                myRef.current += 1;
                return [...state, ""];
            }

            return state;
        },
        []
    );

    return [state, dispatch, myRef];
};

export default useCherry;

Then I call it on button click:

<button
    type="button"
    onClick={() => {
        console.log(myRef.current); // Logs 0
        dispatch({ type: "add" });
        console.log(myRef.current);// Logs 0, expected 1
    }}
>
    Add more cherry
</button>

I don't understand why would cherryRef.current return an old value after calling dispatchCherry(). In theory it should return the incremented one, as myRef is an object.

I'm trying to get the incremented value right after calling the dispatch().

Robo Robok
  • 21,132
  • 17
  • 68
  • 126
  • `dispatch({ type: "add" });` has async-like behavior, you can [read more here](https://stackoverflow.com/questions/64538271/is-reacts-usereducer-is-synchronous-compared-to-asynchronous-nature-of-usestate). – Sandro Skhirtladze Jun 11 '23 at 18:36

2 Answers2

3

Because dispatch() runs and updates the state for the next render, the second console.log() runs before it, and thus returns the old value.

To store and retrieve the useRef value before and after the change separately (outside of the state), consider to export a customized dispatch() for use in another component.

const myDispatchCherry = (action) => {
  if (action?.type === "add") myRef.current += 1;
  dispatch(action);
}

This could also keep the original dispatch() pure to its function, and add the additional tasks as an optional and customizable part.

Test the follow code here: stackblitz

import React, { useReducer, useRef } from "react";

const useCherry = () => {
    const myRef = useRef(0);
    const [state, dispatch] = useReducer(
        (state, action) => {
            if (action.type === "add") {
                return [...state, ""];
            }
            return state;
        },
        []
    );

const myDispatchCherry = (action) => {
  if (action?.type === "add") myRef.current += 1;
  dispatch(action);
}
    return {state, dispatch, myRef, myDispatchCherry};
};

export default useCherry;

export default function App() {
  const {state, myRef, myDispatchCherry} = useCherry();
  return (
    <div>
      <p>{`Cherry: ${state}`}</p>
      <button
    type="button"
    onClick={() => {
        console.log(`myRef count before adding: ${myRef.current}`); // Logs 0
        myDispatchCherry({ type: "add" });
        console.log(`myRef count after adding: ${myRef.current}`); // Logs 1
    }}
>
    Add more cherry
</button>
    </div>
  );
}
John Li
  • 6,976
  • 3
  • 3
  • 27
0

I don't understand why would cherryRef.current return an old value after calling dispatchCherry(). In theory it should return the incremented one, as myRef is an object.

Yes, it does return the incremented value, but since it does not complete the trigger of a re-render yet, it is not shown to you (because useRef is not exactly a state variable). The useState/useReducer hook can be used to manually trigger, or you can change myRef to a state variable.

Example, this can give you next updated value:

dispatch({ type: "add" });
setTimeout(() => {
  console.log(cherryRef.current); // Logs 1
}, 0);
Jishan Shaikh
  • 1,572
  • 2
  • 13
  • 31
  • I'm literally calling `console.log()` before and after, so how is it related to "not showing it to me"? – Robo Robok Jun 11 '23 at 19:04
  • 1
    I meant to say it does not trigger the re-render yet, just after the dispatch({ type: "add" });, try adding a sleep timer (setTimeout) to validate this. Also, edited ans. – Jishan Shaikh Jun 11 '23 at 19:17
  • I know it doesn't cause re-rendering, but it has nothing to do with the problem. – Robo Robok Jun 11 '23 at 19:18