221

I am using functional component with hooks. I need to update state in parent from a child. I am using a prop function in Parent. All works fine except my prop function is getting the previous state and not the current state. My prop function gets executed before useState hook setting current state. How can I can I wait for my call back function to be executed after useState call. I am looking for something like setState(state,callback) from class based components.

Here is the code snippet:

function Parent() {
  const [Name, setName] = useState("");
  getChildChange = getChildChange.bind(this);
  function getChildChange(value) {
    setName(value);
  }

  return <div> {Name} :
    <Child getChildChange={getChildChange} ></Child>
  </div>
}

function Child(props) {
  const [Name, setName] = useState("");
  handleChange = handleChange.bind(this);

  function handleChange(ele) {
    setName(ele.target.value);
    props.getChildChange(collectState());
  }

  function collectState() {
    return Name;
  }

  return (<div>
    <input onChange={handleChange} value={Name}></input>
  </div>);
} 
Abhijith Sasikumar
  • 13,262
  • 4
  • 31
  • 45
Atul
  • 3,013
  • 2
  • 12
  • 15
  • 6
    why don't you just pass down `setName` and call it from the child? – dan-klasson Mar 02 '19 at 03:41
  • I hope we get interesting comments in this thread https://github.com/facebook/react/issues/17969 – RajaSekhar K Apr 19 '20 at 03:40
  • There's an easy way to do this without useEffect https://stackoverflow.com/a/70405577/5823517 – Tunn Dec 18 '21 at 17:37
  • Most answers are putting too much emphasis on making this behave like a class based component. That is a red herring. The real problem is that the child is calling `getChildChange` with the old value. Changing it to `props.getChildChange(ele.target.value)` would solve it. See https://stackoverflow.com/a/56267744/367796 – steinybot Jun 26 '22 at 20:47

8 Answers8

167

You can use useEffect/useLayoutEffect to achieve this:

const SomeComponent = () => {
  const [count, setCount] = React.useState(0)

  React.useEffect(() => {
    if (count > 1) {
      document.title = 'Threshold of over 1 reached.';
    } else {
      document.title = 'No threshold reached.';
    }
  }, [count]);

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
};

If you want to prevent the callback from running on first render, adjust the previous version:

const SomeComponent = () => {
  const [count, setCount] = React.useState(0)

  const didMount = React.useRef(false);

  React.useEffect(() => {
    if (!didMount.current) {
      didMount.current = true;
      return;
    }

    if (count > 1) {
      document.title = 'Threshold of over 1 reached.';
    } else {
      document.title = 'No threshold reached.';
    }
  }, [count]);

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  );
};

More about it over here.

Robin Wieruch
  • 14,900
  • 10
  • 82
  • 107
  • I'm trying to install use-state-with-callback but it doesn't work. It gives an error. What can I do? – Efe FRK Apr 29 '21 at 17:13
145

setState(updater, callback) for useState

Following implementation comes really close to the original setState callback of classes.

Improvements made to accepted answer:

  1. Callback execution is omitted on initial render - we only want to call it on state updates
  2. Callback can be dynamic for each setState invocation, like with classes

Usage

const App = () => {
  const [state, setState] = useStateCallback(0); // same API as useState

  const handleClick = () => {
    setState(
      prev => prev + 1,
      // second argument is callback, `s` being the *updated* state
      s => console.log("I am called after setState, state:", s)
    );
  };

  return <button onClick={handleClick}>Increment</button>;
}

useStateCallback

function useStateCallback(initialState) {
  const [state, setState] = useState(initialState);
  const cbRef = useRef(null); // init mutable ref container for callbacks

  const setStateCallback = useCallback((state, cb) => {
    cbRef.current = cb; // store current, passed callback in ref
    setState(state);
  }, []); // keep object reference stable, exactly like `useState`

  useEffect(() => {
    // cb.current is `null` on initial render, 
    // so we only invoke callback on state *updates*
    if (cbRef.current) {
      cbRef.current(state);
      cbRef.current = null; // reset callback after execution
    }
  }, [state]);

  return [state, setStateCallback];
}
TypeScript version
function useStateCallback<T>(
  initialState: T
): [T, (state: T, cb?: (state: T) => void) => void] {
  const [state, setState] = useState(initialState);
  const cbRef = useRef<((state: T) => void) | undefined>(undefined); // init mutable ref container for callbacks

  const setStateCallback = useCallback((state: T, cb?: (state: T) => void) => {
    cbRef.current = cb; // store current, passed callback in ref
    setState(state);
  }, []); // keep object reference stable, exactly like `useState`

  useEffect(() => {
    // cb.current is `undefined` on initial render,
    // so we only invoke callback on state *updates*
    if (cbRef.current) {
      cbRef.current(state);
      cbRef.current = undefined; // reset callback after execution
    }
  }, [state]);

  return [state, setStateCallback];
}

Further info: React Hooks FAQ: Is there something like instance variables?

Working example

const App = () => {
  const [state, setState] = useStateCallback(0);

  const handleClick = () =>
    setState(
      prev => prev + 1,
      // important: use `s`, not the stale/old closure value `state`
      s => console.log("I am called after setState, state:", s)
    );

  return (
    <div>
      <p>Hello Comp. State: {state} </p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

function useStateCallback(initialState) {
  const [state, setState] = useState(initialState);
  const cbRef = useRef(null);

  const setStateCallback = useCallback((state, cb) => {
    cbRef.current = cb; 
    setState(state);
  }, []);

  useEffect(() => {
    if (cbRef.current) {
      cbRef.current(state);
      cbRef.current = null;
    }
  }, [state]);

  return [state, setStateCallback];
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>
Emil Laine
  • 41,598
  • 9
  • 101
  • 157
ford04
  • 66,267
  • 20
  • 199
  • 171
  • what does `cbRef.current(state);` do in this code inside of the conditional in the `useEffect`? – bot19 Jun 17 '21 at 01:50
  • 1
    @bot19 this is the actual invocation of the callback, which has previously been set via `setState(..,cb)`. `cbRef.current` stores a function. This function is then called - `(state)` - with the current, updated state. – ford04 Jun 17 '21 at 04:26
  • The problem with this snippet is that it _wont_ trigger if the state is identical. This is never a problem with class companents, because you always create a new state object. https://codesandbox.io/s/thirsty-tamas-7cqbp?file=/src/App.js – dwjohnston Sep 06 '21 at 03:01
  • Alternative solution: https://codesandbox.io/s/cocky-mendel-tqh58?file=/src/App.js – dwjohnston Sep 06 '21 at 03:07
  • 1
    @dwjohnston [bailing out of state updates](https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update) in case of identical values is the new React default for Hooks - hence I wouldn't change this behavior in most cases. If you really need to be inline with old class-based comparison for legacy reasons (which behaves this way due to object merge), the codesandbox approach looks reasonable! Instead of using `Symbol`, you might as well wrap the state value in a new object container every time. – ford04 Sep 08 '21 at 12:00
  • @ford04 This is nice solution, but it is not 100% replacement of the original `this.setState`. 1) you call the callback only when the state actually changes, the original one called it always `this.setState(prevState => prevState, () => console.log('Changed'));` 2) You support only 1 callback at the time, the last one. There should be an array of callbacks. You can call your `setState` twice with different callbacks. The simplest solution is to use array of callbacks and probably use only this array as deps in useEffect - `useEffect(..., [cbRefs.current]` – Petr Újezdský Sep 13 '21 at 17:33
  • 2
    https://codesandbox.io/s/gifted-dhawan-hedsp?file=/src/App.tsx - I have used the array and added current state option to the callback. There is also example with the double-update. Hope it helps somebody :) – Petr Újezdský Sep 13 '21 at 18:43
  • 1
    @PetrÚjezdský thanks for your ideas! Re 1: I guess, [this comment](https://stackoverflow.com/questions/54954091/how-to-use-callback-with-usestate-hook-in-react/61842546#comment122132003_61842546) fits well. Re 2: If you call `setState` twice during *same* render cycle and same hook instance, the last value wins in React. So I would expect same behavior when setting a callback and would rather be confused, if both old *and* new callback are invoked. Both that seems to be rather an edge case anyway - most probably you will have an event handler, where setting state is done in different renders – ford04 Sep 18 '21 at 11:27
  • It would require even more overengineering, @Organic, every single time you want to cause a side effect on certain state updates only. You'd have to track different things with various refs and what not. At least this setup helps make it reusable (although still seems brittle if there are several state updates in a row) – Azarro May 16 '22 at 10:54
  • first. time after run npm run ios, the callback does not called, it's only call after next hot reload – famfamfam May 31 '22 at 06:47
43

With React16.x and up, if you want to invoke a callback function on state change using useState hook, you can use the useEffect hook attached to the state change.

import React, { useEffect } from "react";

useEffect(() => {
  props.getChildChange(name); // using camelCase for functions is recommended.
}, [name]); // this will call getChildChange on initial render and when ever name changes.
Abhijith Sasikumar
  • 13,262
  • 4
  • 31
  • 45
  • 6
    What should we do if there is more than one function and only one of them needs to work in the replay? – Gucal Jan 19 '21 at 10:29
  • 1
    @Gucal you can use useEffect multiple times like `useEffect(() => loadFunctionAOnce()). useEffect(() => loadFunctionBIfNameChange(), [name])` – DAMIEN JIANG May 03 '21 at 21:21
  • 2
    This will also run the props.getChildChange on initial render – 0xAnon Sep 15 '21 at 07:45
14

Actually, you should avoid using this when using react hooks. It causes side effects. That's why react team create react hooks.

If you remove codes that tries to bind this, you can just simply pass setName of Parent to Child and call it in handleChange. Cleaner code!

function Parent() {
  const [Name, setName] = useState("");

  return <div> {Name} :
    <Child setName={setName} ></Child>
  </div>
}

function Child(props) {
  const [Name, setName] = useState("");

  function handleChange(ele) {
    setName(ele.target.value);
    props.setName(ele.target.value);
  }

  return (<div>
    <input onChange={handleChange} value={Name}></input>
  </div>);
} 

Moreover, you don't have to create two copies of Name(one in Parent and the other one in Child). Stick to "Single Source of Truth" principle, Child doesn't have to own the state Name but receive it from Parent. Cleanerer node!

function Parent() {
  const [Name, setName] = useState("");

  return <div> {Name} :
    <Child setName={setName} Name={Name}></Child>
  </div>
}

function Child(props) {    
  function handleChange(ele) {
    props.setName(ele.target.value);
  }

  return (<div>
    <input onChange={handleChange} value={props.Name}></input>
  </div>);
} 
徐銘谷
  • 293
  • 2
  • 8
10

we can write customise function which will call the callBack function if any changes in the state

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const useStateCallbackWrapper = (initilValue, callBack) => {
  const [state, setState] = useState(initilValue);
  useEffect(() => callBack(state), [state]);
  return [state, setState];
};

const callBack = state => {
  console.log("---------------", state);
};
function App() {
  const [count, setCount] = useStateCallbackWrapper(0, callBack);
  return (
    <div className="App">
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>+</button>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

`

Sunil Kumar
  • 420
  • 4
  • 13
  • This solution fails at production build with `React Hook useEffect has a missing dependency: 'callBack'. Either include it or remove the dependency array. If 'callBack' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps` – Pablo Albaladejo Feb 07 '20 at 16:30
  • try keeping the useEffect line like `useEffect(() => callBack?callBack(state):null, [state, callBack]);` – RajaSekhar K Apr 18 '20 at 19:11
6

Another way to achieve this:

const [Name, setName] = useState({val:"", callback: null});
React.useEffect(()=>{
  console.log(Name)
  const {callback} = Name;
  callback && callback();
}, [Name]);
setName({val:'foo', callback: ()=>setName({val: 'then bar'})})
james h
  • 115
  • 1
  • 4
4

you can utilize useCallback hook to do this.

function Parent() {
  const [name, setName] = useState("");
  const getChildChange = useCallback( (updatedName) => {
    setName(updatedName);
  }, []);

  return <div> {name} :
    <Child getChildChange={getChildChange} ></Child>
  </div>
}

function Child(props) {
  const [name, setName] = useState("");

  function handleChange(ele) {
    setName(ele.target.value);
    props.getChildChange(ele.target.value);
  }

  function collectState() {
    return name;
  }

  return (<div>
    <input onChange={handleChange} value={name}></input>
  </div>);
}
  • 1
    Setting state in two components for the same variable doesn't sound like a good idea to me. – Isaac Pak Aug 28 '20 at 20:44
  • ```useState Hook doesn't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect()```. This is what i am getting after putting callback – Hidayt Rahman Jun 07 '21 at 05:36
  • Hey @dishwasherWithProgrammingSkill , whats usage of this code ? whats different to setState inline like this : ` setValue(value)} >` – tmohammad78 Nov 26 '21 at 19:28
3

function Parent() {
  const [Name, setName] = useState("");
  getChildChange = getChildChange.bind(this);
  function getChildChange(value) {
    setName(value);
  }

  return <div> {Name} :
    <Child getChildChange={getChildChange} ></Child>
  </div>
}

function Child(props) {
  const [Name, setName] = useState("");
  handleChange = handleChange.bind(this);
  collectState = collectState.bind(this);
  
  function handleChange(ele) {
    setName(ele.target.value);
  }

  function collectState() {
    return Name;
  }
  
   useEffect(() => {
    props.getChildChange(collectState());
   });

  return (<div>
    <input onChange={handleChange} value={Name}></input>
  </div>);
} 

useEffect act as componentDidMount, componentDidUpdate, so after updating state it will work

Kirill
  • 180
  • 4