I have a React component that fetches data using the useEffect hook like so:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
useEffect(() => {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}, [key, options])
return <p>{data}</p>;
}
This runs the hook every time key or options change. However, I'm also caching the data locally, and only want the effect to run when both key AND options change (since for each key/options combination the data will always be the same).
Is there a clean way to depend on the combination of key AND options rather than key OR options using React Hooks?
You can create this sort of logic with useRef(). Consider the following example and sandbox: https://codesandbox.io/s/react-hooks-useeffect-with-multiple-reqs-6ece5
const App = () => {
const [name, setName] = useState();
const [age, setAge] = useState();
const previousValues = useRef({ name, age });
useEffect(() => {
if (
previousValues.current.name !== name &&
previousValues.current.age !== age
) {
//your logic here
console.log(name + " " + age);
console.log(previousValues.current);
//then update the previousValues to be the current values
previousValues.current = { name, age };
}
});
return (
<div>
<input
placeholder="name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
placeholder="age"
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
);
};
Workflow:
We create a ref object for the two values we want to keep track of,
in this case its a name and age. The ref object is previousValues.
useEffect is defined but we do not provide it any dependencies.
Instead, we just have it execute whenever there is a state-change to
name or age.
Now inside useEffect we have conditional logic to check whether the
previous/initial values of both name and age are different than
their corresponding state-values. If they are then good we execute
our logic (console.log).
Lastly after executing the logic, update the ref object (previousValues) to the current values (state).
In order to run the effect when both values change, you need to make use of the previous values and compare them within the hook when either key or options change.
You can write a usePrevious hook and compare old and previous state as mentioned in this post:
How to compare oldValues and newValues on React Hooks useEffect?
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const previous = usePrevious({key, options});
useEffect(() => {
if(previous.key !== key && previous.options !== options) {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}
}, [key, options])
return <p>{data}</p>;
}
All provided solutions are perfectly fine, However there are some more complex situation e.g., When useEffect function should be called ONLY when dependency A and B changed while it also depends on C's value.
So I suggest using sequence of useEffects and intermediate States to provide more space for future logics. Implementation of this approach for asked question would be:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const [needsUpdate, setNeedsUpdate] = useState(()=>({key:false, option:false}));
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, key:true}));
},[key])
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, options:true}));
},[options])
useEffect(() => {
if (needsUpdate.key && needsUpdate.options){
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
setNeedsUpdate(()=>({key:false, option:false}));
}
}, [needsUpdate, key, options])
return <p>{data}</p>;
}
In this way we can apply almost any logic on our useEffect dependencies, However it has own drawbacks which is few more rendering cycle.
You can create a new custom hook which calls the callback with an argument with index/names of dependencies
const useChangesEffect = (callback, dependencies, dependencyNames = null) => {
const prevValues = useRef(dependencies);
useEffect(() => {
const changes = [];
for (let i = 0; i < prevValues.current.length; i++) {
if (!shallowEqual(prevValues.current[i], dependencies[i])) {
changes.push(dependencyNames ? dependencyNames[i] : i);
}
}
callback(changes);
prevValues.current = dependencies;
}, dependencies);
};
useChangesEffect((changes) => {
if (changes.includes(0)) {
console.log('dep1 changed');
}
if (changes.includes(1)) {
console.log('dep2 changed');
}
}, [dep1, dep2]);
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.
I have to react variable, let's call it temp, it's not a state variable but a normal let variable.
the problem is that I am not able to get the updated value in the render.
I know it's not a state variable so it won't rerender the UI, whenever the variable is updated.
but, I am not accessing the variable until the variable is set.
ex: ->
const MyComponent = (props) => {
let temp;
const [loading, setLoading] = useState(true);
init = () => {
setLoading(true);
temp = "updated value";
setLoading(false);
}
useEffect(() => {
init();
}, [])
return (
{laoding ? <div>loading</div> : <div> {temp}</div>}
)
}
in the above code, if the loading is false and the temp variable is set, then the UI should reflect the updated value.
but, it's not,
it works fine in-class components.
am I missing something here?
You can utilize the effect for the functional components. useEffect gets triggered every time its dependency changes which will trigger rerender for you.
However, I can't see the real use of the init variable. So assuming you want to set value during component mount. I think it'll look like the below.
const MyComponent = (props) => {
let temp;
const [loading, setLoading] = useState(true);
useEffect(() => {
// Empty array means that this will be executed when component mounts
init()
}, [])
useEffect(() => {
// Rerender will be triggered whenever temp gets changed
}, [temp])
const init = () => {
setLoading(true);
temp = "updated value";
setLoading(false);
}
return (
<>
{
loading ? <span>loading...</span> : <span>{temp}</span>
}
</>
)
}
NOTE: I'd still go with storing temp as a state in this case because it's the part of react's system and that's how it should be used.
the execution here is: component renders => mounts => use effect runs => state is changed => rerenders => temp is newly created as undefined let. use useRef for temp and assign temp.current = ... in init. it guarantees to persist through renders.
const MyComponent = (props) => {
const temp = useRef();
const [loading, setLoading] = useState(true);
const init = () => {
setLoading(true);
temp.current = "updated value";
setLoading(false);
};
useEffect(() => {
init();
}, []);
return <>{loading ? <div>loading</div> : <div>{temp.current}</div>}</>;
};
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)
}
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 };
};
With const [open, setOpen] = useState(false) I can create a variable open which is persisted over calls of a functional component.
But which hook can I use if I do not want a rerender when setting a variable?
I have a custom hook draft:
const useVariable = (initialValue) => {
const ref = useRef();
return useMemo(() => {
ref.current = [initialValue, (newValue) => { ref.current[0] = newValue }]
}, [])
}
But according to https://reactjs.org/docs/hooks-reference.html#usememo I can not rely that useMemo is not called anytime again.
You can make use of useRef hook if you just want to store the some data in a variable and not re-render when the variable is set
const unsubCallback = useRef(null);
if(!unsubCallback) {
unsubCallback.current = subscribe(userId)
}
Thank to #shubham-khatri I found a solution to my question. Just use the initialValue of the useRef hook:
const useVariable = initialValue => {
const ref = useRef([
initialValue,
param => {
ref.current[0] = typeof param === "function"
? param(ref.current[0])
: param
}
]);
return ref.current;
};
https://codesandbox.io/s/v3zlk1m90
Edit: To account for Christopher Camp's comment I added that also a function can be passed like in useState. See usage in codesandbox