简体   繁体   中英

How to access and change child's state from Parent component

You are given uncontrollable child component. Parent component stores number of counters and renders that amount of child components. It has two buttons Add counter and Increment all counters.

  1. You are not allowed to edit child component.

  2. Implement incrementCounters so that it increments counters of all child components that are rendered.

    incrementCounters = () => { // TODO: implement };


export class ChildCounter extends React.Component{
    state = {
        counter: 0
    };

    increment = () => {
        this.setState(({ counter }) => ({
            counter: counter + 1
        }));
    };

    render() {
        return (
            <div>
                Counter: {this.state.counter}
                <button onClick={this.increment}>+</button>
            </div>
        );
    }
}

import {ChildCounter} from "./Child";


class App extends React.Component {

    state = {counters:1}


    addCounter = () => {
     let counterRn = this.state.counters + 1;
     this.setState({counters:counterRn})
    };

   incrementAll = () => {

   }

   render() {
   return (
       <div>
                { new Array(this.state.counters).fill(0).map((_, index) => {
                    return <ChildCounter key={index} />;
                })
                }
           <br/>
           <button style={{marginRight:10}} onClick={this.addCounter}>Add Counter</button>
           <button onClick={this.incrementAll}>Increment all counters</button>
       </div>
   )
   }
}

First of all I don't know why you don't want to do it with state, it would be much better I think by using an array of counter values and then incrementing them all at once, and having your child component as controller component.

With refs, I was not able to come up with a good solution (not sure). So I have passed the code here, and What I did is, I used a react hook called useImperativeHandle to make your child component take the ref and expose the inner increment method. And then I added an array of refs in your parent component. So when the user click increment all, basically you loop over your refs and call the inner increment method. I am still not sure a correct way to implement array of refs, but I think this should do it. And I have already written this with hooks to make it simpler to understand.


import React, {
  useState,
  useEffect,
  createRef,
  forwardRef,
  useImperativeHandle
} from "react";

const ChildCounter = forwardRef((props, ref) => {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    setCounter((c) => c + 1);
  };

  useImperativeHandle(ref, () => ({
    increment
  }));

  return (
    <div>
      Counter: {counter}
      <button onClick={increment}>+</button>
    </div>
  );
});

const App = () => {
  const [counters, setCounters] = useState(1);
  const [elRefs, setElRefs] = useState([]);

  const addCounter = () => {
    setCounters((c) => c + 1);
  };

  useEffect(() => {
    setElRefs((elRefs) =>
      Array(counters)
        .fill()
        .map((_, i) => elRefs[i] || createRef())
    );
  }, [counters]);

  const incrementAll = () => {
    for (let i = 0; i < elRefs.length; i++) {
      if (elRefs[i] && elRefs[i].current) {
        elRefs[i].current.increment();
      }
    }
  };

  return (
    <div>
      {new Array(counters).fill(0).map((_, index) => {
        return <ChildCounter ref={elRefs[index]} key={index} />;
      })}
      <br />
      <button style={{ marginRight: 10 }} onClick={addCounter}>
        Add Counter
      </button>
      <button onClick={incrementAll}>Increment all counters</button>
    </div>
  );
};

export default App;

https://codesandbox.io/s/trusting-neumann-37e2q?file=/src/App.js:0-1327

This is technically possible to achieve without changing the child by passing down a ref from the parent instead, and have the parent use the ref to access the increment method on the child whenever incrementAll is called:

childCounterChildren = [];

incrementAll = () => {
    for (const counter of this.childCounterChildren) {
        counter.increment();
    }
}
<ChildCounter
    key={index}
    ref={(childCounter) => { if (childCounter) { this.childCounterChildren.push(childCounter); }}}
/>;

 class ChildCounter extends React.Component { state = { counter: 0 }; increment = () => { this.setState(({ counter }) => ({ counter: counter + 1 })); }; render() { return ( <div> Counter: {this.state.counter} <button onClick={this.increment}>+</button> </div> ); } } class App extends React.Component { state = { counters: 1 } childCounterChildren = []; addCounter = () => { let counterRn = this.state.counters + 1; this.setState({ counters: counterRn }) }; incrementAll = () => { for (const counter of this.childCounterChildren) { counter.increment(); } } componentWillUpdate() { this.childCounterChildren.length = 0; } render() { return ( <div> {new Array(this.state.counters).fill(0).map((_, index) => { return <ChildCounter key={index} ref={(childCounter) => { if (childCounter) { this.childCounterChildren.push(childCounter); }}} />; }) } <br /> <button style={{ marginRight: 10 }} onClick={this.addCounter}>Add Counter</button> <button onClick={this.incrementAll}>Increment all counters</button> </div> ) } } ReactDOM.render(<App />, document.querySelector('.react'));
 <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <div class='react'></div>

But it's quite weird. For a real-world problem, I'd recommend lifting state up instead.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM