callback in setState doesn't seem to work - reactjs

I have the following:
addTodo() {
const text = prompt("TODO text please!")
this.setState({todos:[...this.state.todos,
{id:id++,
text: text,
checked:false}]})
console.log(this.state)
}
the console shows an empty array which makes sense as setState is asyncronous. I change the function to use a callback:
addTodo() {
const text = prompt("TODO text please!")
this.setState(function(prevState){
return {todos: [...prevState.todos,
{id: id++,
text: text,
checked: false} ]}
})
console.log(this.state)
}
console.log is still showing an empty array. Doesn't the use of the callback update setState?

setState function's second argument should be the function which need to be called after setting the state. So you should pass callback as second argument like this
this.setState({
todos:[
...this.state.todos,
{id:id++,text: text,checked:false}
]
},() => {console.log(this.state)})

Related

Value is not written to this.state

On a React page I have a method by means of which I'm trying to set the state. However, it doesn't seem to work (see the console.log). What am I doing wrong?
editPlan = (cell, row) => {
return (
<img
src={edit}
alt="edit"
onClick={() => {
console.log(row.id); // prints 2
this.setState({ editId: row.id, openEditModal: true });
console.log(JSON.stringify(this.state.editId)); // prints "". I would expect 2.
console.log(JSON.stringify(this.state.openEditModal)); // prints false. I would expect true.
this.props.getPlan(row.id);
}}
/>
);
};
setState works asynchronously -- it just queues up your desired state change and will get to it later to optimize how your component will re-render.
You can pass a callback to the function as the last argument which will be called when the state change has occurred.
this.setState(
{ editId: row.id, openEditModal: true },
() => {
console.log(JSON.stringify(this.state.editId));
console.log(JSON.stringify(this.state.openEditModal));
}
);
See: https://reactjs.org/docs/react-component.html#setstate
You can try using the state callback function because setState works asynchronously
this.setState({ editId: row.id, openEditModal: true },()=>{
console.log(JSON.stringify(this.state.editId));
console.log(JSON.stringify(this.state.openEditModal));
this.props.getPlan(row.id);
});

React Hooks - keep arguments reference in state

I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea

React State one state behind

So I am having a very minimal program where on click of TouchableOpacity I add an item to my array. So the code for same looks like
// Initial state is []
onClick = () => {
this.setState(
{
arr: this.state.arr.concat({a: 1, b: 2}),
},
console.log(this.state.arr), // gives []
);
};
The console.log in the callback function for setState is behind one state. And on nextClick it gives the state as [{"a": 1, "b": 2}]. So how can I get the current updated state?
You are not passing console.log(...) inside a callback. Try the following :-
onClick = () => {
this.setState(
{
arr: this.state.arr.concat({a: 1, b: 2}),
},
()=>console.log(this.state.arr), //
);
I think you intend to pass a callback as a second argument but now you call console.log directly.
Just change it to () => console.log(this.state.arr),

Infinite loop where useEffect is being re called on re-render even when dependancy value is unchanged

I'm getting an infinite loop in the following and can't figure out why.
In one file I have the following declared. Essentially what I'm trying to achieve is to return a stateful errors response which will be updated by revalidating value only when value changes against two schemas (FYI i'm using YUP but that's irrelevant)
export const myValidation = (validationSchema: any, requiredSchema: any, value: any) => {
const [errors,setErrors] = useState([{}]);
useEffect(() => {
Promise.all([
validationSchema.validate(value, { abortEarly: false }),
requiredSchema.validate(value, { abortEarly: false })])
.catch(error => {
let _errors = error.inner.map((err: any) => {
let path = err.path;
let message = err.message;
return { [path]: message };
});
setErrors(_errors);
});
},[value]);
return Object.assign({}, ...errors);
}
I'm referencing this from within my react component as follows:
const errors = myValidation(requiredSchema,validationSchema,{
title: titleValue,
description: descriptionValue
});
titleValue and descriptionValue are both props on the component.
The component itself looks like this:
export default function TitleDescriptionSection({titleValue, descriptionValue, onChange, disabled}: Props) {
const errors = myValidation(requiredSchema,validationSchema,{
title: titleValue,
description: descriptionValue
});
console.log(errors);
return (
<Card><Card.Section>
<FormLayout>
<TextField type="text" label="Title" value={titleValue}
onChange={(value) => onChange({title: value, description: descriptionValue})}
disabled={disabled} error={errors ? errors.title : null}/>
<RichTextField label='Description' value={descriptionValue}
onChange={(value: string) => onChange({title: titleValue, description: value})}
disabled={disabled} error={errors ? errors.description : null}/>
</FormLayout>
</Card.Section>
</Card>
);
}
When the field values change they call onChange which causes the parent to re-render and pass the values for those fields back as props where the are run through myValidation again.
This all works fine until the myValidation catch is triggered and setErrors is called, sending it into an infinite loop. For example there is a validation condition which determines the "title" field to be invalid if it is an empty string.
I believe that the problem is that value (in myValidation) on the re-render is being treated as "changed" on each re-render even though the contents of value hasn't changed from the previous render. I just can't seem to wrap my head around why.
Probably what is happening is that you are creating a new object at each render (with the {}) and passing it as argument to the myValidation function. Your error set probably triggers a render on the parent component, which re-renders and recreates the value.
The value is considered to be changed because you are creating a new object. If you memoize the object replacing:
{
title: titleValue,
description: descriptionValue
}
with a memoized version like
React.useMemo({
title: titleValue,
description: descriptionValue
}, [titleValue, descriptionValue])
The object will only be created if the values of titleValue or descriptionValue changes, and the infinite loop will not happen
The final code for the function invocation would look like this
const errors = myValidation(requiredSchema,validationSchema,
React.useMemo({
title: titleValue,
description: descriptionValue
}, [titleValue, descriptionValue])
);

Clearing inputs in React

I have a component in which I create a new post. I have a state used to configure and create a form. The main object in the state is called formControls and inside each element looks something like this:
title: {
elementType: "input",
elementConfig: {
type: "text",
placeholder: "Title"
},
value: "",
validation: {
required: true
},
valid: false,
isTouched: false
}
Then I have a submit handler in which I create a new post and I try to clear the inputs. I m using 2 way binding so I try to clear by looping through state, make copies and update the values for each elements in the formControls : title, author and content like this:
for (let key in this.state.formControls) {
const updatedState = { ...this.state.formControls };
const updatedInput = { ...this.state.formControls[key] };
updatedInput.value = "";
updatedState[key] = updatedInput;
console.log(updatedState);
this.setState({
formControls: updatedState
});
}
The things is that it only clears the last element in the form (textarea). I console logged updatedState and in each iteration it clears the current input but in the next iteration the previous cleared input has again the value before clearing so only the last element is cleared in the end. If i move const updatedState = { ...this.state.formControls };
outside the for loop is behaves as it should. Does this happen because of async operation of setState() and it doesn t give me the right previous state when I try to update in each iteration?
I was hoping that maybe someone could help me understand why is like this. I would post more code but is quite long.
The data available to you in the closure is stale after the first call to setState. All iterations in the for .. in block will be run with the "old" data, so your last iteration is the one which is actually setting the fields to the values as they were when the for .. in loop began.
Try calling setState only once and put all your required changes into that.
const updatedFormControls = { ...this.state.formControls };
for (let key in this.state.formControls) {
const updatedInput = { ...updatedFormControls[key] };
updatedInput.value = "";
updatedFormControls[key] = updatedInput;
}
console.log(updatedFormControls);
this.setState({
formControls: updatedFormControls
});
Another way to do the same thing might look like this:
this.setState(state => ({
...state,
formControls: Object.keys(state.formControls).reduce(
(acc, key) => ({
...acc,
[key]: { ...state.formControls[key], value: '' }
}),
{}
)
});

Resources