My form has two inputs:
- username (
type="text"
) - email (
type="email"
)
I also have a .json
file as a "database" to do some validation. The submit button can only be clicked if the user enters a username that doesn't exist on the .json
file. The thing is I don't know if I should pass the "...state" object on every action.type. I am getting some bugs that seem to go away when I use the "...state", but I don't understand why nor if I should always use it.
Here's my code:
const formReducer = (state, action) => {
switch (action.type) {
case "USERNAME_INPUT":
return {
...state,
usernameValue: action.payload,
};
case "API_RETURN_USERNAME":
return {
...state,
usernameValue: action.payload.match
? action.payload.username
: "",
usernameIsValid: !action.payload.match,
emailValue: action.payload.match ? action.payload.email : "",
emailIsValid: !action.payload.email,
apiReturned: true,
};
case "EMAIL_INPUT":
return {
...state,
emailValue: action.payload.value,
emailIsValid: action.payload.isValid,
formIsValid: action.payload.isValid && state.usernameIsValid,
apiReturned: false,
};
default:
return {
usernameValue: "",
emailValue: "",
usernameIsValid: false,
emailIsValid: false,
formIsValid: false,
apiReturned: false,
};
}
CodePudding user response:
The reducer is always called with 2 arguments: The previous state value and The action (most often, an object)
The role of reducer function is to compute on these 2 values and generate the next value of state(read a new state object) and discard old state
All action may not change all keys of your state which is typically, and even in this particular case, a object. We may only want to operate on a few keys of our state object.
So, we have two options, either manually copy all keys or use ES6 spread operator to spread the old state object and then overwrite the keys we want to change with the new value. This will preserve the keys we are not changing.
If you don't take either path, your state will become the object with only the keys you update and hence you may face unexpected behaviour
CodePudding user response:
You should just about always shallow copy the previous state when updating a React state. Think of the useReducer
as a very specialized version of the useState
hook. Instead of returning the state and an updater function, i.e. [state, setState]
, it returns an array of the state and a dispatch function, i.e. [state, dispatch]
. Please recall that state updates in React function components do not merge updates, so failing to copy any of the previous state will result in it not being included in the next state value, as if it were removed.
Note
Unlike the
setState
method found in class components,useState
does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:const [state, setState] = useState({}); setState(prevState => { // Object.assign would also work return {...prevState, ...updatedValues}; });
Another option is
useReducer
, which is more suited for managing state objects that contain multiple sub-values.
From this basic understanding of state updates in function components, or if you are already familiar with "legacy" Redux reducer functions, it is trivial to see and understand why the previous state is copied forward.
Note also the default case typically doesn't have any effect on the state, it just returns the current state value.
The single exception I can think of is when you wanting to set the state to a completely new value like resetting back to initial state.
Example:
const formReducer = (state, action) => {
switch (action.type) {
case "USERNAME_INPUT":
return {
...state,
usernameValue: action.payload,
};
case "API_RETURN_USERNAME":
return {
...state,
usernameValue: action.payload.match
? action.payload.username
: "",
usernameIsValid: !action.payload.match,
emailValue: action.payload.match ? action.payload.email : "",
emailIsValid: !action.payload.email,
apiReturned: true,
};
case "EMAIL_INPUT":
return {
...state,
emailValue: action.payload.value,
emailIsValid: action.payload.isValid,
formIsValid: action.payload.isValid && state.usernameIsValid,
apiReturned: false,
};
case "RESET":
return action.payload;
default:
return state;
}
};
...
const initialState = {
usernameValue: "",
emailValue: "",
usernameIsValid: false,
emailIsValid: false,
formIsValid: false,
apiReturned: false,
}
...
const [state, dispatch] = useReducer(reducer, initialState);
...
const resetState = () => {
dispatch("RESET", { payload: initialState });
};