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
}
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.
I have my ClockBlock component where I lifted state keeped in object "timer":
class ClockBlock extends Component {
constructor(props){
super(props);
this.state = { timer: props.timer }; // timer from Redux store
}
render(){
return(
<div className="clockBlock">
<Circle timer={this.state.timer} updateTimer={this.updateTimer.bind(this)} />
<Clock timer={this.state.timer} updateTimer={this.updateTimer.bind(this)} />
<ClockTerms updateTimer={this.updateTimer.bind(this)} />
</div>
)
}
All the three nested component influence each other via updateTimer function. (except ClockTerms component - it works in one direction). The function is here:
updateTimer(newDuration){
const newState = Object.assign( {}, this.state );
newState.timer.duration = newDuration;
this.setState( newState );
}
So, the problem - when I change timer using ClockTerms component I see changes in the Clock component too (via props, apparently). In the same time in the Circle component in shouldComponentUpdate function i'm trying to see the difference between the old props and the new. Here is this function:
shouldComponentUpdate(nextProps, nextState){
console.log( this.state.timer );
console.log( nextState.timer );
console.log( this.props.timer );
console.log( nextProps.timer );
return true;
}
All the console.log() calls print the same data - new props. Why? And how can I get old props?
PS: i simplified my code above, removing irrelevant from my point of view calculations. Can give whole code, if it is important.
I also use Redux here, but it seems to me it isn't engaged here much.
Great thanks in advance!
UPDATE: I also get the same picture when place the same shouldComponentUpdate function in ClockBlock (parent) component;
You can functionally set state. Because setState is async, when multiple things call setState React uses the most recent properties passed to setState.
Instead you can update state by passing an updater function. This batches all the state updates. When Reacts lifecycle begins to update state it'll process all the pending updater functions in sequence.
This is from the docs describing what happens when setState uses an object.
Subsequent calls will override values from previous calls in the same
cycle, so the quantity will only be incremented once. If the next
state depends on the current state, we recommend using the updater
function form, instead:
this.setState((state) => {
return {quantity: state.quantity + 1};
});
https://reactjs.org/docs/react-component.html#setstate
I've solved a similar situation by comparing this.state to nextProps and updating state. I don't think it's a great solution, but didn't come up with anything else jet.
state = {
dayOff: this.props.myProperty
}
shouldComponentUpdate(nextProps, nextState) {
const shouldUpdate = this.state.myProperty !== nextProps.myProperty;
if (shouldUpdate) this.state.myProperty = nextProps.myProperty
return shouldUpdate;
}
The point is that updateTimer fires BEFORE shouldComponentUpdate runs. That's it. So, when you update your lifted state in the parent component from children components (via passed in props function), keep in mind that shouldComponentUpdate will get already changed state and props.
I think in react frame, the view changes with Component's state,
so I think when mapStateToProps run,and the component get a new store's state,
which mean the props of the component change.
according to react frame ,in this case, the view will not rerender unless the this.setState is called
I'll give one example.
1) You first load your component
2) You make changes on your component, thus changing your redux state for that component
Now, if you want to see the new props that are associated with this component you will see them in your 'render()' method. That's because the react component will re-render automatically if the props changed (as mentioned by Peter Ambruzs). The thing is, if you want to use these new props outside your render function, you will have to update your props(or your local state).
In this case you should use getDerivedStateFromProps (the new componentWillReceiveProps), for example:
constructor(props) {
super(props);
state = {
stateExample: this.props,
}
}
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.stateExample !== prevState.stateExample) {
return { stateExample: nextProps.stateExample }
} else return null;
}
This will update your local state with the new props that you just changed on your redux store.
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.
I'm trying to separate a presentational component from a container component. I have a SitesTable and a SitesTableContainer. The container is responsible for triggering redux actions to fetch the appropriate sites based on the current user.
The problem is the current user is fetched asynchronously, after the container component gets rendered initially. This means that the container component doesn't know that it needs to re-execute the code in its componentDidMount function which would update the data to send to the SitesTable. I think I need to re-render the container component when one of its props(user) changes. How do I do this correctly?
class SitesTableContainer extends React.Component {
static get propTypes() {
return {
sites: React.PropTypes.object,
user: React.PropTypes.object,
isManager: React.PropTypes.boolean
}
}
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
render() {
return <SitesTable sites={this.props.sites}/>
}
}
function mapStateToProps(state) {
const user = userUtils.getCurrentUser(state)
return {
sites: state.get('sites'),
user,
isManager: userUtils.isManager(user)
}
}
export default connect(mapStateToProps)(SitesTableContainer);
You have to add a condition in your componentDidUpdate method.
The example is using fast-deep-equal to compare the objects.
import equal from 'fast-deep-equal'
...
constructor(){
this.updateUser = this.updateUser.bind(this);
}
componentDidMount() {
this.updateUser();
}
componentDidUpdate(prevProps) {
if(!equal(this.props.user, prevProps.user)) // Check if it's a new user, you can also use some unique property, like the ID (this.props.user.id !== prevProps.user.id)
{
this.updateUser();
}
}
updateUser() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Using Hooks (React 16.8.0+)
import React, { useEffect } from 'react';
const SitesTableContainer = ({
user,
isManager,
dispatch,
sites,
}) => {
useEffect(() => {
if(isManager) {
dispatch(actions.fetchAllSites())
} else {
const currentUserId = user.get('id')
dispatch(actions.fetchUsersSites(currentUserId))
}
}, [user]);
return (
return <SitesTable sites={sites}/>
)
}
If the prop you are comparing is an object or an array, you should use useDeepCompareEffect instead of useEffect.
componentWillReceiveProps() is going to be deprecated in the future due to bugs and inconsistencies. An alternative solution for re-rendering a component on props change is to use componentDidUpdate() and shouldComponentUpdate().
componentDidUpdate() is called whenever the component updates AND if shouldComponentUpdate() returns true (If shouldComponentUpdate() is not defined it returns true by default).
shouldComponentUpdate(nextProps){
return nextProps.changedProp !== this.state.changedProp;
}
componentDidUpdate(props){
// Desired operations: ex setting state
}
This same behavior can be accomplished using only the componentDidUpdate() method by including the conditional statement inside of it.
componentDidUpdate(prevProps){
if(prevProps.changedProp !== this.props.changedProp){
this.setState({
changedProp: this.props.changedProp
});
}
}
If one attempts to set the state without a conditional or without defining shouldComponentUpdate() the component will infinitely re-render
You could use KEY unique key (combination of the data) that changes with props, and that component will be rerendered with updated props.
componentWillReceiveProps(nextProps) { // your code here}
I think that is the event you need. componentWillReceiveProps triggers whenever your component receive something through props. From there you can have your checking then do whatever you want to do.
I would recommend having a look at this answer of mine, and see if it is relevant to what you are doing. If I understand your real problem, it's that your just not using your async action correctly and updating the redux "store", which will automatically update your component with it's new props.
This section of your code:
componentDidMount() {
if (this.props.isManager) {
this.props.dispatch(actions.fetchAllSites())
} else {
const currentUserId = this.props.user.get('id')
this.props.dispatch(actions.fetchUsersSites(currentUserId))
}
}
Should not be triggering in a component, it should be handled after executing your first request.
Have a look at this example from redux-thunk:
function makeASandwichWithSecretSauce(forPerson) {
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
You don't necessarily have to use redux-thunk, but it will help you reason about scenarios like this and write code to match.
A friendly method to use is the following, once prop updates it will automatically rerender component:
render {
let textWhenComponentUpdate = this.props.text
return (
<View>
<Text>{textWhenComponentUpdate}</Text>
</View>
)
}
You could use the getDerivedStateFromProps() lifecyle method in the component that you want to be re-rendered, to set it's state based on an incoming change to the props passed to the component. Updating the state will cause a re-render. It works like this:
static getDerivedStateFromProps(nextProps, prevState) {
return { myStateProperty: nextProps.myProp};
}
This will set the value for myStateProperty in the component state to the value of myProp, and the component will re-render.
Make sure you understand potential implications of using this approach. In particular, you need to avoid overwriting the state of your component unintentionally because the props were updated in the parent component unexpectedly. You can perform checking logic if required by comparing the existing state (represented by prevState), to any incoming props value(s).
Only use an updated prop to update the state in cases where the value from props is the source of truth for the state value. If that's the case, there may also be a simpler way to achieve what you need. See - You Probably Don't Need Derived State – React Blog.