React: Feature/Discussion: functional setState was a better version

Created on 4 Feb 2020  路  7Comments  路  Source: facebook/react

In React hooks, to produce an effect similar to functional setState version we will use React.useEffect. However, there is a situation which hooks don't cover. Let's say I have a handleChange function as following

const handleChange = (value: any, name: string, rules: Rule[]) => {
    setState({ ...state, [name]: value });
};

Now if I would want to call one function handleError(rules: Rule[], name: string, value: any) with the same arguments from parent function ( in this case handleChange), I have no way to access those arguments as useEffect hook has to be outside the function and directly inside the component.

React.useEffect(() => {
    handleError() // no access to arguments of rules, name, value
}, [state, currentField]);

Only way to access those arguments is to set them as state variable and use it inside useEffect But imagine the task would have been just passing on the arguments to yet another function i.e. setState's callback if and only if setState supported the functional callback signature. Not sure why it was removed.

Missing to do this.

const handleChange = (value: any, name: string, rules: Rule[]) => {
    setState({ ...state, [name]: value }, handleError(rules: Rule[], name: string, value: any) );
};
Stale Unconfirmed

Most helpful comment

The implementation of the other hook you reference (use-state-with-callback) is very naive:

const useStateWithCallback = (initialState, callback) => {
  const [state, setState] = useState(initialState);

  useEffect(() => callback(state), [state, callback]);

  return [state, setState];
};

I think an implementation of what you're describing could look something like what I've written below, but there are a some potential problems which I want to enumerate first:

  1. If multiple state updates were batched into a single render/commit, it would only call the most recent callback function.
  2. Because the callback function was created before the new render, any props values it closed over might be stale. This could cause problems if your state update was batched along with something else that also updated props. (To work around this you could use another ref, but this starts to get complicated.)
const DANGEROUS_useStateWithCallback = initialValue => {
  const [state, setState] = useState({
    callback: null,
    value: initialValue
  });

  const setStateWrapper = useCallback((newValue, callback) => {
    setState({
      callback,
      value: newValue
    });
  }, []);

  const prevValueRef = useRef(null);

  useLayoutEffect(() => {
    // These are the latest state values-
    // the ones that were just rendered and are being committed.
    const { callback, value } = state;

    if (typeof callback === "function") {
      const prevValue = prevValueRef.current;
      callback(
        value, // current state value
        prevValue // previous state value
      );
    }

    // This holds the state value of the previous render.
    prevValueRef.current = value;
  }, [state]);

  return [state.value, setStateWrapper];
};

I don't think I would recommend using such a hook as shown above. I'd suggest just using useState, useEffect, and useRef (if needed, to track previous state values) as shown in the docs.

All 7 comments

This is certainly not a bug.

@bvaughn You are right. I will have to change the tile to the "feature/discussion" category maybe.

The hooks documentation has an example for how to track previous props/state values:
https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state

In your case that might look something like:

const [state, setState] = useState({
  map: {},
  changes
});
const prevStateRef = useRef(null);

const handleChange = (value, name, rules) => {
  // It's not clear what "rules" is or how it's used
  setState({
    ...state,
    [name]: value
  });
};

useEffect(() => {
  // At this point you can compare prev and current state...

  prevStateRef.current = state;
}, [state]);

If you also needed to track the most recently changed name/value/rules you could add those into the state object as well (so you could inspect them in the effect).

FWIW the docs also say that:

It鈥檚 possible that in the future React will provide a usePrevious Hook out of the box since it鈥檚 a relatively common use case.

Missing to do this.

const handleChange = (value, name, rules) => {
   setState({ ...state, [name]: value }, () => handleError(rules, name, value) );
}

Maybe we can "rebuild" the callback functionality in setState using custom hooks?

Here is a quick prototype.

Usage:

let [state, setStateWithOptionalCallback] = useStateWithCallback(0);

 let handleChange = value => {
    setStateWithOptionalCallback(value, () => {
      console.log(
        "this is the callback, after paint",
        value
      );
    });
  };

Prototype Implementation:

const useStateWithCallback = initialState => {
  const [state, setState] = React.useState(initialState);

  let callbackRef = React.useRef({});

  // maybe React.useCallback usage is overkill?
  let setStateWithOptionalCallback = React.useCallback(
    (setStateProps, callback) => {
      callbackRef.current = { callback, setStateProps };
      setState(setStateProps);
    },
    []
  );
  // or maybe use React.useLayoutEffect ?
  React.useEffect(() => {
    callbackRef.current.callback &&
      callbackRef.current.callback(callbackRef.current.setStateProps);
  }, [state]);

  return [state, setStateWithOptionalCallback];
};

Demo:
https://codesandbox.io/s/reactjs-usestatewithcallback-prototype-fkub9

I don't know whether this implementation guarantees that the correct callbacks are executed for the given setState usage, in case the user spam-clicks the button.


Some other custom hooks exists with alternative API.
e.g.

import useStateWithCallback from 'use-state-with-callback';

const [count, setCount] = useStateWithCallback(0, count => {
    if (count > 1) {
      console.log('Threshold of over 1 reached.');
    } else {
      console.log('No threshold reached.');
    }
  });

The implementation of the other hook you reference (use-state-with-callback) is very naive:

const useStateWithCallback = (initialState, callback) => {
  const [state, setState] = useState(initialState);

  useEffect(() => callback(state), [state, callback]);

  return [state, setState];
};

I think an implementation of what you're describing could look something like what I've written below, but there are a some potential problems which I want to enumerate first:

  1. If multiple state updates were batched into a single render/commit, it would only call the most recent callback function.
  2. Because the callback function was created before the new render, any props values it closed over might be stale. This could cause problems if your state update was batched along with something else that also updated props. (To work around this you could use another ref, but this starts to get complicated.)
const DANGEROUS_useStateWithCallback = initialValue => {
  const [state, setState] = useState({
    callback: null,
    value: initialValue
  });

  const setStateWrapper = useCallback((newValue, callback) => {
    setState({
      callback,
      value: newValue
    });
  }, []);

  const prevValueRef = useRef(null);

  useLayoutEffect(() => {
    // These are the latest state values-
    // the ones that were just rendered and are being committed.
    const { callback, value } = state;

    if (typeof callback === "function") {
      const prevValue = prevValueRef.current;
      callback(
        value, // current state value
        prevValue // previous state value
      );
    }

    // This holds the state value of the previous render.
    prevValueRef.current = value;
  }, [state]);

  return [state.value, setStateWrapper];
};

I don't think I would recommend using such a hook as shown above. I'd suggest just using useState, useEffect, and useRef (if needed, to track previous state values) as shown in the docs.

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

gaearon picture gaearon  路  227Comments

iammerrick picture iammerrick  路  94Comments

gaearon picture gaearon  路  126Comments

addyosmani picture addyosmani  路  143Comments

acdlite picture acdlite  路  83Comments