I have an object saved in a state I am trying to create a function to update parts of the object.
function removeError(val) {
setPersonsState((prevState) => ({
...prevState,
val: "",
}));
}
I have tried to pass val to the object key to update it.
onClick={removeError('Bob')}
Where Bob replaces val to update in the object. Sorry if terminology is bad.
Well, you should change it to be in the following way, sine you don't want to change a key named val, but the key with name equal to the value of val
function removeError(val) {
setPersonsState((prevState) => ({
...prevState,
[val]: "",
}));
}
And, as suggested in the comments by #RoshanKanwar, also the click handler is not correct, it should be
onClick = {() => removeError('Bob')}
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'm trying to save both team members in select into an array within a state object. My expectation would be to be able to ctrl-click the options and this would update state (using the on change handler). For right now I have this, which only ends up saving the first option in state:
setProjectDetails((prevState) => ({
...prevState,
[e.target.name]: e.target.value,
}));
I'm working with a mongoose/mongodb backend, so the id's are associated with the team members. I'm looking for the state to end up like this:
const initialProjectDetails = {
title: "test",
description: "test",
teamMembers: ["628c0133e5edf7b21cd8f31b", "628c0133e5edf7b21cd8f31b"], };
Form:
I think you forgot to update the desired key in the object (teamMembers):
setProjectDetails((prevState) => ({ ...prevState, teamMembers: e.target.value, }));
I defined this interface and hook with an initialization:
interface userInterface {
email:string
name:string
last_name:string
}
const [userData, setUserData] = useState <userInterface> ({
email:"",
name:"",
last_name:"",
})
then if you just wanted to change the name only. How should it be done with setUserData?
That is, I want to leave the email and the last_name as they are but only modify the name
#Jay Lu is correct.
I wanted to note a couple things.
Typescript Interfaces
Interface name should use PascalCasing
Key names should use camelCasing
Prefer utility types where appropriate
[optional] Interface name should be prefaced with a capital "I"
This used to be a convention now more people are moving away from it. Your choice
Interface Update: 1) Naming Conventions
interface UserInterface {
email:string
name:string
lastName:string
}
The next thing we want to do is use a utility type to simplify the verbosity of our code.
Interface Update: 2) Utility Type
We will actually change this interface to a type to easily use the utility type Record.
type UserInterface = Record<"email" | "name" | "lastName", string>;
As for your component, you didn't provide much detail there so I will provide details on setting state.
Functional Component: Updating State
It's very important to establish what variables or data you need to "subscribe" to changes. For instance, if the email and name are static (never changing) there is no need to have them in state which would result in a state variable defaulting to an empty string:
const [userData, setUserData] = useState("");
If that's not the case and you indeed need to update and manage the email, name, and lastName updating state is simple: spread the existing state with the updated value. You do this using the setter function provided in the tuple returned from useState. In this case that's setUserData. The setter function takes a value that is the same type as your default or it can accept a callback function where it provides you the current state value. This is really powerful and really useful for updating a state object variable. In your case we have:
setUserData((previous) => ({...previous, name: "Updated name value"});
What's happening here? The setUserData provides us the "previous" state if we pass it a callback. On the first call of this function "previous" is:
{
email: "",
name: "",
lastName: ""
}
We are taking that value and spreading it over in a new object with the updated value. This is the same as Object.assign. If you spread a key that already exists in the object it will be replaced. After we spread our state object looks like:
{
email: "", // didn't change because we didn't pass a new value
lastName: "", // didn't change because we didn't pass a new value
name: "Updated name value" // changed because we passed a new value
}
Which means, if you wanted to update the email you can by simply doing:
setUserData((previous) => ({...previous, email: "hello#world.com"});
Now your state object will be:
{
email: "hello#world.com",
lastName: "",
name: "Updated name value"
}
And if you call setUserData with a callback again, the previous value with be that object above.
If you want to set it back to the original values you can update the entire state without using the callback. Why? Because we don't need to preserve any values since we want to overwrite it:
setUserData({ email: "", lastName: "", name: ""});
There is a slight improvement to that though. If we decide that at some point we want to "reset to default" we should store the default value in a variable and reuse it. Not 100% necessary but it might be a good update especially if you have a complex component.
Quick Note on the Power of Typescript
If you were to try and update state with a new key that you didn't have defined before let's say "helloWorld" typescript will give you an error because "helloWorld" is not defined in your UserData type.
Hopefully #Jay Lu's answer and some of this info helped. If you provide more details we might be able to offer more guidance.
Simple expample:
setUserData((prev) => ({ ...prev, name: 'Name you want' }));
const { useState } = React;
const DemoComponent = () => {
const [userData, setUserData] = useState({
email: "",
name: "",
last_name: ""
});
const handleBtnOnClick = (name) => {
setUserData((prev) => ({ ...prev, name: name }));
};
return (
<div>
<button
onClick={() => {
handleBtnOnClick("Jay");
}}
>
Jay
</button>
<button
onClick={() => {
handleBtnOnClick("Andy");
}}
>
Andy
</button>
<button
onClick={() => {
handleBtnOnClick("Olivia");
}}
>
Olivia
</button>
<div>{JSON.stringify(userData, null, "\t")}</div>
</div>
);
}
ReactDOM.render(
<DemoComponent />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I have a React web app that is effectively a ton of Questions. These questions need to be validated/laid-out based on their own state values (ie: must be a number in a number field), as well as on the values of each other. A few examples of the more complex 'validation':
Questions A, B, and C might be required to have non-empty values before allowing a 'save' button.
Question B's allowable range of values might be dependent on the value of question A.
Question C might only show if question A is set to 'true'.
You can imagine many other interactions. The app has hundreds of questions - as such, I have their configuration in a JSON object like this:
{ id: 'version', required: true, label: 'Software Version', xs: 3 },
{
id: 'licenseType', label: 'License Type', xs: 2,
select: {
[DICTIONARY.FREEWARE]: DICTIONARY.FREEWARE,
[DICTIONARY.CENTER_LICENSE]: DICTIONARY.CENTER_LICENSE,
[DICTIONARY.ENTERPRISE_LICENSE]: DICTIONARY.ENTERPRISE_LICENSE
}
},
... etc.
I would then turn this object into actual questions using a map in the FormPage component, the parent of all the questions. Given the need to store these interaction in the closest common parent, I store all of the Question values in a formData state variable object and the FormPage looks like so:
function FormPage(props) {
const [formData, setFormData] = useState(BLANK_REQUEST.asSubmitted);
const handleValueChange = (evt, id) => {
setFormData({ ...formData, [id]: evt.target.value})
}
return <div>
{QUESTIONS_CONFIG.map(qConfig => <Question qConfig={qConfig} value={formData[qConfig.id]} handleValueChange={handleValueChange}/>)}
// other stuff too
</div>
}
The Question component is basically just a glorified material UI textField that has it's value set to props.value and it's onChange set to props.handleValueChange. The rest of the qConfig object and Question component is about layout and irrelevant to the question.
The problem with this approach was that every keypress results in the formData object changing... which results in a re-render of the FormPage component... which then results in a complete re-render/rebuild of all my hundreds of Question components. It technically works, but results performance so slow you could watch your characters show up as you type.
To attempt solve this, I modified Question to hold it's own value in it's own state and we no longer pass formData to it... the Question component looking something like this:
function Question(props) {
const { qConfig, valueChangedListener, defaultValue } = props;
const [value, setValue] = useState(props);
useEffect(() => {
if (qConfig.value && typeof defaultValue !== 'undefined') {
setValue(qConfig.value);
}
}, [qConfig.value])
const handleValueChange = (evt, id) => {
setValue(evt.target.value);
valueChangedListener(evt.target.value, id)
}
return <div style={{ maxWidth: '100%' }}>
<TextField
// various other params unrelated...
value={value ? value : ''}
onChange={(evt) => handleValueChange(evt, qConfig.id)}
>
// code to handle 'select' questions.
</TextField>
</div>
}
Notably, now, when it's value changes, it stores it's own value only lets FormPage know it's value was updated so that FormPage can do some multi-question validation.
To finish this off, on the FormPage I added a callback function:
const processValueChange = (value, id) => {
setFormData({ ...formData, [id]: value })
};
and then kept my useEffect that does cross-question validation based on the formData:
useEffect(() => { // validation is actually bigger than this, but this is a good example
let missingArr = requiredFields.filter(requiredID => !formData[requiredID]);
setDisabledReason(missingArr.length ? "Required fields (" + missingArr.join(", ") + ") must be filled out" : '');
}, [formData, requiredFields]);
the return from FormPage had a minor change to this:
return <div>
{questionConfiguration.map(qConfig =>
<Question
qConfig={qConfig}
valueChangedListener={processValueChange}
/>
</ div>
)
}
Now, my problem is -- ALL of the questions still re-render on every keypress...
I thought that perhaps the function I was passing to the Question component was being re-generated so I tried wrapping processValueChange in a useCallback:
const processValueChange = React.useCallback((value, id) => {
setFormData({ ...formData, [id]: value })
}
},[]);
but that didn't help.
My guess is that even though formData (a state object on the FormPage) is not used in the return... its modification is still triggering a full re-render every time.
But I need to store the value of the children so I can do some stuff with those values.
... but if I store the value of the children in the parent state, it re-renders everything and is unacceptbaly slow.
I do not know how to solve this? Help?
How would a functional component store all the values of its children (for validation, layout, etc)... without triggering a re-render on every modification of said data? (I'd only want a re-render if the validation/layout function found something that needed changing)
EDIT:
Minimal sandbox: https://codesandbox.io/s/inspiring-ritchie-b0yki
I have a console.log in the Question component so we can see when they render.
In react, I'm trying with a onClick event add a new object (dictionary) with the id of the clicked object as a key (prevId).
When the first object is created, it will not have value on that parameter so I put a default value '0', however in that case the key prevId will have the value [Object object]
I've tried with ??, and with the functions String(), JSON.Stringify(), and Object.ToString() and still can't that clickedCompId (and prevId) will be '0'
What else can I do?
Thanks
const addCount = (clickedCompId = '0') => {
//setDivVector((prev) => [...prev, {id: randomString.generate()}]);
setDivComp(() => ({
id: randomString.generate(10),
prevId: clickedCompId
}));
};
when you call addCount, you have to explicitly pass the param to the addCount function.
onClick={() => addCount(id)}
When you do onClick={addCount} it is the equivalent of
onClick={evt => addCount(evt)}
And that is not what you want because you need another data than the click js event.