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

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.

Related

Invalid custom hook call

I'm just a react beginner. I'm trying to create a custom hook, which will be triggered once an onClick event is triggered. By what I see, I need to use the useRef hook, to take into account if the component is rendered by first time, or if it's being re-rendered.
My code approach is the next:
const Clear = (value) => {
const useClearHook = () => {
const stateRef = useRef(value.value.state);
console.log(stateRef);
useEffect(() => {
console.log("useEffect: ");
stateRef.current = value.value.state;
stateRef.current.result = [""];
stateRef.current.secondNumber = [""];
stateRef.current.mathOp = "";
console.log(stateRef.current);
value.value.setState({
...stateRef.current,
result: value.value.state.result,
secondNumber: value.value.state.secondNumber,
mathOp: value.value.state.mathOp,
});
}, [stateRef.current]);
console.log(value.value.state);
};
return <button onClick={useClearHook}>Clear</button>;
};
Any suggestion? Maybe I might not call ...stateRef.current in setState. I'm not sure about my mistake.
Any help will be appreciated.
Thanks!
Your problem is useClearHook is not a component (the component always goes with the first capitalized letter like UseClearHook), so that's why when you call useRef in a non-component, it will throw that error. Similarly, for useEffect, you need to put it under a proper component.
The way you're using state is also not correct, you need to call useState instead
Here is a possible fix for you
const Clear = (value) => {
const [clearState, setClearState] = useState()
const useClearHook = () => {
setClearState((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
If your states on the upper component (outside of Clear). You can try this way too
const Clear = ({value, setValue}) => {
const useClearHook = () => {
setValue((prevState) => ({
...prevState,
result: [""],
secondNumber: [""],
mathOp: "",
}));
};
return <button onClick={useClearHook}>Clear</button>;
};
Here is how we pass it
<Clear value={value} setValue={setValue} />
The declaration for setValue and value can be like this in the upper component
const [value, setValue] = useState()

Passing selectors and action creators as props to a generic component

Let’s say I have a custom Input component (similar to an HTML <input/>) which can be used to represent any value in the Redux store.
The API I’m currently using is that Input takes two props value and onChange and is then used as follows:
const ParentComponent = () => {
const dispatch = useDispatch();
const valueA = useSelector(selectValueA);
const valueB = useSelector(selectValueB);
const valueC = useSelector(selectValueC);
const handleChangeA = newValue => dispatch(valueAChanged(newValue));
const handleChangeB = newValue => dispatch(valueBChanged(newValue));
const handleChangeC = newValue => dispatch(valueCChanged(newValue));
return (
<div>
<Input value={valueA} onChange={handleChangeA}/>
<Input value={valueB} onChange={handleChangeB}/>
<Input value={valueB} onChange={handleChangeC}/>
</div>
}
It works fine but the issue is that whenever valueA changes, then the whole ParentComponent rerenders instead of just the corresponding Input, which causes performance issues and goes against the best practice "have components select only the data they need from the store".
A solution would be to make wrapper components:
const ParentComponent = () => {
return (
<div>
<InputA/>
<InputB/>
<InputC/>
</div>
);
};
const InputA = () => {
const dispatch = useDispatch();
const value = useSelector(selectValueA)
const handleChange = newValue => dispatch(valueAChanged(newValue));
return <Input value={value} onChange={handleChange}/>;
};
const InputB = () => {
const dispatch = useDispatch();
const value = useSelector(selectValueB)
const handleChange = newValue => dispatch(valueBChanged(newValue));
return <Input value={value} onChange={handleChange}/>;
};
const InputC = () => {
const dispatch = useDispatch();
const value = useSelector(selectValueC)
const handleChange = newValue => dispatch(valueCChanged(newValue));
return <Input value={value} onChange={handleChange}/>;
};
…
It would fix the problem, but as you can see it is a whole lot of boilerplate that I’d rather avoid.
So I was wondering if I could instead directly pass the selector itself and the action creator as props:
const ParentComponent = () => {
return (
<div>
<ConnectedInput selector={selectValueA} actionCreator={valueAChanged}/>
<ConnectedInput selector={selectValueB} actionCreator={valueBChanged}/>
<ConnectedInput selector={selectValueC} actionCreator={valueCChanged}/>
</div>
);
};
const ConnectedInput = ({selector, actionCreator}) => {
const dispatch = useDispatch();
const value = useSelector(selector);
const handleChange = newValue => dispatch(actionCreator(newValue));
return <Input value={value} onChange={handleChange}/>;
};
I think it looks very neat and I can’t see anything wrong with it (although I haven’t tried it yet), but it also seems like a pretty unusual pattern, at least I couldn’t find any similar code.
So is there any problem with it that I’m not seeing? Or is there any other solution to connect generic components to the store?

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

Run Effect hook only when both dependencies change

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]);

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