Make useMemo recalculate only when dependency changes to certain value, not on all of its changes

Viewed 39

In the code below, I want successValue to be recalculated only when the state changes to 'success', and not 'fail' or 'loading' or 'init'. Therefore, I am providing state === success as dependency to useMemo:

import React, { useMemo, useEffect, useState } from 'react';
import { render } from 'react-dom';

type Status = 'init' | 'loading' | 'fail' | 'success'

const App = () => {
  const [state, setState] = useState<Status>('init')

  const successValue = useMemo(() => {
    if (state === 'success') {
      return 'success value'
    }
    return 'fallback value'
  }, [state === 'success'])

  useEffect(() => {
    setTimeout(() => setState('success'), 1000)
  }, [])

  return (
    <div>
      <div>Status: {state}</div>
      <div>Success value: {successValue}</div>
      <button onClick={() => setState('loading')}>Loading</button>
      <button onClick={() => setState('fail')}>Fail</button>
      <button onClick={() => setState('success')}>Success</button>
    </div>
  )
}

render(<App />, document.getElementById('root'));

Here is an interactive demo: you will see that if you click 'fail' button for example, the successValue will change to fallback value - and I don't want that, I want it to stay as success value.

I think I misunderstood useMemo or how the dependencies work. Can someone provide and explanation and help me fix this?

1 Answers

I don't think useMemo alone will quite work for this use case because you want to recalculate it conditionally - but useMemo, if the dependency array changes, will unconditionally compute a new value.

Either store the prior value in state or a ref, then return that instead of returning 'fallback value', or do the calculation with useState / useEffect instead.

const { useState, useEffect } = React;
const App = () => {
  const [state, setState] = useState('init')
  const [successValue, setSuccessValue] = useState();
  useEffect(() => {
    if (state === 'success') {
      setSuccessValue('success value');
    }
  }, [state === 'success']);
  useEffect(() => {
    setTimeout(() => setState('success'), 1000)
  }, [])

  return (
    <div>
      <div>Status: {state}</div>
      <div>Success value: {successValue}</div>
      <button onClick={() => setState('loading')}>Loading</button>
      <button onClick={() => setState('fail')}>Fail</button>
      <button onClick={() => setState('success')}>Success</button>
    </div>
  )
}

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

Related