P0lska
P0lska

Reputation: 471

How do you pass an onClick function to grandchild component in React?

I'm just starting out with React, adapting the tic tac toe tutorial for my case. I'm trying to click on the grandchild component to change the state of the grandparent component . Code is as follows:

class App extends React.Component {
  
  constructor(props) {
    super(props);
    this.state = {
      fields: [
        {
          id: 1,
          show: false
        },
        {
          id: 2,
          show: false
        }
      ]
    }
  }

  handleClick(i) {
    const fields = this.state.fields.slice();
    fields[i].show = true;
    this.setState({fields: fields});
  }
  
  render() {return <Preview />}
      
}

const Preview = (props) => {
  
  return (
    <div className="preview">
      {props.fields.map((field) => (
        <Field data={field} key={field.id} onClick={ props.onClick(field.id) }/>
      ))}
    </div>
  );
};

const Field = props => {
  
  return (
    <div className="field" onClick={ props.onClick } />
  );
};

I get a TypeError: Cannot read property 'state' of undefined from this line:

handleClick(i) {
const fields = this.state.fields.slice();

Upvotes: 2

Views: 1202

Answers (2)

Drew Reese
Drew Reese

Reputation: 202618

Issues

  1. this of the App class isn't bound to the handleClick function. This is cause of TypeError: Cannot read property 'state' of undefined error.
  2. You are mutating your state object. Slicing the array creates a new array reference, but fields[i].show = true; mutates the object reference in state.
  3. You don't pass fields or onClick props to Preview.
  4. The onClick callback isn't called correctly in Preview.

Solution

  1. Bind this to the handler or convert to arrow function so it is automatically bound.

    constructor(props){
      ...
      this.handleClick = this.handleClick.bind(this);
    }
    

    or

    handleClick = (i) => { ..... };
    
  2. DON'T MUTATE STATE. Shallow copy state then update properties.

    handleClick = (id) => {
      this.setState(prevState => ({
        fields: prevState.fields.map((field) => {
          return field.id === id ? {
            ...field,
            show: true,
          } : field;
        }),
      }));
    };
    
  3. Pass fields and handleClick as onClick to Preview.

    render() {
      return (
        <Preview
          fields={this.state.fields}
          onClick={this.handleClick}
        />
      );
    }
    
  4. Call props.onClick correctly with the id.

    {props.fields.map((field) => (
      <Field
        data={field}
        key={field.id}
        onClick={() => props.onClick(field.id)}
      />
    ))}
    

Upvotes: 2

Apolo
Apolo

Reputation: 4050

I've added some explanations, check the comments

  // [...]

  render() {
    // Here you need to pass "fields" and "handleClick" as props:
    return <Preview fields={this.state.fields} onClickField={this.handleClick} />
  }
      
}

const Preview = (props) => {
  // Here you get the props:
  const { fields, onClickField } = props;

  // Your onclick was a function call instead of just a function
  return (
    <div className="preview">
      {fields.map((field) => (
        <Field 
          data={field}
          key={field.id}
          onClick={() => onClickField(field.id) }
        />
      ))}
    </div>
  );
};

const Field = props => {
  
  return (
    <div className="field" onClick={ props.onClick } />
  );
};

Upvotes: 0

Related Questions