Reactjs.org: how to change the state of parent component from child Component if parent state is maintained using useStateHook

Created on 15 Feb 2019  路  12Comments  路  Source: reactjs/reactjs.org

Hi Team,

I have started using the hooks. And come across a scenario where i have to change the state of the parent component from the child component . But I am maintaining the parent component state using useState hook . So i am not getting access to the function to change this state from the child component . Can you provide information on how can I achieve this.

Thanks
Girish hT S

Most helpful comment

Hi!

There's no difference in this scenario between function and class components. In both cases, you want to pass a callback function down to the parent. Let's begin with a class example:

class Parent extends React.Component {
  state = { value: "" }

  handleChange = (newValue) => {
    this.props.setState({ value: newValue });
  }

  render() {
    // We pass a callback to MyInput
    return <MyInput value={this.state.value} onChange={this.handleChange} />
  }
}

class MyInput extends React.Component {
   handleChange = (event) => {
    // Here, we invoke the callback with the new value
    this.props.onChange(event.target.value);
  }

  render() {
    return <input value={this.props.value} onChange={this.handleChange} />
  }
}

With hooks, we can follow the same pattern and pass a callback function down to MyInput:

function Parent() {
  const [value, setValue] = React.useState("");

  function handleChange(newValue) {
    setValue(newValue);
  }

  // We pass a callback to MyInput
  return <MyInput value={value} onChange={handleChange} />;
}

function MyInput(props) {
  function handleChange(event) {
    // Here, we invoke the callback with the new value
    props.onChange(event.target.value);
  }

  return <input value={props.value} onChange={handleChange} />
}

All 12 comments

Hi!

There's no difference in this scenario between function and class components. In both cases, you want to pass a callback function down to the parent. Let's begin with a class example:

class Parent extends React.Component {
  state = { value: "" }

  handleChange = (newValue) => {
    this.props.setState({ value: newValue });
  }

  render() {
    // We pass a callback to MyInput
    return <MyInput value={this.state.value} onChange={this.handleChange} />
  }
}

class MyInput extends React.Component {
   handleChange = (event) => {
    // Here, we invoke the callback with the new value
    this.props.onChange(event.target.value);
  }

  render() {
    return <input value={this.props.value} onChange={this.handleChange} />
  }
}

With hooks, we can follow the same pattern and pass a callback function down to MyInput:

function Parent() {
  const [value, setValue] = React.useState("");

  function handleChange(newValue) {
    setValue(newValue);
  }

  // We pass a callback to MyInput
  return <MyInput value={value} onChange={handleChange} />;
}

function MyInput(props) {
  function handleChange(event) {
    // Here, we invoke the callback with the new value
    props.onChange(event.target.value);
  }

  return <input value={props.value} onChange={handleChange} />
}

Thanks Philip . I had followed the same approach before posting this question . But i had made small typo while using the use state ( used { } instead of [ ]). Because of that it was not working.

Type Error one :
const {value, setValue} = React.useState("");

Correct One:
const [value, setValue] = React.useState("");

After making the change it started working

Thanks
Girish T S

@philipp-spiess what if in your scenario the developer wanted to avoid unnecessarily re-rendering the child component (because handleChange is created on each render and is a prop of the child)?

The only ways to generally solve this problem seems to be by using useReducer, which allows the event argument (payload in the action), since there's no way to "Lift up state" because the event can't be controlled by the parent: https://reactjs.org/docs/lifting-state-up.html

Or am I missing something?

@evbo Depends on what the function is doing! Check out useCallback to create a memoized function which won't change unless the dependencies change.

If there are lots of dependencies than you are right, a useReducer and passing the stable dispatch function down might be a better option. The state can live in the parent and you call dispatcher with the event (or a subset of the event data necessary to calculate the next state).

Thanks helping to wrap my head around react!

I suppose there are a couple other tricks that may be useful in special scenarios where the child must modify state based on some event or data not owned by the parent:

  • have the child own the state - so that _only_ it updates (not the whole parent)
  • have the child only modify a ref variable of the parent (ref changed by child won't cause re-render, but something else still needs to trigger state change _eventually_)

These cases are outlined nicely here: https://www.codebeast.dev/usestate-vs-useref-re-render-or-not/#what-causes-re-rendering

Also, side note: I don't always like the coding convention with useReducer. I think it's a matter of taste having all the state change "business logic" colocated. Did anyone ever consider allowing useCallback to permit sending an argument? e.g:

const memoizedCallback = useCallback(
  (event) => {
    doSomething(a, b, event);
  },
  [a, b],
);

I'm pretty certain that this is a valid pattern! Especially since it is equivalent to this useMemo version:

const memoizedCallback = React.useMemo(() => {
  return event => {
    doSomething(a, b, event);
  };
}, [a, b]);

Thank you, all of the docs had me confused no arguments should be provided.

I guess the author was trying to keep it tl;dr friendly, so this is good to know!

I tried this solution, but it only works on the first invocation in my child component.

const Parent = () => {
  const [carouselIndex, setCarouselIndex] = useState(0);
  const [translateValue, setTranslateValue] = useState(0);

  const handleChangeHero = () => {
    console.log("I'm in the parent component"); // This line gets called successfully every time it's invoked from the child, so the child and parent seem to be communicating.
    setCarouselIndex(carouselIndex + 1); // carouselIndex gets updated correctly on the first call (going from 0 to 1), but on every subsequent call from the child, this line gets ignored.
    setTranslateValue(translateValue + -(slideWidth())); // Same problem with this line: gets called on the first invocation only, then gets ignored on subsequent calls.
  }

  return (
    <Child changeHero={handleChangeHero} />
  )
}

const Child = props => {
  const handleChangeHero = () => {
    console.log("I'm in the child component"); // This line is called successfully every time this handler is invoked in the child. It's being used with "react-with-gesture".
    props.changeHero();
  };
 return (<div>I'm a child</div>);
}

How do I get setCarouselIndex to get called every time in the parent, and have that state value updated?

Philip, this is great, but I tried something similar with multiple inputs and it doesn't work. If you could provide an example with multiple inputs that use one handleChange function based on their name, that would be super helpful! Thanks!

Hi, I have a problem when trying to update the parent state via child component in a functional component.

`const departChanged = (e) => {
  const selectedPoint = e.suggestion.latlng
  const newMarkers = {...markers}

  newMarkers[0] = new L.marker(selectedPoint, {icon: blueIcon})
  setMapCenter([selectedPoint.lat, selectedPoint.lng])
  setMarkers(newMarkers)
}`

I have this method in the parent component called from the child component but all the state values I access are the BASE values of each state variables, even if they are correctly changed beforehand.
Any ideas ?

How could i test a similar scenario? I have already written functionality to pass useState setter method and value as props to child component. These values are set in the child component. How can i replicate this in a unit test with jest? So far i have

    it('button becomes enabled when there are selected sections', () => {
        const initState = [];
        const setState = jest.fn((section) => [...initState, section]);
        const useStateMock = (initState) => [initState, setState];

        jest.spyOn(React, 'useState').mockImplementation(useStateMock);
        const wrapper = getCourseListViewWrapper();
        const courseList = wrapper.find(CourseList);
        courseList.props().updateSelectedSections(sections[0]);

        expect(wrapper.find('[id$="_FooterRegisterButton"]').props().disabled).toBe(false);
      });

The last line fails, the mock useState func fires yet the state in parent doesnt seem to reflect the change, disabled is still true but should now be false. The getCourseListViewWrapper(); is return a shallow render via enzyme.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andresmatasuarez picture andresmatasuarez  路  5Comments

franciscop-invast picture franciscop-invast  路  6Comments

msert29 picture msert29  路  5Comments

gaearon picture gaearon  路  6Comments

dstudzinski picture dstudzinski  路  3Comments