React useState hook - when to use previous state when updating state? - reactjs

In the code below, the form is not updated correctly unless I pass in previous state to the state setting call ('setForm').
To see it in action, run the code as-is, and the console will print 'true', indicating it worked. That's using 'validate1' function. If you replace that with 'validate2' (which doesn't use previous state), it fails to print 'true'.
It seems like in validate2, the second setForm call overwrites the form.long state of the first setForm call, due to the async nature of these calls. But why does validate1 work? Why does using previous state cause it to work? Any documentation on this behavior would be really helpful.
(I can make validate2 work by having one setForm that sets both fields, but the code below is contrived to show a difference in the two ways setForm can be called, with and without previous state.)
CodeSandbox
const {useState, useEffect} = React;
function LoginForm() {
const [form, setForm] = useState({
username: "",
password: "",
long: null
});
useEffect(() => {
console.log(form.long);
}, [form.long]);
const validate1 = (e) => {
e.preventDefault();
setForm((prevState) => ({
...prevState,
long: form.password.length >=3 ? true : false
}));
setForm((prevState) => ({
...prevState,
username: "*****"
}));
};
const validate2 = (e) => {
e.preventDefault();
setForm({
...form,
long: form.password.length >=3 ? true : false
});
setForm({
...form,
username: "*****"
});
};
const updateField = (e) => {
setForm({
...form,
[e.target.name]: e.target.value
});
};
return (
<form onSubmit={validate1}>
<label>
Username:
<input value={form.username} name="username" onChange={updateField} />
</label>
<br />
<label>
Password:
<input
value={form.password}
name="password"
type="password"
onChange={updateField}
/>
</label>
<br />
<button>Submit</button>
</form>
);
}
// Render it
ReactDOM.render(
<LoginForm/>,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

In the React documentation, they mention the following,
If the next state depends on the current state, we recommend using the
updater function form, instead:
this.setState((state) => {
return {quantity: state.quantity + 1};
});
That's the reason validate1 works, because the second call to setForm depends on the previous setForm call state, and the state will be up to date if the updater function form is used.

Why does validate1 work though? Why does using previous state cause it to work?
setForm({
...form,
long: form.password.length >=3 ? true : false
});
setForm({
...form,
username: "*****"
});
The value of form in the second setForm is still the old value until the next re-render. It doesn't reflect the updated form from the previous setForm.
React setState and useState does not make changes directly to the state object.
setState and useState create queues for React core to update the state object of a React component.
So the process to update React state is asynchronous for performance reasons. That’s why changes don’t feel immediate.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately

Related

How do I override the defaultValues in useForm and maintain the isDirty function?

I have a situation where I need to call my DB to obtain some default values. But in the off-chance the data does not exist, I will set some default values in my useForm. Basically, this means that the defaultValues declared in useForm is a fallback value if I fail to obtain the default values from the DB.
From what Im understanding, according to the documentation with regards to useForm,
The defaultValues for inputs are used as the initial value when a component is first rendered, before a user interacts with it.
Or basically, the useForm is one of the first things defined when the page is loaded.
So, unless I can call my DB before useForm is loaded, Im a little stuck on this.
I've read that each Controller field can have something called defaultValue which sounds like the solution, but the documentation mentioned a caveat of
If both defaultValue and defaultValues are set, the value from defaultValues will be used.
I considered setValues but I want to use the isDirty function, which allows field validation and the value used to check whether the form is dirty is based on the useForm defaultValues. Thus, if I were to use setValues, the form would be declared dirty, which is something I do not want.
TL;DR this is what I want:
This is my initial value(the fallback value, result A).
const { formState: { isDirty }, } = useForm(
{defaultValues:{
userID: "",
userName: "",
userClass: "administrator",
}}
);
What I want to do is to make a DB call and replace the data, so that it would now look something like this(result B) if the call is successful(if fail, it will remain as result A).
const { formState: { isDirty }, } = useForm(
{defaultValues:{
userID: "1",
userName: "user",
userClass: "administrator",
}}
);
Please note that the DB call will replace only the userID and userName default values, the default value for userClass will be maintained.
So, the flow is as such:
Case 1: render form -> result A -> DB call -> success -> result B
Case 2: render form -> result A -> DB call -> fail/no data -> result A
So, unless I actually key in an input that is different from the default values of either results depending on the case, both Case 1 and Case 2 should return isDirty==false when I check it.
For react-hook-form#7.22.0 and newer
I think you want to use reset here in combination with useEffect to trigger it when your DB call has finished. You can additionally set some config as the second argument to reset, for example affecting the isDirty state in your case.
Although your answer works there is no need to use getValues here, as reset will only override the fields which you are passing to reset. Your linked answer is for an older version of RHF, where this was necessary.
Also if you're not adding a field dynamically on runtime then you can just pass the whole object you get from your DB call to reset and set the shouldUnregister to true in your useForm config. This way props from your result object which haven't got a corresponding form field will get ignored.
export default function Form() {
const { register, handleSubmit, reset, formState } = useForm({
defaultValues: {
userID: "",
userName: "",
userClass: "administrator"
},
shouldUnregister: true
});
console.log(formState.isDirty);
const onSubmit = (data) => {
console.log(data);
};
const onReset = async () => {
const result = await Promise.resolve({
userID: "123",
userName: "Brian Wilson",
otherField: "bla"
});
reset(result);
};
useEffect(() => {
onReset();
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>User ID</label>
<input type="text" {...register("userID")} />
<label>User Name</label>
<input type="text" {...register("userName")} />
<label>User Class</label>
<input type="text" {...register("userClass")} />
<input type="submit" />
</form>
);
}
Here is a Sandbox demonstrating the explanation above:
For older versions
Just merge your defaultValues or field values via getValues with the result of your DB call.
export default function Form() {
const {
register,
handleSubmit,
reset,
formState,
getValues,
control
} = useForm({
defaultValues: {
userID: "",
userName: "",
userClass: "administrator"
},
shouldUnregister: true
});
console.log(formState.isDirty);
const onSubmit = (data, e) => {
console.log(data);
};
const onReset = async () => {
const result = await delay();
reset({ ...getValues(), ...result });
};
useEffect(() => {
onReset();
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>User ID</label>
<input type="text" {...register("userID")} />
<label>User Name</label>
<input type="text" {...register("userName")} />
<label>User Class</label>
<Controller
name="userClass"
control={control}
render={({ field }) => <input type="text" {...field} />}
/>
<input type="submit" />
</form>
);
}
I went and further googled the issues I had with #knoefel's original answer, and I came across this
Thus, the solution I came up with, based on a combination with #knoefel's answer and the answer in the link:
useEffect(async () => {
let dataArray={};
let result= fetch('my-db-call');
if(result) {
dataArray['userID']=result.userID
dataArray['userName']=result.userName
}
reset({...getValues(), ...dataArray})
}, [])
Apparently, what happens is that the the reset function will first set the values, result A, using ...getValues() and any subsequent data after will replace the previously set values only if it exist. (eg. if my dataArray object lacks the userID key, it will not replace the userID default with the new default. ).
Only after both getValues and dataArray is set then will it reset the default values.
As far as I can tell, this is more or less incline with what I need and should give me result B.

Is there a way to stop useMutation from setting `data` to `undefined` before updating it to a new value?

I have a mock mutation like so:
interface Person {
firstName: string;
lastName: string;
}
async function sendPersonApi({ firstName, lastName }: Person) {
await new Promise((res) => setTimeout(res, 1000));
return {
firstName,
lastName,
status: "success"
};
}
I have two components: <Form /> and <Output />. I basically want the mutation to run on form submit of the <Form />, and then show the result of that mutation in the <Output />.
I have a CodeSandbox with this behavior mostly working: https://codesandbox.io/s/long-worker-k350q?file=/src/App.tsx
Form
const personAtom = atom<Person>({
firstName: "",
lastName: ""
});
function Form() {
const [formState, setFormState] = useAtom(personAtom);
const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(event) => {
event.preventDefault();
const formElement = event.currentTarget;
const formData = new FormData(formElement);
const firstName = formData.get("firstName") as string;
const lastName = formData.get("lastName") as string;
setFormState({ firstName, lastName }); // This does not update the `data`/`isLoading`/`isError` in the <Output /> component
},
[setFormState]
);
return (
<form id="name-form" onSubmit={handleSubmit}>
<input name="firstName" /> <br />
<input name="lastName" /> <br />
<button>Submit</button>
</form>
);
}
Output
function Output() {
const [person] = useAtom(personAtom);
const { mutate, data, isLoading, isError } = useMutation(sendPersonApi, {
mutationKey: "sendPerson"
});
useEffect(() => {
mutate(person);
}, [person]);
return (
<output name="name-output" form="name-form">
<p>data: {JSON.stringify(data)}</p>
<p>isLoading: {JSON.stringify(isLoading)}</p>
<p>isError: {JSON.stringify(isError)}</p>
</output>
);
}
Initially, I had wanted to implement this by doing the mutation itself inside the <Form /> submit handler, but it seems like I was not be able to access the data/isLoading/isError in a different component even if I used the same mutationKey in useMutation.
So I ended up changing the implementation so that the submit handler just updates some global state (using jotai atoms), and then in the Output I have an effect that listens for changes to this global state and then calls mutate().
The problem now though is that the data always gets reset to undefined in every subsequent submit before getting a new value. Is there a way to stop useMutation from setting data to undefined before updating it to a new value?
Also, is there a cleaner way of using the result of a mutation in another component? I'd prefer to have the mutate() done inside the Form and then somehow use the data/isLoading/isError in a different component without having to rely on useEffect.
I think I'm going to just create a separate atom to hold the global state for the current data. Then I'll use the onSuccess of the useMutation to update this global state from within <Form /> and then just use that global state inside <Output />.

React & Redux: State of component is not updating even with mapStateToProps

I posted an answer below, but if someone can explain why this is necessary you'll get the bounty, I went through a redux tutorial and feel like I didn't learn about mapDispatchToProps, only mapStateToProps. If you can explain at a deeper level what exactly mapStateToProps and mapDispatchToProps are doing and how they are different I'll give you the bounty.
Minimum Reproducible Example
I have a mapStateToProps function that looks like
const mapStateToProps = state => {
return {
firstName: state.firstName,
middleName: state.middleName,
lastName: state.lastName,
}
}
const ReduxTabForm = connect(mapStateToProps)(MyTab)
In my MyTab component I have a button that is supposed to be inactive if these 2 field do not have anything entered in, but the state of whether or not the button is disabled does not change
function App() {
const {firstName, lastName} = store.getState().formData
const isDisabled = () => {
const {firstName, lastName} = store.getState().form
const requiredFields = [firstName, lastName]
alert(requiredFields)
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields[i]){
return true
}
}
return false
}
return (
<div className="App">
<div className='bg-light rounded'>
<div className='px-sm-5 pt-0 px-4 flexCenterCol mx-5'>
<div>
<input
type='text'
className="form-control"
value={store.getState().formData['firstName']}
placeholder="First Name"
onChange={(e) => {
store.dispatch(setFormData({'firstName': e.target.value}))
}}
></input>
<input
type='text'
className="form-control"
value={store.getState().formData['lastName']}
placeholder="Last Name"
onChange={(e) => {
store.dispatch(setFormData({'lastName': e.target.value}))
}}
></input>
</div>
<button
type="submit"
disabled={isDisabled()}
>
Button
</button>
</div>
</div>
</div>
)
}
That alert statement executes on page refresh, but does not execute any time after that when I enter data in. I have checked that the redux state updating and it is. The button will not update though, and the isDisabled function will not run more than once
I looked at your reducer code and it looks like this:
...
const reducer = combineReducers({
formData: formReducer,
})
export default reducer
Which means your redux state structure is like this:
state = {
formData: {
firstName: <value>,
middleName: <value>,
lastName: <value>,
}
}
Solution
So, to make your component re-render when the redux state is changed, you need to subscribe to the correct state variables in your mapStateToProps function. Change it to this and will work:
const mapStateToProps = state => {
return {
firstName: state.formData.firstName, // Note that I added "formData"
middleName: state.formData.middleName,
lastName: state.formData.lastName
}
}
Couple of side notes:
It's a better to use the props instead of directly accessing the redux store.
For debugging, console.log is preferred over alert. React DevTools and Redux DevTools are even better.
I don`t know if this will solve your problems, but maybe it is one of the things below:
1 - As Mohammad Faisal said the correct form of calling props should be
const { firstName, lastName } = props;
2 - Instead of reduxTabForm, maybe you could use this instead:
export default connect(mapStateToProps)(MyTab);
3 - And finally, maybe it is an error in the "isDisabled":
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields){
return false
}
}
If you look carefully, you can see that you are not checking if there is an error inside requeiredFields, your are looking if that doesnt exist if (!requiredFields), maybe changing the condition to if(!requiredFields[i]) so it check each variable and not if the array doesn`t exists.
Edit: the return condition is correct? Returning False when something doesn`t exists?
const ReduxTabForm = connect(mapStateToProps,null)(MyTab)
Try this code snippet, as you are not passing dispatch function to your component. It is better to pass null value.
mapdispatchtoProps is the same basic theory of mapStateToProps.You are storing the function inside the store(which usually in actions) and when rendering the component you attach those function in store to your props. After rendering the component you will be able to run the functions which are in store.
your state values are passed to your component as props. so if you want to access them inside component you should do something like this
const {firstName, lastName} = props
What you could do is something like:
<button
type="submit"
disabled={!fistname && !lastname}
/>
this way if any of your fields be falsy, button is disabled. (empty string is falsy)
The React redux documentation also explains mapDispatchToProps
If you call your action called setFormData from your redux/actions.js then you will be mapping the action setFormData (which is an object) to your component. The action setFromData replace mapDispatchToProps. This is what the documentation says about mapping actions to your components
If it’s an object full of action creators, each action creator will be
turned into a prop function that automatically dispatches its action
when called.
To fix your problem, change your connect function call to this.
const ReduxApp = connect(
setFormData,
mapStateToProps
)(App)
Your issues is with this code isDisabled()
const isDisabled = () => {
const {firstName, lastName} = store.getState().form
const requiredFields = [firstName, lastName]
alert(requiredFields)
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields){
return false
}
}
return true
}
You are trying to test in loop !requiredFields, an array you created which always return false, even if it doesn't have values. It's a reference type. What you can do now is
const isDisabled = () => {
const {firstName, lastName} = store.getState().form
const requiredFields = [firstName, lastName]
alert(requiredFields)
for(let i = 0; i < requiredFields.length; i=i+1){
if (!requiredFields[i]){
return false
}
}
return true
}
Your loop will check firstNAme and LastName values if they are undefined or not and test should respond with this.
The problem is in the mapStateToProps method. Your code is setting firstNAme and lastName as formData within state object and you are trying to access it from state object directly.
This codesandbox has your project updated with the fix. I didn't fix any other thing in your code so everything is as it is only mapStateToProps should be:
const mapStateToProps = (state) => {
return {
firstName: state.formData["firstName"],
middleName: state.middleName,
lastName: state.formData["lastName"]
};
};
and this will fix your issue. mapStateToPropsis kind of a projection function which will project entire store to some object which will only have properties based on requirements of your component.
Redux re-render components only when store state changes. Check if you are only updating store's state property not whole state because redux compare state by comparing their references, so if you are doing something like this inside your reducer:
State.firstName = action.payload.firstName;
return State;
Then change this to this:
return {...State, firstName: action.payload.firstName}
Note: If you unable to grasp that then kindly provide your reducer code too so that I can see how you are updating your store state.
Looking at your MRE on GitHub, the first thing I want to say is that your button will only have one state and it is the one that the isDisabled() method returns when you refresh the page. This is because the App component it's not getting refresh every time you write on the input fields, so you will never be able to make it change.
you need to subscribe to the correct state variables in your mapStateToProps function. Like this:
const mapStateToProps = state => {
return {
firstName: state.formData.firstName,
middleName: state.formData.middleName,
lastName: state.formData.lastName
}
}
So now that you have this right, you have to introduce this props into your component, like this:
function App({firstName, lastName}) { ...
Another thing to add, is that your are not initializing the states when you create your reducer in reducer.js. If you don't do this, your initial states for firstName and lastName will be null. Here is how you should do it:
import {combineReducers} from 'redux'
import {SET_FORM_DATA} from './actions'
const initialState = {
firstName: "",
lastName: ""
}
const formReducer = (state = initialState, action) => {
if (action.type === SET_FORM_DATA){
return {
...state,
...action.payload
}
}else{
return state
}
}
const reducer = combineReducers({
formData: formReducer,
})
export default reducer
Finally you have to update App:
function App({firstName, lastName}) {
return (
<div className="App">
<div className='bg-light rounded'>
<div className='px-sm-5 pt-0 px-4 flexCenterCol mx-5'>
<div>
<input
type='text'
className="form-control"
placeholder="First Name"
onChange={(e) => {
store.dispatch(setFormData({'firstName': e.target.value}));
console.log(firstName === "h");
}}
></input>
<input
type='text'
className="form-control"
placeholder="Last Name"
onChange={(e) => {
store.dispatch(setFormData({'lastName': e.target.value}))
alert(JSON.stringify(store.getState()))
}}
></input>
</div>
<button
type="submit"
disabled={firstName == "" || lastName == ""}
>
Button
</button>
</div>
</div>
</div>
);
}
So in this way you will be able to update the states from your store and have the dynamic behavior that you were looking for.
In isDisabled function you read data from form: const {firstName, lastName} = store.getState().form but I guess they are saved to formData.
Changing const {firstName, lastName} = store.getState().form to const {firstName, lastName} = store.getState().formData should help.

Hook managed state on Form does not update in parent component

I have a form:
<FieldLabel label='Label'>
<Textfield
value={formState.name}
onChange={name => setFormState({ ...formState, name })}
name="someName"/>
</FieldLabel>
<FieldLabel label='Description'>
<Textarea
value={formState.description}
onChange={description => setFormState({ ...formState, description}}
name="someDesc"/>
</FieldLabel>
This form is 'dumb' meaning it gets its formState from a parent and dispatches setFormState when input is changed.
In the parent component it looks like:
<MyForm
formState={formState}
setFormState={setFormState} />
where formState and setFormState are hooks:
const initialFormState: FormState = {
name: '',
description: '',
}
const [formState, setFormState] = useState<FormState>({ ...initialFormState });
However, the state does not change, nor am I able to write anything into the inputs on the Form. When I click submit the initial state is logged, and all the properties are empty strings, just like in the initialFormState.
If anyone needs more info I'd be happy to provide it. Thanks.
Adding a codesandbox link: https://codesandbox.io/s/admiring-haze-gsy59?file=/src/App.js
onChange on input does not emit the value, it emits an event.
Replace with onChange={e => setFormState({ ...formState, name: e.target.value })}

How to save input default value in ReactJS

I'm working on my first project and I found another problem. My professor told me, my edit form can't be empty at start and it has to store default values. And that's what I've made. Sadly I found it problematic. What's wrong?
First thing:
*My default input can't be changed.
Second:
*Even when my default value is visible for me, after POST it sends "" as value.
I've tried a lot of things, like setting value from this.state.userResponse.map, but ofcourse it fails. Even tried to escape React and save default values into LocalStorage only to make it work.
class EditProtege extends Component {
state = {
firstname: "",
userResponse: []
};
getUserByID() {
fetch(`http://localhost:9000/proteges/${utils.i}`)
.then(res => res.json())
.then(res => this.setState({ userResponse: res }))
.catch(err => err);
}
handleNameChange = event => {
this.setState({ firstname: event.target.value });
};
handleSubmit = event => {
event.preventDefault();
axios
.put(`http://localhost:9000/proteges/${utils.i}`, {
firstname: this.state.firstname,
})
.then(res => {
console.log(res);
console.log(res.data);
});
window.location.reload();
};
componentDidMount() {
this.getUserByID();
//this.setDefaultValues();
}
render() {
return (
<div className="creation-form">
{this.state.userResponse.map(resp => (
<div>
<br />
<rb.Form onSubmit={this.handleSubmit}>
<rb.FormGroup>
<rb.Row>
<rb.Col md={12}>
<rb.FormControl
type="text"
name="firstname"
size="md"
onChange={this.handleNameChange}
placeholder="Name"
default={this.userResponse.proteges}
required
/>
</rb.Col>
</rb.FormGroup>
<rb.Button type="submit" variant="dark" size="lg" block>
Zatwierdź edycję
</rb.Button>
</rb.FormGroup>
</rb.Form>
</div>
))}
</div>
);
}
}
export default EditProtege;
Like I've said before, I want to POST default value and to be able to change it if I want to. Now its untouchable and sends empty string.
UPDATE: changed default into defaultValue and it seems like I can edit it again, still, problem with saving this data still occurs.
I would suggest setting your state's firstName to a default value, then use the value attribute on your rb input component and set it to this.state.firstName while still keeping the onChange in place and get rid of the default attribute.
This way, each time you change the input value of your field, the state will be reset effectively, and it will also update the input field value. When you send your post request with the current state data, it should be accurate as well.

Resources