React useState with Object with multiple boolean fields - reactjs

I have this object initialised with useState:
const [
emailNotifications,
setEmailNotifications,
] = useState<emailNotifications>({
rating: false,
favourites: false,
payments: false,
refunds: false,
sales: false,
});
And I created a function that should dynamically change the value for each field but I am struggling with assigning the opposite boolean value onClick. This is the function:
const handleEmailNotificationsSettings = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setEmailNotifications({
...emailNotifications,
[event.target.id]: !event.target.id,
});
};
What am I doing wrong?

Your approach is right just one minor thing that you trying to achieve here is wrong.
setEmailNotifications({
...emailNotifications,
[event.target.id]: !event.target.id, //Here
});
when you are setting dynamic value to the state you are expecting it to be the Boolean value which is not
solution:
setEmailNotifications({
...emailNotifications,
[event.target.id]: !emailNotifications[event.target.id],
});

#kunal panchal's answer is totally valid Javascript, but it does cause a Typescript error because the type of event.target.id is string so Typescript does not know for sure that it's a valid key of emailNotifications. You have to assert that it is correct by using as.
!emailNotifications[event.target.id as keyof EmailNotifications]
One way to avoid this is to get the boolean value by looking at the checked property on the input rather than toggling the state.
As a sidenote, it's a good best practice to get the current state by using a setState callback so that you always get the correct value if multiple updates are batched together.
const _handleEmailNotificationsSettings = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setEmailNotifications(prevState => ({
...prevState,
[event.target.id]: event.target.checked,
}));
};
This is probably the best solution for a checkbox.
Another approach which is more flexible to other situations is to use a curried function. Instead of getting the property from the event.target.id, where it will always be string, we pass the property as an argument to create an individual handler for each property.
const handleEmailNotificationsSettings = (
property: keyof EmailNotifications
) => () => {
setEmailNotifications((prevState) => ({
...prevState,
[property]: !emailNotifications[property]
}));
};
or
const handleEmailNotificationsSettings = (
property: keyof EmailNotifications
) => (event: React.ChangeEvent<HTMLInputElement>) => {
setEmailNotifications((prevState) => ({
...prevState,
[property]: event.target.checked
}));
};
which you use like this:
<input
type="checkbox"
checked={emailNotifications.favourites}
onChange={handleEmailNotificationsSettings("favourites")}
/>
Those solutions avoid having to make as assertions in the event handler, but sometimes they are inevitable. I am looping through your state using (Object.keys(emailNotifications) and I need to make an assertion there because Object.keys always returns string[].
import React, { useState } from "react";
// I am defining this separately so that I can use typeof to extract the type
// you don't need to do this if you have the type defined elsewhere
const initialNotifications = {
rating: false,
favourites: false,
payments: false,
refunds: false,
sales: false
};
type EmailNotifications = typeof initialNotifications;
const MyComponent = () => {
// you don't really need to declare the type when you have an initial value
const [emailNotifications, setEmailNotifications] = useState(
initialNotifications
);
const handleEmailNotificationsSettings = (
property: keyof EmailNotifications
) => (event: React.ChangeEvent<HTMLInputElement>) => {
setEmailNotifications((prevState) => ({
...prevState,
[property]: event.target.checked
}));
};
return (
<div>
{(Object.keys(emailNotifications) as Array<keyof EmailNotifications>).map(
(property) => (
<div key={property}>
<label>
<input
type="checkbox"
id={property}
checked={emailNotifications[property]}
onChange={handleEmailNotificationsSettings(property)}
/>
{property}
</label>
</div>
)
)}
</div>
);
};
export default MyComponent;

Related

Typing a letter in the middle of a string takes the cursor back to the end - Material UI

I implemented a ChipInput component which when focused will behave a string and onBlur will be transformed into a Chip component with the chips introduced by a comma. All works well except for a single issue. When I type in multiple words, and I want to add a new letter in between, the cursor takes me to the end but I want the cursor to stay at the same position.
Could someone please help me out? Thanks.
This is my Stackblitz link
This issue occurs because the ChipInput component is rerendered every time there is a change in the value of name in index.tsx.
One solution to this problem is to change the way state inside ChipInput.tsx is updated.
Firstly, remove the following useEffect code from ChipInput.tsx:
useEffect(() => {
setState({
inputValue: values,
value: compact(map(split(values, separator), (i) => trim(i))),
});
}, [values]);
Change the initial value of state to:
const [state, setState] = useSetState<IChipInputState>({
value: compact(map(split(values, separator), (i) => trim(i))),
inputValue: values,
isFocused: false,
});
Modify the setState statement the deleteChip function to:
setState({
value: newChipValues,
inputValue: join(newChipValues, `${separator} `),
isFocused: false,
});
The deleteChip function would then look like:
const deleteChip = (chipValue: string): void => {
const newChipValues = without(state.value, chipValue);
setState({
value: newChipValues,
inputValue: join(newChipValues, `${separator} `),
isFocused: false,
});
onHandleChange(join(newChipValues, `${separator} `), name);
};
Finally add
setState(prev => ({...prev, inputValue: onChangeValue }));
to the end of the onInputChange prop of the Autocomplete component in ChipInput.tsx. The onInputChange prop would then look like:
onInputChange={(event, newInputValue) => {
let onChangeValue = newInputValue;
if (acceptOnlyNumbers) {
onChangeValue = replace(newInputValue, /[^\d, ]/, '');
}
if (acceptOnlyNumbersPercentage) {
onChangeValue = replace(newInputValue, /[^\d,% ]/, '');
}
onHandleChange(onChangeValue, name);
setState(prev => ({...prev, inputValue: onChangeValue }));
}}

object Object when entering a value into the input

An error appears when entering any value
At first I used class components, but then I started redoing them for functional ones and everything broke.
In my state I get a value like this:title > title:"[object Object] and the last symbol which I entered.
Here is the code
reducer
export const postsReducer = (state = initialState, action) => {
switch (action.type) {
case CREATE_POST:
return {...state, posts: state.posts.concat(action.payload)}
default:return state
}
}
Action
export function createPost(post){
return{
type: CREATE_POST,
payload:post
}
}
and a function in a class component
this.setState(prev => ({
...prev, ...{
[event.target.name]: event.target.value
}
}))
so I converted it into a functional one. In setTitle I store the value const [title, setTitle] = useState('');
setTitle(prev => ({
...prev, ...{
[event.target.name]:event.target.value
}
}))
This depends on how you are referencing value on your input. Given your current setTitle operation, if you are referencing title like:
<input type="text" name="title" onInput={handleInput} value={title} />
The problem is that you are turning title into an object with your setTitle operation. An object with property "title" such as { title: "some text" }. That then get's stringified into [object Object].
You could change setTitle to the following to keep it as a flat string:
setTitle(e.target.value)
Or you could change the structure of your state to be an object of form properties:
// create an object with properties to hold your form values
const [form, setForm] = useState({ title: '' });
function handleInput(e) {
setForm(prev => ({
...prev,
[e.target.name]: e.target.value,
}));
}
// reference specific property on form state object
<input type="text" onInput={handleInput} value={form.title} />
Hopefully that helps!

Common toggle function doesn't work well with setState()

I have a state which represents this interface:
StateInterface {
variableOne: string;
variableTwo: boolean;
variableThree: boolean;
// ...
}
And toggle function:
toggleFunction = (value: keyof StateInterface): void => {
this.setState((state) => ({
[value]: !state[value]
}));
};
Not all variables are boolean in my state
But TSLint is telling me that the function is missing some properties.
Is it possible to use only one toggle function for my state?
so what about to make your function to only accpet keys that are boolean in your interface?
interface IStateInterface {
variableOne: string;
variableTwo: boolean;
variableThree: boolean;
// ...
}
// here you make type filtered type
type FilterType<Base, Condition> = {
[Key in keyof Base]:
Base[Key] extends Condition ? Key : never
};
// so this is type where all type that don't go throught condition are "never"
type filteredState = FilterType<IStateInterface, boolean>;
and here just pick the keys
type allowedKeys = filteredState [keyof filteredState];
so in your function you have
toggleFunction = (value: allowedKeys ): void => {
this.setState((state) => ({
[value]: !state[value]
}));
};
playground
Could you please more elaborate the query more specifically about what your toggle function will do.
From above toggle function I can see that you are changing the state of a boolean state variable.
What about variableOne which is string.
For your case you have to setState conditional based on the value.
If-else might work to check value is boolean or not.
toggleFunction = (value: keyof StateInterface): void => {
if (typeof variable === "boolean")
{
this.setState((prevState) => ({
[value]: !prevState[value]
}));
} else {
this.setState((prevState) => ({
[value]: 'Some value'
}));
}
};
for setState method we have 2 options for using:
In your case, you used function, so which will be mutated only property not for all state's properties.
You can try to use this code:
toggleFunction = (value: keyof StateInterface): void => {
this.setState((state) => {
value.forEach(property => {
if( typeof(state[property]) != 'string'){
state[property]= !state[property]
}
});
return state;
};
I don't know why, but it works:
this.setState((state) => ({
...state,
[value]: !state[value],
}));
Without ...state I get an error about missing fields

Simple setState on object using hooks

const [state, setState] = useState({ city: '', country: '' });
const handleCityChange = event => {
setState(prevState => {
console.log(prevState);
return { ...prevState, city: event.target.value };
});
};
//...
<input
type="text"
placeholder="city"
value={state.city}
onChange={handleCityChange}
/>
I'm trying to destructuring prevState, which is an object, and update only the city property. On first keystroke, it's working fine without error, but as soon as I'm typing second letter, i will hit error
Uncaught TypeError: Cannot read property 'value' of null
May I know which part is the null coming from? And from chrome console I see below warning not sure if it's related
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https:...//react-event-pooling for more information.
UPDATES:
If we're not using setState functionally, then it's working fine?
const handleCityChange = event => {
setState({ ...state, city: event.target.value });
};
An event has to be handled synchronously in React. You can extract the value from the target before you call setState:
const handleCityChange = event => {
const { value } = event.target;
setState(prevState => {
return { ...prevState, city: value };
});
};
Another way of going about it is to persist the event, and it can be used asynchronously.
const handleCityChange = event => {
event.persist();
setState(prevState => {
return { ...prevState, city: event.target.value };
});
};

Best way to set state using dynamic key name?

I have a state that is as follows:
state = {
isDirty: false,
form: {
username: '',
firstName: '',
lastName: ''
}
}
I have a couple of input fields that are linked back to the state via the onChanged event that call using a matching ID and using the function below:
inputChangedHandler = (event) => {
const updatedState = { ...this.state, isDirty: true };
updatedState.form[event.target.id] = event.target.value;
this.setState(updatedState);
}
My question is this the best way to set the state when the form is being bound? Is there a way to make this to use even less lines without getting too overly complicated? I'm finding that I have to do this bit of code on EVERY page that I use a form for two-way binding (is there a better approach?).
Any input would be appreciated! Thanks!
Actually, there are many ways to do that.
I feel this is a simple way using spread operator
inputChangedHandler = (event) => {
const id = [event.target.id];
const value = [event.target.value];
this.setState(prevState => ({
form: {...prevState.form,
[id]: value
}, isDirty: true
}))
}
if you want to make it shorter:
inputChangedHandler = (id, value) => {
const { form } = this.state;
this.setState({
isDirty: true,
form: Object.assign({}, form, { [id]: value }
// or using lodash, this way you will be able to update nested object values
form: Object.assign({}, form, _.set(form, id, value)
});
}
...
<input id="username" onChange={({ target }) => inputChangeHandler(target.id, target.value)} />
though I would suggest you to use reduxForm https://redux-form.com/7.3.0/examples/simple/

Resources