2

how do I call a function of a child component in the parent component when using setState with a callback function?

Background information

Three components of a simple quiz app:

App.jsx  
-- Questions.jsx  
-- Results.jsx 

To access the answers in the results component AND the questions component, the answers must be saved in the state of the app component.

To modify the state of the app component, each time the user answers a question, I need to call a function that is passed down from the app component to the questions component. After setting the State of the app component, the next question should be displayed. Hence, the function nextQuestion() in the questions component should be called. Normally, I would just write setState({results: results}, nextQuestion()) but since nextQuestion() is a function of the questions component and not of the app component, this does not work.

How can I solve this problem?

Thank you very much!

In the questions component:

checkAnswer = (answer) => {

    if (answer !== this.state.solution) {

      let s = this.state;
      this.props.saveResult(s.question, s.solution[0], answer);

      //[...]
    }
  };

newQuestion = () => {
    //[...]

    let question = [...this.state.question];
    let solution = [...this.state.solution];

    const task = taskGenerator.taskDirectory[taskId](); //generate new question + solution
    question = task.question;
    solution = task.solution;
    this.setState({ question, solution });
  };

In the app component:

saveResult = (question, solution, answer) => {
    let results = cloneDeep(this.state.results)
    results.question = question
    results.solution = solution
    results.answer = answer
    this.setState({ results: results }, newQuestion()); //here is the problem; how do I call the newQuestion function of the child component?
  };
John
  • 23
  • 4

3 Answers3

1
class App extends Component {
...
  setAnswers(answers, callback){
     this.setState({results:answers},callback)
  }
  render(){
    return <Questions setAnswers={this.setAnswers}/>
  }
}

class Questions extends Component {

  onSubmit(answers){
     this.props.setAnswers(answers,this.nextQuestion)
  }
}

another way would be to react to state change in Questions child component, probably the better way.

Anuja
  • 908
  • 6
  • 11
  • Thanks! If I understand your proposal correctly, then the function nextQuestion is passed as an argument to the parents function and is executed there after setState is successfully executed. As a consequence, in the function nextQuestion, I cannot access and modify the state of the questions component anymore, can I? – John Jun 14 '21 at 16:30
  • you can if thats what you want. you can pass ```this.state``` to the callback. ```this.setState({results: answers}, ()=>callback(this.state))``` which then will receive the modified state in ```Question``` component. – Anuja Jun 14 '21 at 16:45
  • Okay, did not see that! Thanks! – John Jun 14 '21 at 18:22
1

If I understand your problem correctly, you should be able to pass the function as props to the child component from the parent. I believe this post answers your question as well. Call child method from parent

Cheers!

Jay Jones
  • 11
  • 1
1

how do I call a function of a child component in the parent component when using setState with a callback function?

In short, you don't :) Instead, you pass the state from the parent to the child. That's actually the root of your problem with:

but since nextQuestion() is a function of the questions component and not of the app component, this does not work.

There are two "rules" to follow as a React dev for where your state needs to go in your app. To understand them, you have to think of your app as a "tree", with App at the top and all children components, grandchildren components, etc. below ...

  1. The state must be at or above every component that needs to use the state
  2. The state should be as low as possible in your app tree (while still following #1)

It seems you are trying to follow rule #2 and keep your state low (good) ... but your app is breaking that first rule: you have state (nextQuestion relies on this.state.question) which your App depends on. That state is "lower" (in your app tree) than it needs to be, and since it's not at the App-level, App can't use it.

What this means is that you need to move nextQuestion (and the state(s) powering it) into App, so that your code in App can access nextQuestion. Then, you can pass any data that Question needs as props, from App.

With this structure your state (and associated functions like nextQuestion) will live as high as it needs to live (ie. at the App-level), so that all logic that relies on it has access, and any "lower" components (like Question) will simply have that state passed "down the tree" via props.

machineghost
  • 33,529
  • 30
  • 159
  • 234
  • 1
    Thanks for the comprehensive answer!! I hoped to not further "litter" the app component (>250 lines) with further functions but it seems to be that I have to. – John Jun 14 '21 at 16:23
  • Again, your desire to "keep your state low on the tree" (ie. don't shove it all into App) is a good and healthy one! But you just have to temper it with making sure every component can access the data it needs, and in this case app does need the data. – machineghost Jun 14 '21 at 19:48