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

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)
}

Related

React.JS set new state and use it in same function

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.

React detect which props have changed on useEffect trigger

I want to detect which of the argument props have changed inside my use effect. How can I achieve this? I need something like this:
const myComponent = () => {
...
useEffect(() => {
if (prop1 === isTheOneThatChangedAndCusedTheTrigger){
doSomething(prop1);
}else{
doSomething(prop2);
}
},[prop1, prop2]);
};
export function myComponent;
While you can use something like usePrevious to retain a reference to the last value a particular state contained, and then compare it to the current value in state inside the effect hook...
const usePrevious = (state) => {
const ref = useRef();
useEffect(() => {
ref.current = state;
}, [value]);
return ref.current;
}
const myComponent = () => {
const [prop1, setProp1] = useState();
const prevProp1 = usePrevious(prop1);
const [prop2, setProp2] = useState();
const prevProp2 = usePrevious(prop2);
useEffect(() => {
if (prop1 !== prevProp1){
doSomething(prop1);
}
// Both may have changed at the same time - so you might not want `else`
if (prop2 !== prevProp2) {
doSomething(prop2);
}
},[prop1, prop2]);
};
It's somewhat ugly. Consider if there's a better way to structure your code so that this isn't needed - such as by using separate effects.
useEffect(() => {
doSomething(prop1);
}, [prop1]);
useEffect(() => {
doSomething(prop2);
}, [prop2]);

Context not in sync

I have a component that is using Context like so:
export const MyContext = React.createContext({});
export const MyComponent = React.memo(({children}) => {
const [myVar, setMyVar] = React.useState({});
const myFunction = () => {
console.log(myVar);
setMyVar({...myVar, {extraData: 'hi there'}});
};
const updateMyVar = React.useCallback((data) => {
setMyVar(data);
}, []);
const doSomethingElse = React.useCallback(() => {
myFunction();
}, []);
return (
<MyContext.Provider value={{myVar, updateMyVar, doSomethingElse}}>{children}</MyContext.Provider>
);
});
And then using it in a component:
const {myVar, updateMyVar, doSomethingElse} = React.useContext(FormContext);
The child component can seem to update myVar just fine, but inside of the MyComponent component, when I try to read myVar in something like the myFunction function just returns whatever the state was initially initialized with. It never updates to show the data that is there currently. The funny thing is that the child component always reads the correct data.
Since there is no code sandbox linked to the question, I can only take a guess. I think your myFunction will be stale if it is called within doSomethingElse as the dependencies are stale.
Can you try this ?
const doSomethingElse = React.useCallback(() => {
myFunction();
}, [myFunction]);
If you don't want to do that, another way would be to do this. Here you are accessing the current value of the state using the callback variant of state setter function. Let me know if this helps.
const myFunction = () => {
console.log(myVar);
setMyVar(currentMyVar => {... currentMyVar, {extraData: 'hi there'}});
};

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 };
};

How to use static variables with react hooks

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

Resources