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'}});
};
Related
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]);
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>}</>;
};
here is the context
I am invoking a function invoked(..) from another component. Inside invoked I setState but the state does not get set. It does not trigger the useEffect nor does it take the updated value in the useInterval.
Below is the code.
Component 1 = exampleComp.ts
const exampleComp = ({initValue: ExampleProps}) {
const [params, setParams] = useState<{field1: null | string}> ({field1: null});
const invoked = (x: string) => {
console.log("inside invoked"); // this gets printed
setParams({"field1": x});
};
useEffect(() => {
console.log(params); // does not come here :(
}, [params]);
const fetchData = {...};
useInterval(
() => {
if (!params.field1 || fetching) return;
console.log("in interval", params); // does not come here too :(
fetchData(params.field1);
},
params && !fetching ? 5000: null,
);
return {invoked, ...}
}
Component 2:
const newComp = ({initValue: ExampleProps}) {
const {invoked, ..} = useExampleComp({...}); //Example comp is the above component.
useEffect(() => {
invoked(x);
}, []);
}
Any help will be appreciated! thanks.
Turns out that the component was unmounting too early. This happens when the scope of the component is too narrow.
If you have a similar problem, try logging the component to see if this is indeed happening.
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 };
};