React useState - How should I use the setter function? - reactjs

Basic Situation
Let's say I have a basic useState of a number:
const [valueOne, setValueOne] = useState(0);
I can write an increase function in two ways:
First way:
// for one value
const increaseOneFirstWay = useCallback(() => {
setValueOne((prev) => prev + 1);
}, []); // doesnt have dependency
Since the setter function of a useState doesn't change (source), I don't have to add any dependencies to my callback function.
Second way
const increaseOneSecondWay = useCallback(() => {
setValueOne(valueOne + 1);
}, [valueOne]); // has one dependency
Here, since I am using valueOne, I have to add a dependency, so the callback updates accordingly.
For a basic callback like this, using both ways seems fine. But what if it gets more complicated?
Complicated Situation
Now, instead of having one state, we will have three:
const [valueTwo, setValueTwo] = useState(0);
const [valueThree, setValueThree] = useState(0);
const [valueFour, setValueFour] = useState(0);
This time, the callback will need to use all three values. And some of them together.
First way:
// for several values:
const increaseSeveralFirstWay = useCallback(() => {
setValueTwo((valueTwoPrev) => {
setValueThree((valueThreePrev) => {
setValueFour((valueFourPrev) => {
return valueFourPrev + valueThreePrev + valueTwoPrev + 1;
});
return valueThreePrev + valueTwoPrev + 1;
});
return valueTwoPrev + 1;
});
}, []); // doesnt have dependency
Second way:
const increaseSeveralSecondWay = useCallback(() => {
setValueTwo(valueTwo + 1);
setValueThree(valueThree + valueTwo + 1);
setValueFour(valueFour + valueThree + valueTwo + 1);
}, [valueTwo, valueThree, valueFour]); // has several dependency
Let's say that valueTwo, valueThree, and valueFour also change independently, wouldn't the first way be a better choice? Or is there a reason why someone would use the second way (Not opinion-based, but maybe performance? maybe it's not recommended at all to use the first way?)
Codesandbox

In the case you have multiple states depending on each other the solution is often to use a reducer. However sometimes the use of a reducer is not necessary since the state can be simplified.
I will here demonstrate the 2 solutions with 2 examples:
Solution 1: Using a reducer
useReducer is usually preferable to useState when you have complex
state logic that involves multiple sub-values or when the next state
depends on the previous one. -- React Docs
import { useReducer } from 'react';
const initialNumberState = {
valueOne: 0,
valueTwo: 0,
valueThree: 0,
};
const numberReducer = (prevState, action) => {
if (action.type === 'INCREASE_NUMBER') {
const { valueOne, valueTwo, valueThree } = prevState;
const newValueOne = valueOne + 1;
const newValueTwo = valueOne + valueTwo + 1;
const newValueThree = valueOne + valueTwo + valueThree + 1;
return {
valueOne: newValueOne,
valueTwo: newValueTwo,
valueThree: newValueThree,
};
}
return prevState;
};
const CustomComponent = (props) => {
const [numberState, dispatch] = useReducer(
numberReducer,
initialNumberState
);
const { valueOne, valueTwo, valueThree } = numberState;
const handleClick = () => {
dispatch({ type: 'INCREASE_NUMBER', value: 'not_used_in_this_case' });
};
return (
<div>
<ul>
<li>Number 1: {valueOne}</li>
<li>Number 2: {valueTwo}</li>
<li>Number 3: {valueThree}</li>
</ul>
<button onClick={handleClick}>Click Me!</button>
</div>
);
};
export default CustomComponent;
Solution 2: Simplifying the state
This is the case when we can derive all the data we need from independent states.
For example imagine we are validating a form with separate states:
const [isEmailValid, setIsEmailValid] = useState(false);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const [isFormValid, setIsFormValid] = useState(false);
Here setting the state for the email and password validation is easy. However we start encountering issues when we want to set the state for the form.
handlePasswordChange = (event) =>{
passwordValue = event.currentTarget.value;
const isValid = validatePassword(passwordValue);
setIsPasswordValid(isValid);
const formValid = isPasswordValid && isEmailValid;
setIsFormValid(formValid);
/* Here we will encounter issues since we are updating
the form validity on a stale password validity value; */
}
Here the solution could have been : const formValid = isValid && isEmailValid;
But the optimal solution is simplifying the state:
const [isEmailValid, setIsEmailValid] = useState(false);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const isFormValid = isEmailValid && isPasswordValid;
This is a simplistic example and you might think this never happens. But we often over complicate things.

Related

React getting previous version of state when calling a function in useContext

I am using a context like the following:
const placeCurrentOrder = async () => {
alert(`placing order for ${mealQuantity} and ${drinkQuantity}`)
}
<OrderContext.Provider
value={{
placeCurrentOrder,
setMealQuantity,
setDrinkQuantity,
}}
>
and I'm calling this context deep down with something like this (when the user clicks a button):
const x = () => {
orderContext.setMealQuantity(newMealQuantity)
orderContext.setDrinkQuantity(newDrinkQuantity)
await orderContext.placeCurrentOrder()
}
Sort of like I expect, the state doesn't update in time, and I always get the previous value of the state. I don't want to have a useEffect, because I want control over exactly when I call it (for example, if mealQuantity and drinkQuantity both get new values here, I don't want it being called twice. The real function is far more complex.)
What is the best way to resolve this? I run into issues like this all the time but I haven't really gotten a satisfactory answer yet.
You can set them in a ref. Then use the current value when you want to use it. The easiest way is probably to just create a custom hook something like:
const useStateWithRef = (initialValue) => {
const ref = useRef(initialValue)
const [state, setState] = useState(initialValue)
const updateState = (newState) => {
ref.current = typeof newState === 'function' ? newState(state) : newState
setState(ref.current)
}
return [state, updateState, ref]
}
then in your context provider component you can use it like:
const [mealQuantity, setMealQuantity, mealQuantityRef] = useStateWithRef(0)
const [drinkQuantity, setDrinkQuantity, drinkQuantityRef] = useStateWithRef(0)
const placeOrder = () => {
console.log(mealQuantityRef.current, drinkQuantityRef.current)
}
You can also just add a ref specifically for the order and then just update it with a useEffect hook when a value changes.
const [drinkQuantity, setDrinkQuantity] = useState(0)
const [mealQuantity, setMealQuantity] = useState(0)
const orderRef = useRef({
drinkQuantity,
mealQuantity
})
useEffect(() => {
orderRef.current = {
drinkQuantity,
mealQuantity,
}
}, [drinkQuantity, mealQuantity])
const placeOrder = () => {
console.log(orderRef.current)
}

Update React State dependent from another state with hooks

I want to update a state dependent on another state change. For example I have state one and if state one changes I want to change state two dependent on the changes of state one. What is the best way of doing this? I have prepared two examples.
Example 1
Here I change State2 inside the state change callback of state 1.
const DependentStateExample1 = () => {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const changeState1 = () => {
setState1((prev) => {
const newState = prev + 5;
setState2((prevState2) => prevState2 + newState);
return newState;
});
};
return (
<div>
<span>{state1}</span><span>{state2}</span>
<button onClick={changeState1}>Change State 1</button>
</div>
);
};
Example 2
Here I use useEffect, which will execute everytime when state1 changes.
const DependentStateExample2 = () => {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const changeState1 = () => {
setState1((prev) => prev + 5);
};
useEffect(() => setState2((prev) => prev + state1), [state1]);
return (
<div>
<span>{state1}</span>
<span>{state2}</span>
<button onClick={changeState1}>Change State 1</button>
</div>
);
};
Both ways give me the same result, but which one is better, or is there a better way solving this? Can someone please explain?
Let's start from the fact that YOU know the relation between the first state and the second, so instead of writing this logical dependency in code, you could take it out and just write the code so that it produces the same result, but so that React doesn't have to 'wait' until setState has completed:
setState1(oldState1 => oldState1 + 5);
setState2(oldState2 => oldState1 + oldState2 + 5); // we know setState is async so remember on the second line you don't have yet setState1 completed
The principle could be extended for more complex dependencies between the states.

React Effect dependecy with two useState values and Effect Refiring

Following is my code
const [accountUsers, setAccountUsers] = useState([]);
const [listLoading, setListLoading] = useState(true);
const [totalAccountUsers, setTotalAccountUsers] = useState(0);
const getAccountUserList = useCallback(
async (page = 0, pageSize = 10) => {
console.log('Kitne users hain??==', accountUsers.length);
if (
// TODO: Move this logic to action for reusability
accountUsers.length < (page + 1) * pageSize &&
(accountUsers.length < totalAccountUsers || accountUsers.length === 0)
) {
console.log('inside if condition');
console.log(' Total Account users=', totalAccountUsers);
console.log(
'first condiont=',
' accountUsers.length < (page + 1) * pageSize ',
accountUsers.length < (page + 1) * pageSize,
' AND'
);
console.log(
'second one condition',
' accountUsers.length < totalAccountUsers =',
accountUsers.length < totalAccountUsers,
' OR ',
'second two condition',
' accountUsers.length === 0',
accountUsers.length === 0
);
setListLoading(true);
const data = await getAccountUsers(id, page + 1, pageSize);
setAccountUsers((users) => users.concat(data.data.data.results));
setTotalAccountUsers(data.data.data.total);
setListLoading(false);
}
},
[accountUsers, totalAccountUsers, id]
);
const refetchUsers = useCallback(() => {
setAccountUsers([]);
setTotalAccountUsers(0);
}, []);
useEffect(() => {
getAccountUserList();
}, [getAccountUserList]);
return (
<>
<Button onClick={() => refetchUsers()}>Refetch</Button>
<Button onClick={async ()=> await delete(id); refetchUsers()> Delete </Button>
</>
)
My refetch function works fine when called individually. But when an async function is involved the callback is called twice. I feel in the refetch functions two setStates are causing it to call the callback twices with the help of effect.
Why does that happen and what's the work around?
IF refetch is called before any async operation it works perfectly fine though. Is it some issue with react batching the state updates before firing effects.
Your code is effectively as below by condensing your dependency tree (and ignoring some unrelated stuff):
const Home = () => {
const [accountUsers, setAccountUsers] = useState<number[]>([]);
const [totalAccountUsers, setTotalAccountUsers] = useState(0);
React.useEffect(() => {
const getAccountUserList = async () => {
const data = await Promise.resolve({results: [1, 2, 3]});
setAccountUsers((users) => users.concat(data.results));
setTotalAccountUsers(data.results.length);
};
getAccountUserList();
},
[accountUsers, totalAccountUsers]);
const refetchUsers = () => {
setAccountUsers([]);
setTotalAccountUsers(0);
};
return (
<>
<button onClick={() => refetchUsers()}>Refetch</button>
</>
);
};
We can easily spot the issue here - infinite rerendering caused by the useEffect hook. You are seeing the api is called twice likely because (and luckily) it has finished reading all data and the if condition in your code stops further state updating.
Some personal suggestions:
Avoid useCallback from the beginning. It's an optimization, and we should start with correct core functionality not optimization.
For user events, try to avoid triggering side effects using useEffect - it'll likely require additional state update and leads to unnecessary rerendering. Do that in the event handler directly.
Use useEffect without dependency (one time) for initial data loading.
Try to avoid dependency "hierarchy" - For example in your original case useEffect depends on getAccountUserList which depends on other state objects. This kind of hierarchy makes it harder to read the code and can lead to bugs hard to spot. Instead try to structure your code so that they depend on state directly.
The below code (again, simplified) should work for your scenario:
const Home = () => {
const [accountUsers, setAccountUsers] = useState<number[]>([]);
const [totalAccountUsers, setTotalAccountUsers] = useState(0);
const getAccountUserList = async () => {
const data = await Promise.resolve({results: [1, 2, 3]});
setAccountUsers((users) => users.concat(data.results));
setTotalAccountUsers(data.results.length);
};
React.useEffect(() => {
getAccountUserList();
}, []);
const refetchUsers = () => {
setAccountUsers([]);
setTotalAccountUsers(0);
getAccountUserList();
};
return (
<>
<button onClick={() => refetchUsers()}>Refetch</button>
</>
);
};

Second call to setState prevents first call to setState from executing

I am making a rock, paper, scissors game. In the code below, I have a context file that is used to store the global state. I also am showing my choices component. When a user clicks on a button in the choices component, the setChoices method is called which should set the user choice and cpu choice variables in the global state. Then, the cpuScore() method is ran afterwards to increment the cpu score (just to illustrate the problem). The cpu score updates as expected, but the choice variables are not updated. If I do not run the cpuScore method, the choice variables update as expected, but obviously not the score.
//context file
import React, { createContext, useState } from 'react';
const GameContext = createContext();
const GameContextProvider = props => {
const [gameItems, setGameItems] = useState(
{userChoice: null, userScore: 0, cpuChoice: null, cpuScore: 0}
);
const setChoices = (userChoice, cpuChoice) => {
setGameItems({...gameItems, userChoice: userChoice, cpuChoice: cpuChoice})
}
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
return (
<GameContext.Provider value={{gameItems, setChoices, cpuScore}}>
{ props.children }
</GameContext.Provider>
)
}
export default GameContextProvider;
//choices component
import React, { useContext } from 'react';
import { GameContext } from '../contexts/GameContext';
const Choices = (props) => {
const { setChoices, cpuScore } = useContext(GameContext);
const getCpuChoice = () => {
const cpuChoices = ['r', 'p', 's'];
const randomIndex = Math.floor((Math.random() * 3));
const cpuDecision = cpuChoices[randomIndex];
return cpuDecision
}
const playGame = (e) => {
const userChoice = e.target.id;
const cpuChoice = getCpuChoice();
setChoices(userChoice, cpuChoice);
cpuScore();
}
return (
<div>
<h1>Make Your Selection</h1>
<div className="choices">
<i className="choice fas fa-hand-paper fa-10x" id="p" onClick={playGame}></i>
<i className="choice fas fa-hand-rock fa-10x" id="r" onClick={playGame}></i>
<i className="choice fas fa-hand-scissors fa-10x" id='s' onClick={playGame}></i>
</div>
</div>
)
What do I need to change to set the state for both choice and score?
Since calls to setState are asynchronous, your two calls to setState are interfering with each other, the later one is overwriting the earlier one.
You have a few options.
Separate your state so that the values don't affect each other:
const [choices, setChoices] = useState({ user: null, cpu: null });
const [scores, setScores] = useState({ user: 0, cpu: 0);
Or go even further and set each of the two choices and two scores as their own state value.
Keep all state in one object, but update it all at once:
const setChoices = (userChoice, cpuChoice) => {
const cpuScore = gameItems.cpuScore + 1;
setGameItems({
...gameItems,
userChoice,
cpuChoice,
cpuScore
});
}
Use useReducer:
const initialState = {
userChoice: null,
userScore: 0,
cpuChoice: null,
cpuScore: 0
}
const [gameItems, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "UPDATE_CHOICES":
return {
...state,
userChoice: action.userChoice,
cpuChoice: action.cpuChoice
};
case 'UPDATE_CPU_SCORE':
return {
...state,
cpuScore: state.cpuScore + 1
}
default:
return state;
}
}, initialState);
const setChoices = (userChoice, cpuChoice) => {
dispatch({ type: 'UPDATE_CHOICES', userChoice, cpuChoice });
};
const cpuScore = () => {
dispatch({ type: 'UPDATE_CPU_SCORE'})
};
Basically, React doesn't immediately updates after you call setState.
If you call cpuScore() right after setChoices(), the cpuScore function you are calling is still the function from the current render, not after setChoices() update. Because of that, cpuScore() will set the state again, using the spread value of gameItems (which is still hasn't changed because the update from setChoices hasn't kicked in) which cause the changes by setChoices() to be overridden.
If React always immediately updates after every setState call, the performance would be atrocious. What React does is it will batch multiple setState calls into one update, so it doesn't update the DOM repeatedly.
const cpuScore = () => {
setGameItems({...gameItems, cpuScore: gameItems.cpuScore + 1})
}
My suggestion would be either to separate this two states, so they don't get overwritten by each other, or create a new function that handle all this updates in one place.

React hook with constant input parameter - hook creator?

Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.
Following is a simplified example:
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = constantIdArray.map((id) => useCallback(() => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
}));
return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput(idArray);
return (
<div>
<div onClick={callbacks[0]}>
{state[0]}
</div>
<div onClick={callbacks[1]}>
{state[1]}
</div>
<div onClick={callbacks[2]}>
{state[2]}
</div>
</div>
)
}
Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:
const createUseHookWithConstantInput = (constantIdArray) => () => {
...
}
const idArray = ['id-1', 'id-2', 'id-3'];
const useHookWithConstantInput = createUseHookWithConstantInput(idArray)
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput();
return (
...
)
}
How do you solve situations like this?
One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:
const useHookWithConstantInput = (constantIdArray) => {
const [state, changeState] = useState({});
const [callbacks, setCallbacks] = useState([]);
useEffect(() => {
changeState(buildInitialState(constantIdArray));
const callbacksArray = constantIdArray.map((id) => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
});
setCallbacks(callbacksArray);
}, []);
return { state, callbacks };
}
Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.
If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.
Another solution to go for would be with useMemo. This is what I ended up implementing.
const createCallback = (id, changeState) => () => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
};
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = useMemo(() =>
constantIdArray.map((id) => createCallback(id, changeState)),
[],
);
return { state, callbacks };
};

Resources