globus243
globus243

Reputation: 729

React: trigger event in child component by click on parent

Context:

I want to trigger an event in a parents child component by an onClick on the parent element

Code:

Parent PlantContainer:

import React from "react";
import ClipLoader from "react-spinners/ClipLoader";
import Box from '@material-ui/core/Box';
import ShowMetric from '../showMetric';


export default class PlantContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLoading: false,
    };
  }

  render() {    
    return (
      <Box>
        <h2>{this.props.plantName}</h2>
        <ShowMetric 
          setting={this.props.plantName + ".moisture"} 
          unit="%">Moisture:</ShowMetric>
        <ShowMetric 
          setting={this.props.plantName + ".conductivity"} 
          unit="%">Fertility:</ShowMetric>
      </Box>
    );
  }
}

Child ShowMetric:

import React from "react";
import ClipLoader from "react-spinners/ClipLoader";
import resolvePath from 'object-resolve-path';


export default class ShowMetric extends React.Component {
  constructor(props) {
    super(props);
    this.getData = this.getData.bind(this);
    this.state = {
      isLoading: false,
      reading: 0,
    };
  }

  getData() {
    this.setState({ isLoading: true });
    fetch(URL_HERE, {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "*",
      },
    })
    .then(function (response) {
      return response.json();
    })
    .then((json) =>
      this.setState({
        reading: resolvePath(json, this.props.setting),
        isLoading: false,
      })
    );
  }

  componentDidMount() {
    this.getData();
  }

  render() {
    if (this.state.isLoading) {
      return <ClipLoader />;
    }

    return (
        <div onClick={this.getData}>
          {this.props.children + " "}
          <nobr>{`${this.state.reading.toFixed(1)} ${this.props.unit}`}</nobr>
        </div>
    );
  }
}

Main App.js:

import './App.css';
import React from 'react';
import Container from '@material-ui/core/Container';
import Box from '@material-ui/core/Box';
import PlantContainer from './components/plantContainer';



function App() {
  return (
    <div className="App">
      <Container maxWidth="md">
          <Box className="flexBox">
            <PlantContainer plantName="Plant_1"/>
            <PlantContainer plantName="Plant_2"/>
          </Box>
      </Container>
    </div>
  );
}

export default App;

Problem

The above code works as expected, as <ShowMetric/> shows the information and reloads when I click on it.

Now I want to reload all <ShowMetric/> Elements in PlantContainer (maybe trigger the getData() function for each of them) when I click the <H2> Element of PlantContainer.

I tried to find ways how to pass down events or informations to children, but since props can't change at runtime (?) and I don't think a reference would be the best way here, I am a bit at lost on how to implement this.

And as this is my very first react web App and endeavour into this framework please call out any fishy thing you can find in the code.

Upvotes: 1

Views: 7494

Answers (2)

rottitime
rottitime

Reputation: 2461

The useImperativeHandle hook is perfect to allow child components and refs.

Fully working example with Typescript support too!:

Edit affectionate-ben-q4ox51

//Child Component 

//Create your ref types here
export type RefHandler = {
  pressAlert: () => void;
  inputRef: RefObject<HTMLInputElement>;
};

const Child = forwardRef<RefHandler, Props>((props, ref) => {
  const submitRef = useRef<HTMLButtonElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  //Initialise your refs here
  useImperativeHandle(ref, () => ({
    inputRef: inputRef,
    pressAlert: () => submitRef?.current?.click()
  }));

  return (
    <div>
      <p>Child Component</p>
      <input type="text" value="lorem ipsum" ref={inputRef} />
      <br />
      <button onClick={() => alert("Alert pressed")} ref={submitRef}>
        Alert
      </button>
    </div>
  );
});
//Parent 
export default function Parent() {
  const childRef = useRef<RefHandler>(null);

  return (
    <>
      <p>Parent</p>
      <button
        onClick={() => {
          alert(childRef?.current?.inputRef?.current?.value);
        }}
      >
        Read child input
      </button>
      <button onClick={() => childRef?.current?.pressAlert()}>
        Press child button
      </button>
      <hr />
      <Child ref={childRef} />
    </>
  );
}

Upvotes: 3

Olivier Boiss&#233;
Olivier Boiss&#233;

Reputation: 18093

I think the more elegant way to do this would be to store all the data in the parent component and pass it down to the children through the props.

Here is a possible solution (I used function components as it should be privileged over the class components) :

PlantContainer

function fetchData() {
  return fetch(URL_HERE, {
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "*",
    },
  })
  .then(response => response.json());
}

export default function PlantContainer(props) {
  const [data, setData] = React.useState({
    isLoading: false,
    'moisture': 0, 
    'conductivity': 0
  });

  function loadData() {
    setData({...data, isLoading: true});
    fetchData().then(json => {
      setData({
        isLoading: false,
        'moisture': resolvePath(json, `${props.plantName}.moisture`),
        'conductivity': resolvePath(json, `${props.plantName}.conductivity`)
      });
    });
  }
  
  React.useEffect(loadData, []);
  
  return (
    <Box>
      <h2 onClick={loadData}>{props.plantName}</h2>
      {data.isLoading && <ClipLoader/>}
      {!data.isLoading && (
        <ShowMetric
          reading={data['moisture']}
          unit="%">Moisture:</ShowMetric>
        <ShowMetric
          reading={data['conductivity']}
          unit="%">Fertility:</ShowMetric>
      )}
    </Box>
  );
}

ShowMetric

export default function ShowMetric(props) {
  return (
    <div>
      {props.children + " "}
      <nobr>{`${props.reading.toFixed(1)} ${props.unit}`}</nobr>
    </div>
  );
}

As you can retrieve all the data by calling the service a single time, it seems to be useless to reload only one metric, so I only give to opportunity to reload both metrics by clicking on the h2 element.

Upvotes: 1

Related Questions