state default values
state = {
moveType: {
value: 0,
open: false,
completed: false
}
};
// callback to update new state
let step = 'moveType';
let val = 3; // new value
let newObj = { ...this.state[step], value: val };
console.log(newObj);
this.setState({[step]: newObj }, function () {console.log(this.state);});
console.log(newObj) shows new values proper, but this.state still shows old values.. can you tell me what i'm doing wrong?
Setting state in react is pretty sensitive thing to do.
The best practices I've used to is always control object deep merge manually and use this.setState(state => { ... return new state; }) type of call, like in this example:
this.setState(state => ({
...state,
[step]: { ...(state[step] || {}), ...newObj },
}), () => console.log(this.state));
SNIPPET UPDATE start
[step]: { ...state[step], ...newObj }
Changed to:
[step]: { ...(state[step] || {}), ...newObj }
To deal correctly with cases, when state does not have this step key yet
SNIPPET UPDATE end
Thing is, that when you use this.state (in let newObj = { ...this.state[step]), it might have an outdated value, due to some pending (not merged yet) changes to the state, that you've called just couple of milliseconds ago.
Thus I recommend to use callback approach: this.setState(state => { ... use state and return new state;}) which guarantees that the state you use has latest value
Related
I have a pimRegistration state initialization as shown in the chrome redux-devtools screen capture below. The nesting being referenced is pimRegistration (state.domain.patient):
I updated the patient.name object with the following spread operator statement:
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
...where property is the "name" property of the patient object with value. After the update, the following screenshot shows the new state:
Note that the original patient object (purple in the screenshot) is updated with the name object, duplicated and placed at the root of the state (yellow in screenshot).
I would like to overwrite the properties of the pimRegistration(state).domain.patient object, not to create a new patient object.
The state update is called as shown below.
store.update((state) => ({
...state,
...patientPath, // state.domain.patient
...{ [property]: value },
}));
I have tried my different combinations without achieving the desired result.
The complete update function is shown below.
update(property: string, path: string, value: any) {
const paths: string[] = path.split(".");
const pathReducer = (state: IRegistrationState, path_: string) => {
if (paths.length <= 0) {
return state.domain;
}
return state[path_];
};
const domainPath = state.domain;
let patientPath, nokPath, referrerPath;
if (path.includes("patient")) {
patientPath = paths.reduce(pathReducer, state);
}
if (path.includes("nok")) {
nokPath = paths.reduce(pathReducer, state);
}
if (path.includes("referrer")) {
referrerPath = paths.reduce(pathReducer, state);
}
store.update((state) => ({
...state,
...patientPath,
...{ [property]: value },
}));
}
The function above is invoked with the following statement in Angular 2.
if (this.path.includes("patient")) {
this._repo.update("name", "domain.patient", this.name);
}
Thanks
Deep updates to a store can be tricky. In your function you seem to be spreading the updates at the root rather than at the level you want the update at. This answer here outlines the usual practice to update the state. In short, something like
const newState = {
...state,
domain: {
...state.domain,
patient: {
...state.domain.patient,
[property]: value
}
}
}
Dynamically passing a path and updating this state can be… cumbersome. There are libraries that can help you do it such as immer, but you can possibly hack your way around with normal JS/TS.
const [obj, setObj] = useState{
id1: false,
id2: false,
...
})
const someFunc = async(id)=> {
setObj({...obj, [id]: true})
await longApiCall()
setObj({...obj, [id]: false})
}
This function gets called a couple times (or more), and this should set both to true then both to false, but they complete at close to the same time. Because setting state is asynchronous, each set state only flips one to false because the second one overwrites it back to true.
What's the best way to ensure the state object always gets the correct updates?
I know that I can use a ref or directly set the property on the obj without using the state setter, but I want to maintain the behavior that this state change causes a rerender.
// after calls are finished
obj = {
id1: true,
id2: false
}
//desired outcome
obj = {
id1: false,
id2: false
}
Use the function version of set state instead. React will pass you in the latest value of the state, and you can calculate the new state from that:
const someFunc = async(id)=> {
setObj(prev => {
return {...prev, [id]: true};
});
await longApiCall();
setObj(prev => {
return {...prev, [id]: false};
});
}
This is the cleanest solution I could come up with.
const objMimic = useRef({})
const someFunc = async(id)=> {
objMimic.current[id] = true
setObj({...obj, [id]: true})
await longApiCall()
objMimic.current[id] = false
setObj({...objMimic.current})
}
I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});
I've been thinking about what would be the best way among these options to update a nested property using React setState() method. I'm also opened to more efficient methods considering performance and avoiding possible conflicts with other possible concurrent state changes.
Note: I'm using a class component that extends React.Component. If you're using React.PureComponent you must be extra careful when updating nested properties because that might not trigger a re-render if you don't change any top-level property of your state. Here's a sandbox illustrating this issue:
CodeSandbox - Component vs PureComponent and nested state changes
Back to this question - My concern here is about performance and possible conflicts between other concurrent setState() calls when updating a nested property on state:
Example:
Let's say I'm building a form component and I will initialize my form state with the following object:
this.state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
},
email: {
touched: false,
dirty: false,
valid: false,
invalid: false,
value: 'some_initial_value'
}
}
}
From my research, by using setState(), React will shallow merge the object that we pass to it, which means that it's only going to check the top level properties, which in this example are isSubmitting and inputs.
So we can either pass it a full newState object containing those two top-level properties (isSubmitting and inputs), or we can pass one of those properties and that will be shallow merged into the previous state.
QUESTION 1
Do you agree that it is best practice to pass only the state top-level property that we are updating? For example, if we are not updating the isSubmitting property, we should avoid passing it to setState() in other to avoid possible conflicts/overwrites with other concurrent calls to setState() that might have been queued together with this one? Is this correct?
In this example, we would pass an object with only the inputs property. That would avoid conflict/overwrite with another setState() that might be trying to update the isSubmitting property.
QUESTION 2
What is the best way, performance-wise, to copy the current state to change its nested properties?
In this case, imagine that I want to set state.inputs.username.touched = true.
Even though you could do this:
this.setState( (state) => {
state.inputs.username.touched = true;
return state;
});
You shouldn't. Because, from React Docs, we have that:
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props.
So, from the excerpt above we can infer that we should build a new object from the current state object, in order to change it and manipulate it as we want and pass it to setState() to update the state.
And since we are dealing with nested objects, we need a way to deep copy the object, and assuming you don't want to use any 3rd party libraries (lodash) to do so, what I've come up with was:
this.setState( (state) => {
let newState = JSON.parse(JSON.stringify(state));
newState.inputs.username.touched = true;
return ({
inputs: newState.inputs
});
});
Note that when your state has nested object you also shouldn't use let newState = Object.assign({},state). Because that would shallow copy the state nested object reference and thus you would still be mutating state directly, since newState.inputs === state.inputs === this.state.inputs would be true. All of them would point to the same object inputs.
But since JSON.parse(JSON.stringify(obj)) has its performance limitations and also there are some data types, or circular data, that might not be JSON-friendly, what other approach would you recommend to deep copy the nested object in order to update it?
The other solution I've come up with is the following:
this.setState( (state) => {
let usernameInput = {};
usernameInput['username'] = Object.assign({},state.inputs.username);
usernameInput.username.touched = true;
let newInputs = Object.assign({},state.inputs,usernameInput);
return({
inputs: newInputs
});
};
What I did in this second alternative was to create an new object from the innermost object that I'm going to update (which in this case is the username object). And I have to get those values inside the key username, and that's why I'm using usernameInput['username'] because later I will merge it into a newInputs object. Everything is done using Object.assign().
This second option has gotten better performance results. At least 50% better.
Any other ideas on this subject? Sorry for the long question but I think it illustrates the problem well.
EDIT: Solution I've adopted from answers below:
My TextInput component onChange event listener (I'm serving it through React Context):
onChange={this.context.onChange(this.props.name)}
My onChange function inside my Form Component
onChange(inputName) {
return(
(event) => {
event.preventDefault();
const newValue = event.target.value;
this.setState( (prevState) => {
return({
inputs: {
...prevState.inputs,
[inputName]: {
...prevState.inputs[inputName],
value: newValue
}
}
});
});
}
);
}
I can think of a few other ways to achieve it.
Deconstructing every nested element and only overriding the right one :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
username: {
...prevState.inputs.username,
touched: true
}
}
}))
Using the deconstructing operator to copy your inputs :
this.setState(prevState => {
const inputs = {...prevState.inputs};
inputs.username.touched = true;
return { inputs }
})
EDIT
First solution using computed properties :
this.setState(prevState => ({
inputs: {
...prevState.inputs,
[field]: {
...prevState.inputs.[field],
[action]: value
}
}
}))
You can try with nested Object.Assign:
const newState = Object.assign({}, state, {
inputs: Object.assign({}, state.inputs, {
username: Object.assign({}, state.inputs.username, { touched: true }),
}),
});
};
You can also use spread operator:
{
...state,
inputs: {
...state.inputs,
username: {
...state.inputs.username,
touched: true
}
}
This is proper way to update nested property and keep state immutable.
I made a util function that updates nested states with dynamic keys.
function _recUpdateState(state, selector, newval) {
if (selector.length > 1) {
let field = selector.shift();
let subObject = {};
try {
//Select the subobject if it exists
subObject = { ..._recUpdateState(state[field], selector, newval) };
} catch {
//Create the subobject if it doesn't exist
subObject = {
..._recUpdateState(state, selector, newval)
};
}
return { ...state, [field]: subObject };
} else {
let updatedState = {};
updatedState[selector.shift()] = newval;
return { ...state, ...updatedState };
}
}
function updateState(state, selector, newval, autoAssign = true) {
let newState = _recUpdateState(state, selector, newval);
if (autoAssign) return Object.assign(state, newState);
return newState;
}
// Example
let initState = {
sub1: {
val1: "val1",
val2: "val2",
sub2: {
other: "other value",
testVal: null
}
}
}
console.log(initState)
updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
console.log(initState)
You pass a state along with a list of key selectors and the new value.
You can also set the autoAssign value to false to return an object that is a copy of the old state but with the new updated field - otherwise autoAssign = true with update the previous state.
Lastly, if the sequence of selectors don't appear in the object, an object and all nested objects with those keys will be created.
Use the spread operator
let {foo} = this.state;
foo = {
...foo,
bar: baz
}
this.setState({
foo
})
Suppose I have a redux store with this state structure:
{
items: {
"id1" : {
foo: "foo1",
bar: "bar1"
},
"id2": {
foo: "foo2",
bar: "bar2"
}
}
}
This store evolves by receiving full new values of items:
const reduceItems = function(items = {}, action) {
if (action.type === 'RECEIVE_ITEM') {
return {
...items,
[action.payload.id]: action.payload,
};
}
return items;
};
I want to display a Root view that renders a list of SubItem views, that only extract a part of the state.
For example the SubItem view only cares about the foos, and should get it:
function SubItem({ id, foo }) {
return <div key={id}>{foo}</div>
}
Since I only care about "subpart" of the states, that's what I want to pass to a "dumb" Root view:
const Root = function({ subitems }) {
// subitems[0] => { id: 'id1', foo: "foo1" }
// subitems[1] => { id; 'id2', foo : "foo2" }
const children = subitems.map(SubItem);
return <div>{children}</div>;
};
I can easily connect this component to subscribe to changes in the state:
function mapStatesToProps(state) {
return {
subitems: xxxSelectSubItems(state)
}
}
return connect(mapStatesToProps)(Root)
My fundamental problem is what happens when the part of the state that I don't care about (bar) changes.
Or even, when I receive a new value of an item, where neither foo nor bar has changed:
setInterval(() => {
store.dispatch({
type: 'RECEIVE_ITEM',
payload: {
id: 'id1',
foo: 'foo1',
bar: 'bar1',
},
});
}, 1000);
If I use the "naive" selector implementation:
// naive version
function toSubItem(id, item) {
const foo = item.foo;
return { id, foo };
}
function dumbSelectSubItems(state) {
const ids = Object.keys(state.items);
return ids.map(id => {
const item = state.items[id];
return toSubItem(id, item);
});
}
Then the list is a completely new object at every called, and my component gets rendered everytime, for nothing.
Of course, if I use a 'constant' selector, that always return the same list, since the connected component is pure, it is re-renderered (but that's just to illustrate connected components are pure):
// fully pure implementation
const SUBITEMS = [
{
id: 'id0',
foo: 'foo0',
},
];
function constSelectSubItems(state) {
return SUBITEMS;
}
Now this gets a bit tricky if I use an "almostConst" version where the List changes, but contains the same element.
const SUBITEM = {
id: 'id0',
foo: 'foo0',
};
function almostConstSelectSubItems(state) {
return [SUBITEM];
}
Now, predictably, since the list is different, even though the item inside is the same, the component gets rerendered every second.
This is where I though 'reselect' could help, but I'm wondering if I am not missing the point entirely. I can get reselect to behave using this:
const reselectSelectIds = (state, props) => Object.keys(state.items);
const reselectSelectItems = (state, props) => state.items;
const reselectSelectSubItems = createSelector([reSelectIds, reSelectItems], (ids, items) => {
return ids.map(id => toSubItem(id, items));
});
But then it behaves exactly like the naive version.
So:
is it pointless to try to memoize an array ?
can reselect handle this ?
should I change the organisation of the state ?
should I just implement shouldComponentUpdate on the Root, using a "deepEqual" test ?
should I give up on Root being a connected component, and make each LeafItems be connected components themselves ?
could immutable.js help ?
is it actually not an issue, because React is smart and will not repaint anything once the virtual-dom is computed ?
It's possible what I'm trying to do his meaningless, and hides an issue in my redux store, so feel free to state obvious errors.
You're definitely right about the new array references causing re-renders, and sort of on the right track with your selectors, but you do need to change your approach some.
Rather than having a selector that immediately returns Object.keys(state.item), you need to deal with the object itself:
const selectItems = state => state.items;
const selectSubItems = createSelector(
selectItems,
(items) => {
const ids = Object.keys(items);
return ids.map(id => toSubItem(id, items));
}
);
That way, the array will only get recalculated when the state.items object is replaced.
Beyond that, yes, you may also want to look at connecting your individual list item components so that each one looks up its own data by ID. See my blog post Practical Redux, Part 6: Connected Lists, Forms, and Performance for examples. I also have a bunch of related articles in the Redux Techniques#Selectors and Normalization and Performance#Redux Performance sections of my React/Redux links list.