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?
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/
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 :)
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.
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.
Related
The component receives a list of Objects as prop. I want to display it with infinite scroll.
It has the state "curNum", which is current number of items to be displayed. "curNum" changes when scroll down so the list of items is sliced based on the "curNum".
The list of object will be updated by the parent component, and "curNum" should be reset to initial value.
With componentWillReceiveProps I can do:
componentWillReceiveProps(newProps) {
this.setState({
curNum: initNum,
});
}
but how can I rewrite it using getDerivedStateFromProps? I read that the new method will be triggerged even if the state changes. So how can I know I am recieving new props?
Do I have to mirror a copy of the list to the sate and then deep check if the list of objects are equal every time?
There are a few ways that you can use to update the state when props change
Use getDerivedStateFromProps: Note that its suggested that you avoid using this method as much as possible since this is called on each update and initial render and not just on Parent component re-render or props change. If however you want to use it, you need to store the prevState too
Code:
static getDerivedStateFromProps(props, state) {
if (state.prevCurNum !== props.initNum) {
return {
currNum: props.initNum,
prevCurNum: props.initNum
}
} else {
return { prevCurNum: props.initNum }
}
}
Assign the state from props in constructor and control the key props to component
code
<MyComponent key={this.state.initNum} initNum={this.state.initNum} />
and in MyComponent
constructor(props) {
super(props);
this.state= {
currNum: props.initNum
}
}
In the above example if the prop initNum changes, the key to the component will change and it will result in the component to re-mount calling its constructor
The third way is to use memoization in render, but its mostly useful when the state is derived from a complex computation of props and isn't supposed to change locally.
Insdead of using setState in componentWillReceiveProps, I just set the props to component's local property directly:
class Inventory extends React.PureComponent {
pieData = {};
doSomeTransition = (pieData) => {
};
componentWillReceiveProps(nextProps) {
if (this.props.productionOverview !== nextProps.productionOverview) {
this.pieData = this.doSomeTransition(nextProps.productionOverview);
}
}
render() {
return (
<Production chartData={this.pieData} />
);
}
}
The <Production /> component re-render well as every time props. productionOverviewchanges.
I feel like this is a wrong way but I can't tell why, because all the components work well as expected.
componentWillReceiveProps is usually used to update your React component state when the props have changed. Which is why you will usually see setState inside that function.
That being said there is nothing wrong in calling something else, it all depends on the type of operation you are actually doing inside doSomeTransition ...
I don't believe you will experience any obvious side effects but you will not have any guarantees on when the property's value is actually updated/modified whereas utilizing the this.state you know the state is being handled in accordance to the state's lifecycle.
Complete Reactjs newbie here. I know setState() is asynchronous. I know if I do setState() then the states are queued and batched, so I will not see the state changed immediately. Fair enough.
I have also read the following link and other questions in SO:
https://reactjs.org/docs/react-component.html#setstate
I can use: callback methods in setState, componentDidUpdate() lifecycle method, concat if the state is an array etc. I get all these ways. My problem is a simple one, also I am banging my head on this issue for past 2 days so I am literally at my wit's end now.
I have this logic:
class ItemList extends React.Component {
constructor(props) {
super(props);
this.state = {showCompleted: false};
this.shouldShowFullList = false;
this.onShowCompleted = this.onShowCompleted.bind(this);
}
onShowCompleted(event) {
this.setState({showCompleted: event}, () => {this.shouldShowFullList = this.state.showCompleted;});
}
render() {
if (this.shouldShowFullList) {
return this.renderAllItemsIncludingCompleted();
} else {
return this.renderOnlyNotCompletedItems();
}
}
...
The logic is self-explanatory. My problem is even when I call this.shouldShowFullList in callback method of setState(), it still does not show an updated value. Value of this.shouldShowFullList is false when it should be true and vice versa. What is the best way to have the value of this.shouldShowFullList in lockstep with the this.state.showCompleted?
NOTE: onShowCompleted() is a callback method triggered from a child component. When a checkbox called "Show Completed" is checked, I should show a complete list of items, or else just the items which are not completed - something like ToDo list.
At onShowCompleted do
this.setState({ showCompleted: true })
or if you want toggle the value
this.setState({ showCompleted: !this.state.showCompleted })
Then in the render method you can do
if (this.state.showCompleted) {
return this.renderAllItemsIncludingCompleted();
} else {
return this.renderOnlyNotCompletedItems();
}
When you set a state using setState the method render is called with this.state updated. I think the callback (second argument) of this.setState is called after the render.
Because updating this.state make a new render it seems that react is pushing you to use this.state in the render method. In fact it's made for this usage. If you want to make a variable that have no purpose in the render you can use this.myVariable. Best pratice is to use only this.stateand this.props in the render (or function that depend of it).
My React component sets a state property called userInGroup based on props.
this.state = {
userInGroup: this.props.user
? this.props.user.groups.includes(props.group._id)
: false,
};
This works but does not update when the props change and the value of userInGroup should also change, until I refresh the page. How can I make this update reactively?
Maybe I could use componentWillUpdate or componentDidUpdate but then Id be repeating the logic used by userInGroup. Is this repetition inevitable?
You need to use getDerivedStateFromProps. The other methods are now deprecated and deemed unsafe.
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.
This method exists for rare use cases where the state depends on changes in props over time. For example, it might be handy for implementing a component that compares its previous and next children to decide which of them to animate in and out.
static getDerivedStateFromProps(props) {
return {
userInGroup: props.user
? props.user.groups.includes(props.group._id)
: false,
}
}
Yes you need to make use of componentWillReceiveProps along with constructor/componentDidMount, when you want to update state when props change since constructor/componentDidMount are called only once when the component mounts, and componentWillReceiveProps() is invoked before a mounted component receives new props or the Parent component updates
You could write a function that contains the logic
this.state = {
userInGroup: this.getUserStatus();
};
componentWillReceiveProps(nextProps) {
if(nextProps.user !== this.props.user) {
this.setState({userInGroup: this.getUserStatus(nextProps)})
}
}
getUserStatus = (newProps) => {
const data = newProps || this.props
return data.user
? data.user.groups.includes(data.group._id)
: false
}
I have CompetitionSection which repeats all the competitions from database. When user clicks on one, it redirects him to a Competition Page, loads for a second and renders the page with all the details in it. So far, so good.
But when users goes back to the Competition Section and then click on the second competition, it instantly loads up the previous competition, 0 loading time.
From my point of view, what is failing is that the props of the component are not updating when I render the component (from the second time). Is not a router problem, which was my first instinct because I'm seeing the route.params changing acordingly, but the actions I dispatch to change the props are not dispatching. Here's a bit of code of said component.
class CompetitionPage extends React.Component {
componentWillMount() {
let id = getIdByName(this.props.params.shortname)
this.props.dispatch(getCompAction(id));
this.props.dispatch(getCompMatches(id));
this.props.dispatch(getCompParticipants(id));
this.props.dispatch(getCompBracket(id));
}
render() {
let { comp, compMatches, compBracket, compParticipants } = this.props
...
I tried every lifecycle method I know. component Will/Did Mount, component Will/Did update and I even set shouldUpdate to true and didn't do the trick. As I understand, the problem will be solved with a lifecycle method to dispatch the actions everytime an user enters Competition Page and not just for the first time. I'm running out of options here, so any help will be appreciated.
NOTE: I'm a newbie at React/Redux so I KNOW there are a couple of things there are anti-pattern/poorly done.
UPDATE: Added CompetitionsSection
class CompetitionsSection extends React.Component {
render() {
const {competitions} = this.props;
return (
...
{ Object.keys(competitions).map(function(comp, i) {
return (
<div key={i} className={competitions[comp].status ===
undefined? 'hide-it':'col-xs-12 col-md-6'}>
...
<Link to={"/competitions/"+competitions[comp].shortName}>
<RaisedButton label="Ver Torneo" primary={true} />
</Link>
...
It helps to better understand the lifecycle hooks. Mounting a component is when it is placed on the DOM. That can only happen once until it is removed from the DOM. An UPDATE occurs when new props are passed or setState is called. There are a few methods to troubleshoot when updates are not happening when you think they should:
Ensure that you are changing state in componentDidMount or componentDidUpdate. You cannot trigger an update in componentWillMount.
Make sure that the new props or state are completely new objects. If you are passing an object down in props and you are just mutating the object, it will not trigger an update. For instance, this would not trigger a update:
class CompetitionPage extends React.Component {
constructor(props) {
super(props)
this.state = {
competitions: [ compA, compB ]
}
}
triggerUpdate() {
this.setState({
competitions: competitions.push(compC)
})
}
componentDidMount() {
triggerUpdate()
}
render() {
return(
<div>
Hello
</div>
)
}
This is due to the fact that a new competition is being appended to the array in state. The correct way is to completly create a new state object and change what needs to be changed:
const newCompetitions = this.state.competitions.concat(compC)
this.setState(Object.assign({}, this.state, { competitions: newCompetitions }))
Use ComponentWillRecieveProps on an update to compare previous and current prop values. You can setState here if clean up needs to be done:
Read more about this method in the React documentation:
https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops