128
votes

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?

3

3 Answers

229
votes

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/

6
votes

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 :)

1
votes

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.