156

I'm attempting to make a nice ApiWrapper component to populate data in various child components. From everything I've read, this should work: https://jsfiddle.net/vinniejames/m1mesp6z/1/

class ApiWrapper extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      response: {
        "title": 'nothing fetched yet'
      }
    };
  }

  componentDidMount() {
    this._makeApiCall(this.props.endpoint);
  }

  _makeApiCall(endpoint) {
    fetch(endpoint).then(function(response) {
      this.setState({
        response: response
      });
    }.bind(this))
  }

  render() {
    return <Child data = {
      this.state.response
    }
    />;
  }
}

class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: props.data
    };
  }

  render() {
    console.log(this.state.data, 'new data');
    return ( < span > {
      this.state.data.title
    } < /span>);
  };
}

var element = < ApiWrapper endpoint = "https://jsonplaceholder.typicode.com/posts/1" / > ;

ReactDOM.render(
  element,
  document.getElementById('container')
);

But for some reason, it seems the child component is not updating when the parent state changes.

Am I missing something here?

Vinnie James
  • 5,763
  • 6
  • 43
  • 52

4 Answers4

257

There are two issues with your code.

Your child component's initial state is set from props.

this.state = {
  data: props.data
};

Quoting from this SO Answer:

Passing the intial state to a component as a prop is an anti-pattern because the getInitialState (in our case the constuctor) method is only called the first time the component renders. Never more. Meaning that, if you re-render that component passing a different value as a prop, the component will not react accordingly, because the component will keep the state from the first time it was rendered. It's very error prone.

So if you can't avoid such a situation the ideal solution is to use the method componentWillReceiveProps to listen for new props.

Adding the below code to your child component will solve your problem with Child component re-rendering.

componentWillReceiveProps(nextProps) {
  this.setState({ data: nextProps.data });  
}

The second issue is with the fetch.

_makeApiCall(endpoint) {
  fetch(endpoint)
    .then((response) => response.json())   // ----> you missed this part
    .then((response) => this.setState({ response }));
}

And here is a working fiddle: https://jsfiddle.net/o8b04mLy/

Community
  • 1
  • 1
yadhu
  • 15,423
  • 7
  • 32
  • 49
  • 1
    "It's okay to [initialize state based on props](https://facebook.github.io/react/docs/react-component.html#constructor) if you know what you're doing" Are there any other downsides to setting state via prop, beside the fact that `nextProp` wont trigger a re-render without `componentWillReceiveProps(nextProps)`? – Vinnie James Dec 20 '16 at 18:29
  • 16
    As far as I know, there are no other downsides. But in your case we could clearly avoid having a state inside child component. The parent can just pass data as props, and when parent re-renders with its new state, the child also would re-render (with new props). Actually maintaining a state inside child is unnecessary here. Pure components FTW! – yadhu Dec 21 '16 at 03:08
  • 12
    For anyone reading from now on, check `static getDerivedStateFromProps(nextProps, prevState)` https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops – GoatsWearHats May 24 '18 at 02:10
  • 1
    @free-soul I feel like a moron for not thinking to do that. Thank you for pointing this out. – John R Perry Oct 14 '18 at 12:45
  • 4
    Is there any other solution to this other than componentWillReceiveProps() because it is deprecated now? – LearningMath Nov 14 '18 at 03:00
  • 4
    @LearningMath please see the latest [React docs](https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops) which speaks about alternative ways. You might have to reconsider your logic. – yadhu Nov 27 '18 at 03:16
  • 1
    nicely explained. How safe is it to use `{ data && data.map(item => { return () }) }`? – Sharan Arumugam Mar 05 '19 at 10:52
  • 1
    @SharanArumugam It seems to be that what you're asking is a totally different question. Can you please post it as a different question elaborating your concern so that myself or someone else out there could help you? – yadhu Mar 05 '19 at 13:47
  • 1
    I love how they state that passing in state via props is an anti-pattern, but they offer no alternative. Great. – Andrew Koster Oct 29 '19 at 04:00
  • 2
    Just wanted to leave a comment calling to attention that `componentWillReceiveProps` will be dropped at version 17. They advise to use `getDerivedStateFromProps` as a substitute. Hope it helps people! – Diogo Santo May 11 '20 at 16:33
  • I guess React.createContext(); can also be used here... and then use: useContext(GameContext); in the children to get refreshed context – viruskimera Dec 01 '20 at 06:05
  • @SharanArumugam Yes. It should work – Avisek Chakraborty Jan 12 '22 at 18:42
  • 1
    @DiogoSanto is right. `componentWillReceiveProps` should not be the accepted answer anymore. – ufukty Feb 20 '22 at 10:42
11

If the above solution has still not solved your problem I'll suggest you see once how you're changing the state, if you're not returning a new object then sometimes react sees no difference in the new previous and the changed state, it's a good practice to always pass a new object when changing the state, seeing the new object react will definitely re-render all the components needing that have access to that changed state.

For example: -

Here I'll change one property of an array of objects in my state, look at how I spread all the data in a new object. Also, the code below might look a bit alien to you, it's a redux reducer function BUT don't worry it's just a method to change the state.

export const addItemToCart = (cartItems,cartItemToBeAdded) => {
        return cartItems.map(item => {
            if(item.id===existingItem.id){
                ++item.quantity;        
            }
            // I can simply return item but instead I spread the item and return a new object
            return {...item} 
        })
    } 

Just make sure you're changing the state with a new object, even if you make a minor change in the state just spread it in a new object and then return, this will trigger rendering in all the appropriate places. Hope this helped. Let me know if I'm wrong somewhere :)

2

There are some things you need to change.

When fetch get the response, it is not a json. I was looking for how can I get this json and I discovered this link.

By the other side, you need to think that constructor function is called only once.

So, you need to change the way that you retrieve the data in <Child> component.

Here, I left an example code: https://jsfiddle.net/emq1ztqj/

I hope that helps.

saeta
  • 4,048
  • 2
  • 31
  • 48
  • Thank you. Although, looking at the example you made it still seems as though Child never updates. Any suggestions on how to change the way Child receives the data? – Vinnie James Dec 20 '16 at 01:47
  • 2
    Are you sure? I see that `` component retrieve the new data from `https://jsonplaceholder.typicode.com/posts/1` and do re-render. – saeta Dec 20 '16 at 01:53
  • 1
    Yes, I see it working now on OSX. iOS wasnt triggering the re-render in the StackExchange app browser – Vinnie James Dec 20 '16 at 18:30
1

Accepted answer and componentWillReceiveProps

The componentWillReceiveProps call in accepted answer is deprecated and will be removed from React with version 17 React Docs: UNSAFE_componentWillReceiveProps()

Using derived state logic in React

As the React docs is pointing, using derived state (meaning: a component reflecting a change that is happened in its props) can make your components harder to think, and could be an anti-pattern. React Docs: You Probably Don't Need Derived State

Current solution: getDerivedStateFromProps

If you choose to use derived state, current solution is using getDerivedStateFromProps call as @DiogoSanto said.

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing. React Docs: static getDerivedStateFromProps()

How to use componentWillReceiveProps

This method can not access instance properties. All it does describing React how to compute new state from a given props. Whenever props are changed, React will call this method and will use the object returned by this method as the new state.

class Child extends React.Component {
    constructor() {
        super(props);
        // nothing changed, assign the state for the 
        // first time to teach its initial shape. 
        // (it will work without this, but will throw 
        // a warning)
        this.state = {
            data: props.data
        };
    }

    componentWillReceiveProps(props) {
        // return the new state as object, do not call .setState()
        return { 
            data: props.data
        };
    }

    render() {
        // nothing changed, will be called after 
        // componentWillReceiveProps returned the new state, 
        // each time props are updated.
        return ( 
            <span>{this.state.data.title}</span>
        );
    }
}

Caution

  • Re-rendering a component according to a change happened in parent component can be annoying for user because of losing the user input on that component.
  • Derived state logic can make components harder to understand, think on. Use wisely.
ufukty
  • 312
  • 3
  • 10