React Warning: Cannot update a component from inside the function body of a different component

前端 未结 10 1080
攒了一身酷
攒了一身酷 2021-01-01 12:24

I am using Redux with Class Components in React. Having the below two states in Redux store.

{ spinner: false, refresh: false }

In Parent C

相关标签:
10条回答
  • 2021-01-01 12:31

    If your code calls a function in a parent component upon a condition being met like this:

    const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
        const { data, loading, error } = useQuery(QUERY);
    
        if (data && data.users.length === 0) {
            return handleNoUsersLoaded();
        }
    
        return (
            <div>
                <p>Users are loaded.</p>
            </div>
        );
    };
    

    Try wrapping the condition in a useEffect:

    const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
        const { data, loading, error } = useQuery(QUERY);
    
        useEffect(() => {
            if (data && data.users.length === 0) {
                return handleNoUsersLoaded();
            }
        }, [data, handleNoUsersLoaded]);
    
        return (
            <div>
                <p>Users are loaded.</p>
            </div>
        );
    };
    
    0 讨论(0)
  • 2021-01-01 12:32

    It seems that you have latest build of React@16.13.x. You can find more details about it here. It is specified that you should not setState of another component from other component.

    from the docs:

    It is supported to call setState during render, but only for the same component. If you call setState during a render on a different component, you will now see a warning:
    Warning: Cannot update a component from inside the function body of a different component.
    This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the setState call into useEffect.


    Coming to the actual question.

    I think there is no need of getDerivedStateFromProps in the child component body. If you want to trigger the bound event. Then you can call it via the onClick of the Child component as i can see it is a <button/>.

    class Child extends React.Component {
      constructor(props){
        super(props);
        this.updateState = this.updateState.bind(this);
      }
      updateState() { // call this onClick to trigger the update
        if (somecondition) {
          // doing some redux store update
          this.props.reloadApp();
        }
      }
    
      render() {
        return <button onClick={this.updateState} />;
      }
    }
    
    0 讨论(0)
  • 2021-01-01 12:36

    I was running into this problem writing a filter component with a few text boxes that allows the user to limit the items in a list within another component. I was tracking my filtered items in Redux state. This solution is essentially that of @Rajnikant; with some sample code.

    I received the warning because of following. Note the props.setFilteredItems in the render function.

    import {setFilteredItems} from './myActions';
    const myFilters = props => {
      const [nameFilter, setNameFilter] = useState('');
      const [cityFilter, setCityFilter] = useState('');
    
      const filterName = record => record.name.startsWith(nameFilter);
      const filterCity = record => record.city.startsWith(cityFilter);
    
      const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
      props.setFilteredItems(selectedRecords); //  <-- Danger! Updates Redux during a render!
    
      return <div>
        <input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
        <input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
      </div>
    };
    
    const mapStateToProps = state => ({
      records: state.stuff.items,
      filteredItems: state.stuff.filteredItems
    });
    const mapDispatchToProps = { setFilteredItems };
    export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
    

    When I ran this code with React 16.12.0, I received the warning listed in the topic of this thread in my browser console. Based on the stack trace, the offending line was my props.setFilteredItems invocation within the render function. So I simply enclosed the filter invocations and state change in a useEffect as below.

    import {setFilteredItems} from './myActions';
    const myFilters = props => {
      const [nameFilter, setNameFilter] = useState('');
      const [cityFilter, setCityFilter] = useState('');
    
      useEffect(() => {
        const filterName = record => record.name.startsWith(nameFilter);
        const filterCity = record => record.city.startsWith(cityFilter);
    
        const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
        props.setFilteredItems(selectedRecords); //  <-- OK now; effect runs outside of render.
      }, [nameFilter, cityFilter]);
    
      return <div>
        <input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
        <input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
      </div>
    };
    
    const mapStateToProps = state => ({
      records: state.stuff.items,
      filteredItems: state.stuff.filteredItems
    });
    const mapDispatchToProps = { setFilteredItems };
    export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
    

    When I first added the useEffect I blew the top off the stack since every invocation of useEffect caused state change. I had to add an array of skipping effects so that the effect only ran when the filter fields themselves changed.

    0 讨论(0)
  • 2021-01-01 12:39

    I had same issue after upgrading react and react native, i just solved that issue by putting my props.navigation.setOptions to in useEffect. If someone is facing same problen that i had i just want to suggest him put your state changing or whatever inside useEffect

    0 讨论(0)
  • 2021-01-01 12:41

    Same error but different scenario

    tldr; wrapping state update in setTimeout fixes it.

    This scenarios was causing the issue which IMO is a valid use case.

    const [someState, setSomeState] = useState(someValue);
    const doUpdate = useRef((someNewValue) => {
      setSomeState(someNewValue);
    }).current;
    return (
      <SomeComponent onSomeUpdate={doUpdate} />
    );
    
    

    fix

    const [someState, setSomeState] = useState(someValue);
    const doUpdate = useRef((someNewValue) => {
      setTimeout(() => {
       setSomeState(someNewValue);
      }, 0);
    }).current;
    return (
      <SomeComponent onSomeUpdate={doUpdate} />
    );
    
    
    0 讨论(0)
  • 2021-01-01 12:43

    I suggest looking at video below. As the warning in the OP's question suggests, there's a change detection issue with the parent (Parent) attempting to update one child's (Child 2) attribute prematurely as the result of another sibling child's (Child 1) callback to the parent. For me, Child 2 was prematurely/incorrectly calling the passed in Parent callback thus throwing the warning.

    Note, this commuincation workflow is only an option. I personally prefer exchange and update of data between components via a shared Redux store. However, sometimes it's overkill. The video suggests a clean alternative where the children are 'dumb' and only converse via props mand callbacks.

    Also note, If the callback is invoked on an Child 1 'event' like a button click it'll work since, by then, the children have been updated. No need for timeouts, useEffects, etc. UseState will suffice for this narrow scenario.

    Here's the link (thanks Masoud):

    https://www.youtube.com/watch?v=Qf68sssXPtM

    0 讨论(0)
提交回复
热议问题