Sergey Kostrukov
Sergey Kostrukov

Reputation: 1150

Delayed invocation useEffect causes inconsistencies of component model

When dependencies of useEffect hook change, the effect function of the useEffect hook being invoked only after the render cycle (more discussed here). This frequently creates situations when UI become inconsistent due to inconsistency in the component model.

Here is a simple example where useEffect used to run computation of resultValue based on value of inputValue:

function App() {
  const [inputValue, incrementInputValue] = useReducer((s, _) => s + 1, 0);
  const [resultValue, setResultValue] = useState(0);

  useEffect(() => {
    // It could be an asynchronous call to business logic etc.
    setResultValue(inputValue * 2);
  }, [inputValue]);

  console.log("Render: %d * 2 = %d", inputValue, resultValue);

  return (
    <div className="App">
      <p>{inputValue} * 2 = {resultValue}</p>
      <button onClick={() => { console.log('Click'); incrementInputValue(undefined); }}>
        Increment
      </button>
    </div>
  );
}

This code returns the following log:

Render: 0 * 2 = 0
Click 
Render: 1 * 2 = 0 // inputValue was updated, but useEffect not called yet
Render: 1 * 2 = 2
Click 
Render: 2 * 2 = 2 // inputValue was updated, but useEffect not called yet
Render: 2 * 2 = 4

Code Sandbox

What would be the best approach to avoid such inconsistencies caused by delayed invocation of useEffect?

Upvotes: 1

Views: 235

Answers (1)

Dennis Vash
Dennis Vash

Reputation: 53964

There is an entire lifecycle that runs after dispatching an action (incrementInputValue), you won't make it consistent without "removing" this lifecycle:

  1. Clicking a button.
  2. Dispatching an action (incrementInputValue)
  3. Re-rendering duo to a state change (inputValue)
  4. Running useEffect callback duo to inputValue change.
  5. Re-rendering duo to a state change (resultValue)

Therefore as said, you need to get rid of phase 3.

There are some solutions, you can use a reference, or combine it within a single state.

const reducer = (state, increase) => ({
  inputValue: state.inputValue + increase,
  resultValue: state.resultValue + increase * 2
});

const initial = { inputValue: 0, resultValue: 0 };

function App() {
  const [state, increaseResult] = useReducer(reducer, initial);

  useEffect(() => {
    console.log(state);
  });

  return (
    <div className="App">
      <p>
        {state.inputValue} * 2 = {state.resultValue}
      </p>
      <button
        onClick={() => {
          console.log('Click');
          increaseResult(1);
        }}
      >
        Increment
      </button>
    </div>
  );
}

What is the best approach? It depends on the use case, its more a question of when and why using a reference instead of the state.

In this particular example one can argue that you can derive all logic with just division: inputValue = resultValue / 2.

Edit gifted-poitras-3bng4

Upvotes: 1

Related Questions