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.
Related
I was trying to setState and use the new value inside of same function.
Functions:
const { setTasks, projects, setProjects } = useContext(TasksContext)
const [input, setInput] = useState('')
const handleNewProject = () => {
setProjects((prevState) => [...prevState, {[input]: {} }])
tasksSetter(input)
setInput('')
}
const tasksSetter = (findKey) => {
const obj = projects?.find(project => Object.keys(project)[0] === findKey)
const taskArray = Object.values(obj)
setTasks(taskArray)
}
Input:
<input type='text' value={input} onChange={(e) => setInput(e.target.value)}></input>
<button onClick={() => handleNewProject()}><PlusCircleIcon/></button>
I understand, that when we get to tasksSetter's execution my projects state hasn't been updated yet. The only way to overcome it, that I can think of, is to use useEffect:
useEffect(() => {
tasksSetter(input)
}, [projects])
But I can't really do that, because I would like to use tasksSetter in other places, as onClick function and pass another values as argument as well. Can I keep my code DRY and don't write the same code over again? If so, how?
Here's something you can do when you can't rely on the state after an update.
const handleNewProject = () => {
const newProjects = [...projects, {[input]: {} }];
setProjects(newProjects);
tasksSetter(input, newProjects);
setInput('')
}
const tasksSetter = (findKey, projects) => {
const obj = projects?.find(project => Object.keys(project)[0] === findKey)
const taskArray = Object.values(obj)
setTasks(taskArray)
}
We make a new array with the update we want, then we can use it to set it to the state and also pass it to your tasksSetter function in order to use it. You do not use the state itself but you do get the updated array out of it and the state will be updated at some point.
Given a small hook defined as
const useCount = ({ trigger }) => {
const [count, setCount] = useState(1)
const increment = () => setCount(count => count + 1);
return {
data: count,
increment,
trigger,
}
}
and being consumed as
function App() {
let data;
const log = useCallback(() => {
console.log(data);
}, [data]);
const hooked = useCount({
trigger: log
});
({ data } = hooked);
const { trigger, increment } = hooked;
return (
<div className="App">
<div>{data}</div>
<button onClick={increment}>Increment</button>
<button onClick={trigger}>Log</button>
</div>
);
}
If we click on increment, the data is updated. If we click on Log, the memoized value of 1 is logged.
Q1. Is it an anti-pattern to consume data returned by the hook in the memoized callback that is itself passed to the hook?
Q2. Why does it log 1 rather than undefined. If the log fn has picked up 1, why doesn't it pick up subsequent updates?
Q3. Removing the memoization for log fn would make it work. Is there any other apporach to work around this that doesn't involve removing memoization?
A small reproduction is available in this codesandbox.
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.
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 };
};
Just tried some react-hooks and got some questions.
Consider this functional component with react-hooks:
const Counter = (props) => {
console.log("Counter component");
const [count, setCount] = useState(0);
const handleIncrease = () => {
setCount(count + 1);
}
const handleDecrease = () => {
setCount(count - 1);
}
return (
<div>
<button onClick={handleIncrease}>+</button>
<button onClick={handleDecrease}>-</button>
<p>{count}</p>
</div>
)
}
It logged everytime I clicked the "+" or "-".
Does it means that every handlers inside this component(or say, function) are redeclared and reassigned to a variable? If it did, won't it cause some performance issues?
To me the functional component with hooks seems like a huge render method of a classical component like this:
class Counter extends React.Component {
state = {
count: 0,
}
render() {
const handleIncrease = () => {
this.setState({ count: this.state.count + 1 });
}
const handleDecrease = () => {
this.setState({ count: this.state.count - 1 });
}
return (
<div>
<button onClick={handleIncrease}>+</button>
<button onClick={handleDecrease}>-</button>
<p>{count}</p>
</div>
)
}
}
which I think nobody will do this.
Did I have misunderstandings of the React's render mechanism or it's just not the best practice when using the functional component with react-hooks?
Although in functional components functions are recreated on every render, the performance cost of it much less compared to the benefits.
You can refer this post for more details: Performance penalty of creating handlers on every render
However you can still optimise so that functions are not recreated on every render using useCallback or useReducer(depending on whether your updates are complex or not)
const Counter = (props) => {
console.log("Counter component");
const [count, setCount] = useState(0);
const handleIncrease = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, [])
const handleDecrease = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, [])
return (
<div>
<button onClick={handleIncrease}>+</button>
<button onClick={handleDecrease}>-</button>
<p>{count}</p>
</div>
)
}
In the above example the functions are re-created only on the initial render and using the state update callback we can update the state and avoid closure issues