Search code examples
javascriptreactjscomponentsparent-childsetstate

change state in Grandparent Component with button


Hello i'm new on react and have to do a Tutorial where i have to change the state of the Child Component with a button onClick function. currently i'm use a button in my Parent component to do it and it works but now i have to use call this button in other child components and not directly in the Parent to restart my Tutorial.

but i dont know how i can do it. ill happy about every suggestion.

class Child extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      run: this.props.run,
      stepIndex: this.props.stepIndex
    }
  }


 componentWillReceiveProps (props) {
    this.setState({ run: props.run, stepIndex: props.stepIndex })
  }

callback = (tour) => {
    const { action, index, type } = tour

    // if you delete  action === 'skip', the Tutorial will not start on other pages after skipped once.
    if (action === 'close' || action === 'skip' || type === 'tour:end') {
      this.setState({ run: false })
    } else if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) {
      this.setState({ stepIndex: index + (action === ACTIONS.PREV ? -1 : 1) })
    }
  }

 render () {
    let { run, stepIndex, steps } = this.state

    if (this.props.location.pathname === '/') {
      steps = []
      run = false
    } else if (this.props.location.pathname === '/matches/' || this.props.location.pathname.length === '/matches') {
      steps = matchSiteSteps
    } else if (this.props.location.pathname.startsWith('/matches/') && this.props.location.pathname.endsWith('/edit/')) {
      steps = matchEditorSteps
    } else if (this.props.location.pathname.startsWith('/matches/') && this.props.location.pathname.includes('sequence')) {
      steps = matchSequenceSteps
    } else if (this.props.location.pathname.startsWith('/matches/') && this.props.location.pathname.match('\\d+')) {
      steps = matchSteps
    }

    return (
      <>
        <Joyride
          callback={this.callback}
          run={run}
          stepIndex={stepIndex}
          steps={steps}
          continuous
          disableOverlayClose
          spotlightClicks
          showSkipButton
          locale={{
            back: <span>Zurück</span>,
            last: (<span>Beenden</span>),
            next: (<span>Weiter</span>)
          }}
        />
      </>
    )
  }
}



class Parent extends Component {
  constructor (props) {
    super(props)
    this.handler = this.handler.bind(this)
    this.state = {
      run: true,
      stepIndex: 0,
    }
  }

  handler () {
    this.setState({ run: true, stepIndex: 0 })
  }
render () {
    return (
    //some other stuff
        <RestartButton handler={this.handler} />
        <Tutorial run={this.state.run} stepIndex={this.state.stepIndex} />
    //some other stuff
    )
  }
}



class RestartButton extends React.Component {
  render () {
    return (
      <button className='restartButton' onClick={() => this.props.handler()}>click</button>
    )
  }
}

Solution

  • You shouldn't store props in the child component state if state.run and state.stepIndex are always going to be the same as props.run and props.stepIndex. Instead you should just use those props directly when you need to use them in the render method. stepIndex={this.props.stepIndex} will pass exactly the same values to the Joyride component as setting the child component's state.stepIndex equal to props.stepIndex and then passing stepIndex={this.state.stepIndex}.

    If you want to change the value of the parent component's state from a child component, you can pass the handler method (bound to the parent component) through as many layers of components as you want, or to as many different children as you want.

    class Tutorial extends React.Component {
        constructor(props) {
          super(props)
        }
    
        render() {
            return ( 
              <>
                <RestartButton handler={this.props.handler}/>
                <Joyride
                  callback={this.callback}
                  run={this.props.run}
                  stepIndex = {this.props.stepIndex}
                  steps={steps}
                  continuous
                  disableOverlayClose
                  spotlightClicks
                  showSkipButton
                  locale={{
                    back: < span > Zurück < /span>,
                    last: ( < span > Beenden < /span>),
                    next: ( < span > Weiter < /span>)
                  }}
                />
              </>
            )
        }
    }
    
    
    
    class Parent extends Component {
      constructor(props) {
        super(props)
        this.handler = this.handler.bind(this)
        this.state = {
          run: true,
          stepIndex: 0,
        }
      }
    
      handler() {
        this.setState({run: true, stepIndex: 0})
      }
    
      render() {
        return (
          <Tutorial
            run={this.state.run}
            stepIndex={this.state.stepIndex}
          />
        )
      }
    }
    
    
    
    class RestartButton extends React.Component {
      render() {
        return (
          <button
            className='restartButton'
            onClick={() => this.props.handler()}
          > click </button>
        )
      }
    }
    

    (Also, componentWillReceiveProps is deprecated and you should use componentDidUpdate instead, if you do need to do something on component update).