I'm confused.
How change specific element in array of object? For example, i want change 'isComplete' in object with id=1
Anything like this? In this case your new state depends on the old state so you have to use the setState's function parameter version.
this.setState(prevState => {
const updatedTasks = prevState.task.map(task => {
return (task.id === 1 ? Object.assign({}, task, {isComplete: !task.isComplete}) : task)
})
return {task: updatedTasks}
})
Related
I use Recoil state management in ReactJS to preserve a keyboard letters data, for example
lettersAtom = atom(
key: 'Letters'
default: {
allowed : ['A','C','D']
pressedCounter : {'A':2, 'D':5}
}
)
lettersPressedSelect = selector({
key: 'LettersPressed',
get: ({ get }) => get(lettersAtom).pressedCounter, //Not work, returns undefined
set: () => ({ set }, pressedLetter) => {
let newState = {...lettersAtom};
newState.pressedCounter[pressedLetter]++;
set(lettersAtom, newState);
}
}),
In functional component i use
const [letters,setLetters] = useRecoilState(lettersAtom);
const [pressedCounter, setPressedCounter] = useRecoilState(lettersPressedSelect);
each time the a keyboard letter pressed the pressedCounter I want to increased for corresponded letter like that
setPressedCounter('A');
setPressedCounter('C'); ///etc...
How to achieve that ? Does recoil have a way to get/set a specific part/sub of json attribute ? (without make another atom? - I want to keep "Single source of truth")
Or do you have a suggetion better best practice to do that ?
There are some bugs in your code: no const, braces in atom call and no get inside the set. You also need spread the pressedCounter.
Overwise your solution works fine.
In Recoil you update the whole atom. So in this particular case you probably don't need the selector. Here is a working example with both approaches:
https://codesandbox.io/s/modest-wind-kosp7o?file=/src/App.js
It a best-practice to keep atom values rather simple.
You can update the state based on the existing state in a selector in a couple ways. You could use the get() callback from the setter or you could use the updater form of the setter where you pass a function as the new value which receives the current value as a parameter.
However, it's a good practice to have symmetry for the getter and setters of a selector. For example, here's a selector family which gets and sets the value of a counter:
const lettersPressedState = selectorFamily({
key: 'LettersPressed',
get: letter => ({ get }) => get(lettersAtom).pressedCounter[letter],
set: letter => ({ set }, newPressedValue) => {
set(lettersAtom, existingLetters => ({
...existingLetters,
pressedCounter: {
...existingLetters.pressedCounter,
[letter]: newPressedValue,
},
});
},
});
But note that the above will set the new value with a new counter value where you originally wanted the setter to increment the value. That's not really setting a new value and is more like an action. For that you don't really need a selector abstraction at all and can just use an updater when setting the atom:
const [letters, setLetters] = useRecoilState(lettersAtom);
const incrementCounter = pressedLetter =>
setLetters(existingLetters => ({
...existingLetters,
pressedCounter: {
...existingLetters.pressedCounter,
[pressedLetter]: (existingLetters.pressedCounter[pressedLetter] ?? 0) + 1,
},
});
Note that this uses the updater form of the selector to ensure it is incrementing based on the current value and not a potentially stale value as of the rendering.
Or, you can potentially simplify things more and use simpler values in the atoms by using an atom family for the pressed counter:
const pressedState = atomFamily({
key: 'LettersPressed',
default: 0,
});
And you can update in your component like the following:
const [counter, setCounter] = useRecoilState(pressedState(letter));
const incrementCounter = setCounter(x => x + 1);
Or create an general incrementor callback:
const incrementCounter = useRecoilCallback(({ set }) => pressedLetter => {
set(pressedState(pressedLetter)), x => x + 1 );
});
So the sort answer help by user4980215 is:
set: () => ({ get, set }, pressedLetter) => {
let newState = {...get(lettersAtom)};
newState.pressedCounter[pressedLetter]++;
set(lettersAtom, newState);
}
I have a component with an array of objects in local state:
const [myState, setState] = useState<SomeObjectType[]>([]);
I can update a single object in that array by making a copy of the entire array and then update the property of the single object I wish to update:
OPTION 1
const state = [...myState];
state[id].description = description;
setState(state);
Or I can use map:
OPTION 2
const newState = talkingPoints.map(el => {
// 👇️ if id equals, update description property
if (el.id === id) {
return {...el, description};
}
// 👇️ otherwise return as is
return el;
});
setData(newState);
Or can I do this (since I am 100% certain the id exists in the array)?
OPTION 3
const handleUpdate = () => {
setState(prevState => ({
...prevState,
prevState[id].description = description
}))
}
For cases where I am not 100% certain I can use map and find it:
OPTION 4
const handleUpdate = () => {
setState(prevState => ({
myState: prevState.myState.map(el => el.id === id ? { ...el, description } : el)
}))
}
What is recommended/best practice?
Is Option 1 (fully array copy) faster than using map?
Option 3 and opntion 4 are incorrect, the prevState/myState is an array and you are returning an object, this will surely cause error in typescript. As to option 1 and 2, they only differ between declaritive and imperitive way of programming. And declaritive programming makes more readable code.
On option 1:
const newState = [...myState];
newState[index].description = "new desc";
// Access index position as opposed to id
// since the item's position may not align with it's id
setState(newState );
On option 2 with map function, it can be written as follows:
setState(myState.map(item => item.id === id ? {...item, item.description: "new desc"} : item))
In conclusion, use map/filter is more preferable to update array.
Here's sandbox to compare option 1 and 2
I am updating a sub array in a react state array of 'components' with a new key value like this.
this.setState({
components: {
...this.state.components,
[this.state.key]: {
...this.state.components[this.state.key],
[key]: v
}
}
});
this works but it changes this.state.components from an array into an object which I don't want.
I can do
var result = Object.keys(this.state.components).map(function (k) {
return { [k]: this.state.components[k] };
});
this.setState({components: result});
to fix the data but it seems messy and inefficient to set state twice. Is there a better way? I've tried various forms of using [] instead of {}, but from my understanding of the spread operator this should work.
You can use map on the components array currently in state and return the object as is if the key in the state doesn't match the component index, or change the [key] property if they do match.
this.setState(prevState => ({
components: prevState.components.map((c, index) =>
prevState.key === index ? { ...c, [key]: v } : c
)
}));
When I click on button then onClick triggers correct function, run half through and jumps to other function which is not related to it and run through half of it and jumps back to first function, runs half trough again and drops error
Uncaught TypeError: _this.state.searchValue.toLowerCase is not a function
Interesting part is that I click other button before which triggers this function with toLowerCase() and there is no errors.
I dont have any idea whats going on here but so far i was trying to remove few lines to see which line cause it because I dont think that line with toLowerCase() realy is the reason. Everything works when I remove lines where is first this.setState.
Here is my function:
( Alerts is used to track where function is at, that how i know
that it run half through only. It never reach alert("DDD").
This function is which is triggered with button onClick like it should be )
onSelect = (e) => {
const data = e.target.getAttribute('data-id');
const itemId = e.target.getAttribute('data-id');
const itemIdState = !this.state[e.target.getAttribute('data-id')];
alert("AAA")
this.setState(state => { // <--- Somehow problem comes from this setState function
const newState = {};
for (const dataId in state) {
newState[dataId] = dataId === data
}
alert("BBB")
return newState
});
alert("CCC")
this.setState(State => ({
[itemId]: itemIdState,
}), function() {
alert("DDD")
if(this.state[itemId] === true){
this.setState({isAnySelected: true})
}else if(this.state[itemId] === false){
this.setState({isAnySelected: false})
}
})
}
This is other function which is triggered by mistake and is not related to other. It is just returning component which is displayed and when I press on its button then i have this issue.
filterSearch = (id, title, path) => {
let name = title.toLowerCase()
let filter = this.state.searchValue.toLowerCase()
if(name.includes(filter)){
return <SearchResult key={id} data-id={id} pName={path} onClick={this.onSelect} selected={this.state[id]} />
}
}
And here is from where filterSearch is triggered. Behind this.props.searchResult is Redux.
{this.props.searchResult ? this.props.searchResult.map(category =>
this.filterSearch(category.id, category.title, category.path)
) : null
}
I think I see what the problem is: in your problematic this.setState, you cast everything in your state to a boolean:
this.setState(state => {
const newState = {};
for (const dataId in state) {
newState[dataId] = dataId === data
}
alert("BBB")
return newState
});
Your for() statement ends up comparing searchValue to data (some kind of ID), which I imagine more often than not will not be the case, so searchValue ends up getting set to false.
And what happens when you try to do .toLowerCase() on a Boolean?
To fix this, consider structuring your state like this:
this.state = {
searchValue: '',
ids: {},
};
Then, replace your problematic this.setState with something like this:
this.setState((state) => {
const newIDs = {
// Create a clone of your current IDs
...state.ids,
};
Object.keys(newIDs).forEach(key => {
newIDs[key] = key === data
});
alert("BBB")
return {
// searchValue will remain untouched
...state,
// Only update your IDs
ids: newIDs,
}
});
What exactly are you wanting to do here?
this.setState(state => {
const newState = {}; // You are initializing an object
for (const dataId in state) {
newState[dataId] = dataId === data // You are putting in an array every property of state that is equal to data
}
return newState
});
So irrevocably, your this.state.searchValue property will be changed to something else, which is of boolean type. So toLowerCase being a function for string.prototype, you will get an error.
You should describe what you where aiming to get here.
I'm rendering a list of products according to a specific value. I'm doing the render with a Picker Component and when it's different of 306, I'm loading the selected products but IF I come back on the first PickerItem (306) I want to show ALL the products again...
For instance :
if (Value != '306') {
this.setState({
// isAll: false,
labels,
Colours: Value,
filteredProducts: this.state.displayProducts.filter(product => product.colour == Value)
}, () => {
this.onLoadFilteredLabels();
});
} else {
this.setState({
// isAll: true,
Colours: Value,
displayProducts: this.state.products,
displayLabels: this.state.labels
});
}
I'm looking for some advice if there is a better way of doing this ?
Do you think I should separe every setState ?
It's working but I have the feeling that it's a bit tricky and I'm still learning. So I know I can have goods advices here with people here !
Thank you
The easiest approach is to create a custom object for setting states and just passing arguments in a custom method. Apart from that, using ternary could be beneficial:
let stateObj = value === '306' ? {obj : {
// isAll: true,
Colours: Value,
displayProducts: this.state.products,
displayLabels: this.state.labels
}, func : () => {
return false;
}} : {obj : {
// isAll: false,
labels,
Colours: Value,
filteredProducts: this.state.displayProducts.filter(product => product.colour == Value)
}, func : () => {
this.onLoadFilteredLabels();
}}
this.stateSetter(stateObj);
Next define your custom method:
stateSetter = (stateObj) => {
let {obj, func} = stateObj;
this.setState(obj,func);
}
You can make use of ternary operator, equality check etc. So that your code will look clean. For example,
this.setState({
isAll: Value === '306',
filterProducts: Value === '306' ? [yourFilterProducts] : []
});
Generally, it's ok if you have several setState() calls in different branches of a condition.
But in your case, it's better to update in the state only Colours and filter products directly inside the render method:
render() {
const {products: allProducts, Colours} = this.state;
const displayProducts = Value != '306' ? allProducts.filter(product => product.colour == Colours) : allProducts;
return (
<div>
{displayProducts.map(product => <YourProductComponent key={product.id} product={product}/>)}
<div>
);
}
React documentation recommends that if you can calculate required data from state or/and props you should do it instead of introducing new prop or state variable.
PS: there's a common recommendation to not to refer to this.state when in setState().
React runs setState in batch so you can end up with referring to outdated state. You should pass a function as the first argument of setState. More details here.