tl;dr -- I want to fire a callback whenever the state changes in a component
I have a component that mutates this.props.items and fires this.props.onChange on every mutation. I'm currently doing something like this:
removeItem(ix) {
const items = this.state.items.slice();
items.splice(ix, 1);
this.setState({ items });
this.triggerOnChange();
}
I'm wondering if it's possible to remove the manual call to triggerOnChange and instead do it whenever this.state.items is updated
this.setState takes two parameter, first is object (state) and second is a callback (optional)
this.setState({...}, function(){
console.log('changed')
})
From Spec
The second parameter is an optional callback function that will be
executed once setState is completed and the component is re-rendered
You could override setState, if you want to wrap the state change:
setState(state) {
console.log("New state - before: ", state);
const retVal = super.setState(state);
console.log("New state - after");
return retVal;
}
(That may be overkill, I don't think setState is documented to have a return value; but frustratingly, as far as I can tell, it's not documented not to...)
Example:
class MyThingy extends React.Component {
constructor(props) {
super(props);
this.state = {...props};
}
render() {
return (
<div onClick={() => this.update()}>
<div>{this.state.title}</div>
{this.state.items.map(e => (
<div>{e}</div>
))}
</div>
);
}
update() {
this.setState({
items: ['a', 'b', 'c'],
title: "After"
});
}
setState(state) {
console.log("New state - before: ", state);
const retVal = super.setState(state);
console.log("New state - after");
return retVal;
}
}
ReactDOM.render(
<MyThingy items={[]} title="Before - click me" />,
document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Alternately, you might turn this on its head and look at MobX.
Related
Parent component does rerender upon receiving new props but its child component doesn't rerender. Child components only render for the first time and never rerender nor receive props from the redux store
I'm getting updated data from redux store in Parent component but not in the child components. Child components only receive data from redux store when they render for the first time
My Parent Component Home.js
Object seaFCLJSON look like this
const seaFCLJSON ={"rates": {"sort":"faster", "someOther": "someOtherValues"}};
when the redux store gets updated, seaFCLJSON looks like this
const seaFCLJSON ={"rates": {"sort":"cheaper","someOther": "someOtherValues"}};
class Home extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
}
};
render() {
const { seaFCLJSON } = this.props;
return (
<>
{!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl ? (
<FCLContainer fclJSON={seaFCLJSON} />
) : null} //it never rerenders upon getting updated data from redux store
<h5>{JSON.stringify(seaFCLJSON.rates && seaFCLJSON.rates.sort)}</h5> //it rerenders everytime upon getting updated data from redux store
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(Home);
isEmpty.js
export const isEmpty = obj => {
return Object.entries(obj).length === 0 && obj.constructor === Object;
};
My Child Component FCLContainer.js
class FCLContainer extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
console.log("outside state value: ", this.state.seaFCLJSON);
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
console.log("inside state value: ", this.state.seaFCLJSON);
}
};
render() {
const { seaFCLJSON } = this.state;
console.log("rendering .. parent props: ", this.props.fclJSON);
console.log("rendering .. redux store props: ", this.props.seaFCLJSON);
return (
<>
<div className="home-result-container">
<div>
<h5>This child component never rerenders :(</h5>
</div>
</div>
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(FCLContainer);
I don't know whether there are problems in Parent component or problems in the child component. componentWillReceiveProps gets invoked in the parent component but not in the child component. Please ignore any missing semi-colon or braces because I have omitted some unnecessary codes.
Edit 1: I just duplicated value from props to state just for debugging purposes.
I will appreciate your help. Thank you.
Edit 2: I was directly changing an object in redux actions. That's the reason CWRP was not getting fired. It was the problem. For more check out my answer below.
componentWillReceiveProps will be deprecated in react 17, use componentDidUpdate instead, which is invoked immediately after updating occurs
Try something like this:
componentDidUpdate(prevProps, prevState) {
if (this.prevProps.seaFCLJSON !== this.props.seaFCLJSON) {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
}
};
At the first place it is absolutely meaningless to duplicate value from props to state, what is the meaning of it? Totally pointless, just keep it in props
About your issue - most probably this condition doesnt match, thats why child component doesnt trigger
!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl
check it in debugger
As far as I can see, your problem is that you pass the following to your child component:
<FCLContainer fclJSON={seaFCLJSON} />
But you assume that you receive a prop called 'seaFCLJSON':
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
You should change your code to:
<FCLContainer seaFCLJSON={seaFCLJSON} />
Apart from that, as #Paul McLoughlin already mentioned, you should use the prop directly instead of adding it to your state.
I found the issue I was directly mutating the object in actions. I only knew state should not be directly mutated in class or inside reducer. I changed the actions where I was directly changing an object and then saving it in redux store via dispatch and, then I received the updated props in CWRP. This really took me a lot of times to figure out. This kind of issue is hard to find out at least for me. I guess I get this from https://github.com/uberVU/react-guide/issues/17
A lesson I learned: Never directly mutate an Object
I changed this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
seaFCLJSON.rates.sort = "Faster"; //this is the main culprit
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
to this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
// seaFCLJSON.rates.sort = "Faster"; //this was the main culprit, got lost
seaFCLJSON = {
...seaFCLJSON,
rates: { ...seaFCLJSON.rates, sort: "Faster" }
};
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
the power of not mutating data
Side note: Redux Troubleshooting
I have built a simple counter app:
class Counter extends React.Component {
constructor(props) {
super(props);
this.handleAddOne = this.handleAddOne.bind(this);
this.handleMinusOne = this.handleMinusOne.bind(this);
this.handleReset = this.handleReset.bind(this);
this.state = {
count: 0
};
}
componentDidMount() {
const stringCount = localStorage.getItem('count');
const count = parseInt(stringCount);
if (isNaN(count) === false) {
this.setState(() => ({ count }));
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
localStorage.setItem('count', this.state.count);
console.log('componentDidUpdate');
}
}
handleAddOne() {
this.setState((prevState) => {
return {
count: prevState.count + 1
}
});
}
handleMinusOne() {
console.log('handleMinusOne');
this.setState((prevState) => {
return {
count: prevState.count - 1
}
});
}
handleReset() {
this.setState(() => {
return {
count: 0
}
});
}
render() {
return (
<div>
<h1>Count: {this.state.count}</h1>
<button onClick={this.handleAddOne}>+</button>
<button onClick={this.handleMinusOne}>-1</button>
<button onClick={this.handleReset}>reset</button>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById('app'));
The question I have is with componentDidUpdate(). In it, I am checking to see if the prevState.count is not the same as the this.state.count. If it is not the same, then I set localStorage to the new count. If it is same, I do nothing.
In the current componentDidUpdate(), I need prevProps as an argument for this function to work correctly. For example, if I just have this:
componentDidUpdate(prevState) {
if (prevState.count !== this.state.count) {
localStorage.setItem('count', this.state.count);
console.log('componentDidUpdate');
}
}
Then the component sets localStorage every time the reset button is pressed repeatedly, even though the count remains at 0.
What is going on? Why do I need prevProps for componentDidUpdate() to work correctly, if I am never using props in that function?
The first parameter in componentDidUpdate is prevProps. The second parameter is prevState. The documentation clearly states that:
componentDidUpdate(prevProps, prevState, snapshot)
This
componentDidUpdate(prevState) {...}
is not a correct signature for the hook. Even though the first parameter was called prevState, it contains previous props.
It's possible to alternate function parameters based on its arity but this isn't implemented in React and considered a bad practice in general because this leads to more complex signatures.
To not cause linter warnings, unused parameters can be underscored by convention:
componentDidUpdate(_prevProps, prevState) {...}
It doesn't matter if you are using prevProps in your function or not, it has to be included, since the arguments order in functions does matter.
componentDidUpdate(prevProps, prevState)
If you'd omit the prevProps argument, the prevState (even tho the name would say clearly that it's previous version of state) it would stand for the previous version of props.
It's just a blank. You can use either an underscore, to tell others (or just yourself) that it's not used in that function.
componentDidUpdate(_, prevState)
Because componentDidUpdate receives prevProps as the first argument and prevState as second. If you write the function as componentDidUpdate(prevState) it is receiving prevProps but it will be named prevState. No matter how you name the arguments, they will be prevProps, prevState, and snapshot, respectively.
If you don't need the first argument, you can use an underscore eg. componentDidUpdate(_, prevState).
I'm setting state in the parent component, then passing it as props to a child component. In the child, I would like to make a copy of that state object (received through props) to edit without updating the original state object yet. I will then display that information in the current child component and send the updated state object back to the parent to update the original state object. The now updated parent state would then be used to display information in other child elements.
In child component - where 'this.props.shifts' is the passed down state from the parent:
this.dowCopy = { ...this.props.dow };
this.state = {
dowCopy: this.dowCopy
}
addTempShift = (day) => {
const emptyShift = {arbitraryData};
const dowCopyCopy = {...this.state.dowCopy};
dowCopyCopy[day].shifts.push(emptyShift);
this.setState({ dowCopy: dowCopyCopy })
}
Everything works as intended, but when I set state here in the child component, it's also updating state in the parent. It was my understanding that the spread operator took a copy of the state object. What am I missing?
Spread syntax creates shallow copies as told by #Timothy Wilburn. So, if you change copied object property directly then you will mutate the original one. You can search for "deep copy" or you can again use spread syntax in the child component.
class App extends React.Component {
state = {
dow: {
monday: {
shifts: [{ name: "foo" }, { name: "bar" }],
},
}
}
render() {
return (
<div>
In parent: {JSON.stringify(this.state.dow)}
<Child dow={this.state.dow} />
</div>
);
}
}
class Child extends React.Component {
state = {
dowCopy: { ...this.props.dow }
}
addTempShift = (day) => {
const emptyShift = { name: "baz" };
this.setState( prevState => (
{
dowCopy: { ...prevState.dowCopy, [day]: { shifts:
[...prevState.dowCopy[day].shifts, emptyShift] } }
}
))
}
componentDidMount() {
this.addTempShift( "monday");
}
render() {
return (
<div>In Child: { JSON.stringify(this.state.dowCopy)}</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Oh, boy.
But, I agree with #George Chanturidze, you are overdoing. Do not copy the state like that. If you want to show some changed data in the Child component, then change it using this state as its prop. Then, if you want to change the parent's state, gather this data and send back to the parent with a callback and set the state there.
I am using React's setState method, and calling another function when the state has been updated.
Is there a preferred approach as to how to call the function that is passed to setState as a callback.
Both of the approaches below work but is there any performance implications of using one over the other?
this.setState(prevState => {
return {
result: '1-0'
}
}, this.clearResult(500))
or
this.setState(prevState => {
return {
result: '1-1',
}
}, () => this.clearResult(500))
My clearPin method looks like the following. All of this code is within a React component.
clearResult(time) {
setTimeout(() => {
this.setState({
result: '0-0'
})
}, time)
}
Both of the approaches below work but is there any performance implications of using one over the other?
There's a correctness implication: The first one is incorrect, the second one is correct. :-)
In your first example, you're calling this.clearResult(500) and then calling setState (with the result of calling this.clearResult(500) — undefined, in your example — as its second argument). this.setState(prevState => { ... }, this.clearResult(500)); is just like foo(bar()) — first it calls bar, then it passes the result of calling it into foo.
In your second example, you're passing a function into setState that it will call when the state is updated.
You want the second form (or one of the various equivalents to it).
this.setState(prevState => {
return {
result: '1-1',
}
}, () => this.clearResult(500));
// or: }, this.clearResult.bind(this, 500));
// But the arrow is clear and idiomatic
Here's proof that your first example is calling clearResult before calling setState, and before your state change callback is called:
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {value: "a"};
}
// Overriding it PURELY to show what's happening
setState(...args) {
console.log("setState called");
return super.setState(...args);
}
componentDidMount() {
this.setState(
() => {
console.log("state change callback");
return {value: "b"};
},
this.clearResult(500)
);
}
clearResult(delay) {
console.log("clearResult called");
setTimeout(() => {
this.setState({value: "c"});
}, delay);
}
render() {
return <div>{this.state.value}</div>;
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
whereas with () => this.clearResult(500) instead, clearResult is called after setState (and after the state change):
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {value: "a"};
}
// Overriding it PURELY to show what's happening
setState(...args) {
console.log("setState called");
return super.setState(...args);
}
componentDidMount() {
this.setState(
() => {
console.log("state change callback");
return {value: "b"};
},
() => this.clearResult(500)
);
}
clearResult(delay) {
console.log("clearResult called");
setTimeout(() => {
this.setState({value: "c"});
}, delay);
}
render() {
return <div>{this.state.value}</div>;
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Side note 1: If you want, you can be a bit more concise:
this.setState(
() => ({ result: '1-1' }),
() => this.clearResult(500)
);
Side note 2: There's no need to use the function form if the new state you're passing isn't based on current state or props. In your example, it isn't, so yours is one of the places where using the non-callback form is okay:
this.setState(
{ result: '1-1' },
() => this.clearResult(500)
);
That would not be okay if you were using something from this.state or this.props. In that situation, use the callback form and its prevState and props parameters. Always. And there's little harm in always using the callback form, the overhead of a function call is exceptionally trivial on modern JavaScript engines. (It was even trivial on the slowest JS engine of this century: The one in IE6.)
More on that here and here.
I have a loading spinner in a stateless functional component that I'm using while I check for props.
I'd like to use setTimeout to have the loading spinner display for 5 seconds and then change the content if props are still not available, but this code doesn't seem to work:
function LoadingIndicator() {
let content = <span>Loading Spinner Here</span>;
setTimeout(() => {
console.log('6 second delay');
content = <span>Page could not be loaded.</span>;
}, 6000);
return (
<div>
{content}
</div>
);
}
I believe this doesn't work because nothing tells react to re-render this component, but I'd prefer not to upgrade to a container if possible.
Move the timer to the parent. Have the timer change a state value and in its render, pass that state value as a prop to your LoadingIndicator.
Make your component stateful, so you can change its state easily.
class SpinnerComponent extends React.Component {
constructor() {
super();
this.state = { tooLong: false };
}
componentDidMount() {
var thiz = this;
setTimeout(function () {
thiz.setState({ tooLong: true });
}, 1000);
}
render() {
let content = 'Spinner...';
if (this.state.tooLong) {
content = 'It takes too much time...';
}
return (
<div>{content}</div>
);
}
};
ReactDOM.render(
<SpinnerComponent />,
document.getElementById("app")
);
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>