简体   繁体   中英

Is it okey to use side effects in the useState hook callback?

Imagine situation:

const [value, setValue] = useState(false);

const setSomething = (val) => {
  setValue((prev) => {
    fn(); dispatch(action); // or any other side effect
    
    return prev + val;
  });
};

Is it programmatically okey and fine with react principles to call side effects inside useState callback? May it affect the render process somehow?

It is not ok to use side effects inside the updater function . It might affect the render process, depending on the specific side effect.

It is not fine with react principles (separation of concerns, declarative code).

(I remember to have seen some exceptional use cases where putting some code inside the updater function was said to be the only solution, but I can't remember what it was. I'd appreciate an example in the comments.)

1. Consequences of using side effects

It is not ok to use side effects, basically for the same reasons why you shouldn't use side effects outside useEffect anywhere else.

Some side effects might affect the render process, other side effects might work fine (technically), but you are not supposed to rely on what happens inside the setter functions.

React guarantees that eg if you call setState( prev => prev + 1 ) , then state would now be one more than before.

React does not guarantee what will happen behind the scenes to achieve that goal. React might call these setter functions multiple times , or not at all, and in any order:

StrictMode - Detecting unexpected side effects

... Because the above methods might be called more than once, it's important that they do not contain side-effects. ...

2. following react principles

You should not put side effects inside the updater function, because it validates some principles, like separation of concerns and writing declarative code.

Separation of concerns:

setCount should do nothing but setting the count .

Writing declarative code:

Generally, you should write your code declarative, not imperative .

  • Ie your code should "describe" what the state should be, instead of calling functions one after another.
  • Ie you should write "B should be of value X, dependent on A" instead of "Change A, then change B"

In some cases React doesn't "know" anything about your side effects, so you need to take care about a consistent state yourself.

Sometimes you can not avoid writing some imperative code.

useEffect is there to help you with keeping the state consistent, by allowing you to eg relate some imperative code to some state, aka. "specifying dependencies". If you don't use useEffect , you can still write working code, but you are just not using the tools react is providing for this purpose . You are not using React the way it is supposed to be used, and your code becomes less reliable.

Examples for problems with side effects

Eg in this code you would expect that A and B are always identical, but it might give you unexpected results , like B being increased by 2 instead of 1 (eg when in DEV mode and strict mode ):

export function DoSideEffect(){
  const [ A, setA ] = useState(0);
  const [ B, setB ] = useState(0);

  return <div>
    <button onClick={ () => {
      setA( prevA => {                // <-- setA might be called multiple times, with the same value for prevA
        setB( prevB => prevB + 1 );   // <-- setB might be called multiple times, with a _different_ value for prevB
        return prevA + 1;
      } );
    } }>set count</button>
    { A } / { B }
  </div>;
}

Eg this would not display the current value after the side effect, until the component is re-rendered for some other reason, like increasing the count :

export function DoSideEffect(){
  const someValueRef = useRef(0);
  const [ count, setCount ] = useState(0);

  return <div>
    <button onClick={ () => {
      setCount( prevCount => {
        someValueRef.current = someValueRef.current + 1; // <-- some side effect
        return prevCount; // <-- value doesn't change, so react doesn't re-render
      } );
    } }>do side effect</button>

    <button onClick={ () => {
      setCount(prevCount => prevCount + 1 );
    } }>set count</button>

    <span>{ count } / {
      someValueRef.current // <-- react doesn't necessarily display the current value
    }</span>
  </div>;
}

No, it is not ok to issue side-effects from a state updater function, it is to be considered a pure function .

  1. The function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams), and
  2. The function application has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).

You may, or may not, be using the React.StrictMode component, but it's a method to help detect unexpected side effects .

Detecting unexpected side effects

Conceptually, React does work in two phases:

  • The render phase determines what changes need to be made to eg the DOM. During this phase, React calls render and then compares the result to the previous render.
  • The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like componentDidMount and componentDidUpdate during this phase.

The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

Render phase lifecycles include the following class component methods:

  • constructor
  • componentWillMount (or UNSAFE_componentWillMount )
  • componentWillReceiveProps (or UNSAFE_componentWillReceiveProps )
  • componentWillUpdate (or UNSAFE_componentWillUpdate )
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • setState updater functions (the first argument) <--

Because the above methods might be called more than once, it's important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state. Unfortunately, it can be difficult to detect these problems as they can often be non-deterministic.

Strict mode can't automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructor , render , and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState ) <--
  • Functions passed to useState , useMemo , or useReducer

Take a cue from the two highlighted bullet points regarding the intentional double-invoking of state updater functions and treat the state updater functions as pure functions.

For the code snippet you shared, I see no reason at all for the functions to be called from within the updater callback. They could/should be called outside the callback.

Example:

const setSomething = (val) => {
  setValue((prev) => {
    return prev + val;
  });
  fn();
  dispatch(action);
};

I would not

Just because it works doesn't mean it's a good idea. The code sample you shared will function, but I wouldn't do it.

Putting unrelated logic together will confuse the next person who has to work with this code; very often, that "next person" is you : you, six months from now, after you've forgotten all about this code because you finished this feature and moved on. And now you come back and discover that some of the silverware has been stored in the bathroom medicine cabinet, and some of the linens are in the dishwasher, and all the plates are in a box labeled "DVDs".

I don't know how serious you are about the specific code sample you posted, but in case it's relevant: if you're using dispatch that means you've set up some kind of reducer, either with the useReducer hook, or possibly with Redux. If that's true, you should probably consider whether this boolean belongs in your Redux store, too:

const [ value, setValue ] = useState(false)

function setSomething(val) {
  fn()
  dispatch({ ...action, val })
}

(But it might not, and that's fine!)

If you're using actual Redux, you'll also have action-creators, and that's generally the correct place to put code that triggers side effects.

Regardless of whatever state tech you're using, I think you should prefer to avoid putting side-effect code into your individual components. The reason is that components are generally supposed to be reusable, but if you put a side-effect into the component that is not essential to display or interaction of the thing being visualized by the component, then you've just made it harder for other callers to use this component.

If the side-effect is essential to how this component works, then a better way to handle this would be to call setValue and the side-effect function directly instead of wrapping them up together. After all, you don't actually depend on the useState callback to accomplish your side-effect.

const [ value, setValue ] = useState(false)

function setSomething(val) {
  setValue(value + val)
  fn()
  dispatch(action)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM