I would like to be able to use computed values for sub keys when updating state in React.
I understand how to use computed values in straightforward settings like this:
this.setState({ [name]: value });
But I am having trouble getting key-value computation to work for a situation like this:
constructor(props) {
super(props);
this.state = {
foo: { a: 1, b: 2 }
};
}
const keyToChange = 'a';
const value = 3;
this.setState({ foo[keyToChange]: value });
How can I make something that works like
this.setState({ foo.a: value });
But where a can be a computed value?
I have tried the following, but it doesn't seem to work:
const subKeyName = 'a';
// Doesn't work
const nameOfKey = 'foo.' + subKeyName;
this.setState({ [`${nameOfKey}`]: value });
// Doesn't work
this.setState({ foo[subKeyName]: value });
If you want to overwrite the old properties in foo:
this.setState({
foo: {
[keyToChange]: value
}
});
If you want to keep old properties in foo but just add (or replace) one key in it:
this.setState(oldState => {
return {
foo: {
...oldState.foo,
[keyToChange]: value
}
}
});
That last example is using object spread syntax, which is not yet a standardized part of javascript (currently a stage 4 proposal, so it will be part of the language soon). So you should be using this babel plugin if you want to use it at this time. If you don't have that plugin, the equivalent with standard javascript is:
this.setState(oldState => {
return {
foo: Object.assign({}, oldState.foo, {[keyToChange]: 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.
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
})
I have a dictionary named CarValues in my code which contains following data:
CarValues is a dictionary initialized in the state.
dictionary: CarValues
key ==> string
Value ==> Array
key => Honda, Value => white, yellow, red, orange
key => Toyota, Value => white, yellow, green, black
Key => Volkswagen Value => 123, 456, 343
I would like to delete Honda and its value completely from CarValues. Though, I see few similar questions, I couldn't find the best solution for this question.
How can I remove an attribute from a Reactjs component's state object
This should solve your issue
yourMethod(key) {
const copyCarValues= {...this.state.CarValues}
delete copyCarValues[key]
this.setState({
CarValues: copyCarValues,
})
}
I believe in order to truly do this without mutating the state, you will need to re-create the entire state like so.
class Test extends React.Component {
state = {
thingToDelete: {},
otherStuff: {}
};
deleteThingToDelete = () => {
const {thingToDelete, ...state} = this.state;
this.setState(state);
}
}
Using the spread operator, we achieve a shallow clone, so be wary about that. The other option is to use Object.assign but that will also only offer a shallow clone but you will achieve much better browser support.
Probably arriving here a bit late, but here is a way of doing this with hooks and without actually mutating the previous state.
const sampleItems = {
'key1': { id: 1, name: 'test'},
'key2': { id: 2, name: 'test2'},
}
const Test = props => {
const [items, setItems] = useState(sampleItems);
deleteItemFromStateObject = itemKey => {
setItems(({[itemKey]: toDelete, ...rest}) => rest);
}
}
The easiest way to do this would be:
const carValues = Object.assign({}, this.state.carValues)
delete carValues[key]
this.setState({ carValues })
You can use Underscore.js or Lodash http://underscorejs.org/#omit
_.omit(copyCarValues, 'Honda');
First Initialise Array Globally
var dict = []
Add Object into Dictionary
dict.push(
{ key: "One",value: false},
{ key: "Two",value: false},
{ key: "Three",value: false});
Output :
[0: {key: "One", value: false},
1: {key: "Two", value: false},
2: {key: "Three", value: false}]
Update Object from Dictionary
Object.keys(dict).map((index) => {
if (index == 1){
dict[index].value = true
}
});
Output :
[0: {key: "One", value: false},
1: {key: "Two", value: true},
2: {key: "Three", value: false}]
Delete Object from Dictionary
Object.keys(dict).map((index) => {
if (index == 2){
dict.splice(index)
}
});
Output :
[0: {key: "One", value: false},
1: {key: "Two", value: true}]
Here is another simple enough solution to achieve this.
const myCarsValueInState = this.state.myCarsValueInState;
Object.keys(myCarsValueInState).map((index) => {
myCarsValueInState[index] = undefined; // you can update on any condition if you like, this line will update all dictionary object values.
return myCarsValueInState;
});
Simple enough.
I want to use my data which keep in my state instead of the outside class variable (I mean languages if u look at the code below)
In the getSuggestion I change languages.filter(lang.... to this.state.myState.filter(lang... but it's not work
It seem like this.state.myState can't be reach
The error appear at the line return inputLength === 0 ? [] : this.state.myState.filter(lang => in the getSuggestion
import Autosuggest from 'react-autosuggest';
// Imagine you have a list of languages that you'd like to autosuggest.
const languages = [
{
name: 'C',
year: 1972
},
{
name: 'Elm',
year: 2012
},
];
// Teach Autosuggest how to calculate suggestions for any given input value.
const getSuggestions = value => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
return inputLength === 0 ? [] : languages.filter(lang =>
lang.name.toLowerCase().slice(0, inputLength) === inputValue
);
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
const getSuggestionValue = suggestion => suggestion.name;
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}
</div>
);
class Example extends React.Component {
constructor() {
super();
// Autosuggest is a controlled component.
// This means that you need to provide an input value
// and an onChange handler that updates this value (see below).
// Suggestions also need to be provided to the Autosuggest,
// and they are initially empty because the Autosuggest is closed.
this.state = {
value: '',
suggestions: []
myState: [
{
name: 'C',
year: 1972
},
{
name: 'Elm',
year: 2012
}
] ,
};
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: 'Type a programming language',
value,
onChange: this.onChange
};
// Finally, render it!
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
);
}
}
It looks like you are attempting to access this in a function outside of your class, so your this will not be defined in getSuggestions.
If getSuggestions needs to be defined outside of your class, you'll need to make two changes:
define getSuggestions as a normal function as opposed to an arrow function so that we can set getSuggestions's this object manually.
Use Function.prototype.call() to call getSuggestions with a specified value for this.
I wrote up a few examples to show my point. First, notice in the example below, where getSuggestions is an arrow function and it is called by method of Example class:
const getThis = () => {
console.log(this === window || this === undefined); // this is coerced to window or undefined (Strict Mode)
};
class Example {
onSuggestionsFetchRequested() {
console.log(this); // prints correctly
getThis();
};
}
var x = new Example();
x.onSuggestionsFetchRequested(); // prints true once
In getThis, the this object will either be Window or undefined (Strict Mode).
To make the setup above work, we make the two changes proposed above:
const getThis = function() { // change #1: use function() {} vs. () => {}
console.log(this); // correct value for this
};
class Example {
onSuggestionsFetchRequested() {
console.log(this);
getThis.call(this); // change #2: use call()
};
// NOTE: in your code, define onSuggestionsFetchRequested using arrow function syntax like so:
// onSuggestionsFetchRequested = () => { ... };
// I did not use the arrow syntax above so that this example would run in the browser,
// but you would need to in order to use onSuggestionsFetchRequested in callbacks.
}
var x = new Example();
x.onSuggestionsFetchRequested();
Note: I made a small modification above to make the snippet run in the browser. Namely, I defined onSuggestionsFetchRequest as a class method as opposed to a class property (using arrow function syntax) as you have in your code. For your use case, you'll want to keep onSuggestionsFetchRequest defined using arrow function syntax so that it can be used in callbacks.
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