I am trying to have a general function which run setState twice, however, the first setState will not update. Are there any way to get around it? or how to fix this issue?
Parent
const [data, setData] = useState({});
const updateData = (key, value) => {
console.log(key, value);
setData({ ...data, [key]: value });
};
...
<div>
Num 1: {data.num1}, Num2: {data.num2}
</div>
<Child updateData={updateData} />
Child
const { updateData } = props;
const onClick = () => {
updateData("num1", 1);
updateData("num2", 2);
};
return <button onClick={onClick}> Click here </button>
console.log return both values being called, but only 1 value being updated
codesandbox example here
(After some testing, even calliing both in the same parent function, if calling setData twice, it still wont work (see Simplify.js)
While you could use a callback so that the argument contains the currently updated state, including prior state setters that've run but before a re-rendering has occurred:
const updateData = (key, value) => {
setData(data => ({ ...data, [key]: value }));
};
If you have a limited number of possible properties in the data variable, consider using separate states instead:
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const onClick = () => {
setNum1(num1 + 1);
setNum2(num2 + 2);
};
setState is asynchronous so if you do the two calls one after the other in the same function the state won't have updated for the second call and you won't get what you want.
The best practise in general for when setting state based off previous state is to use a callback.
const updateData = (key, value) => {
setData(prevData => { ...prevData, [key]: value });
};
const onClick = () => {
updateData("num1", 1);
updateData("num2", 2);
};
Related
React custom hooks has been written. On Callback the react state has been updated but getting the state old value in the custom hook callback.
In the example, we can see that on each button click the updated state value will be shown inside the component, but we will get the old previous value inside the callback.
For testing purpose, I have added a settimeout. But with or without setTimeout, I am getting the old state value inside the custom hook callback.
Hook:
const useStateWithCallbackLazy = (initialValue) => {
const callbackRef = React.useRef(null);
const [value, setValue] = React.useState(initialValue);
React.useEffect(() => {
if (callbackRef.current) {
// call back method has been called with the value
callbackRef.current(value);
callbackRef.current = null;
}
}, [value]);
const setValueWithCallback = React.useCallback((newValue, callback) => {
callbackRef.current = callback;
debugger;
setValue(newValue);
}, []);
return [value, setValueWithCallback];
};
Inside component:
const [count, setCount] = useStateWithCallbackLazy(0);
const checkTheCountValue = () => {
console.log('current count value - ' + count);
};
const handleClick = () => {
setCount(count + 1, (currentCount) => {
// Callback method
checkTheCountValue();
});
};
Demo code: https://codesandbox.io/s/funny-gagarin-mucxzh
Referred code from the following plugin.
https://github.com/the-road-to-learn-react/use-state-with-callback
According to your codebase, the checkTheCountValue function always logs the previous count.
At the point of calling the handleClick function, the checkTheCountValue function logs the value of count before setting a new value.
I have modified your code like this.
const checkTheCountValue = (value) => {
console.log('current count value - ' + value);
};
const handleClick = () => {
setCount(count + 1, checkTheCountValue);
};
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]);
I made a custom hooks keep track of a state variable that is based on the amount of socket events received.
When I test by sending 10 simultaneous events the result of the state variable total is 6 or 7 or 8 not the expected 10.
const useSocketAmountReceivedState = (event: string): number => {
const [total, setTotal] = useState(0);
useEffect(() => {
Socket.on(event, () => {
console.log(total);
setTotal(total + 1);
});
return (): void => {
Socket.off(event);
};
});
return total;
}
The logs of run look something like
0
1
1
2
3
3
4
4
4
5
Socket in the example above is implementation around websocket.
So I can deduct that total is not updated fast enough, but what is the best pattern to handle this sort of behaviour ?
Socket.on event has to be outside the useEffect function
const useSocketAmountReceivedState = (event: string): number => {
const [total, setTotal] = useState(0);
Socket.on(event, () => {
console.log(total);
setTotal(total + 1);
});
useEffect(() => {
return (): void => {
Socket.off(event);
};
}, []);
return total;
}
Try putting an empty array as the second argument in the hook. You don't want this to register an event each time the component renders.
const useSocketAmountReceivedState = (event: string): number => {
const [total, setTotal] = useState(0);
useEffect(() => {
Socket.on(event, () => {
console.log(total);
setTotal(total + 1);
});
return (): void => {
Socket.off(event);
};
}, [total]);
return total;
}
UPDATE:
I made an update to my initial answer, and added Total into the dependency array of the React Hook.
Note that the second argument, aka dependency array. It is an array that accepts state or prop. And it instructs React to run this hook each time any of the elements in the dependency array changes.
If you pass an empty array, then the hook will only be run at initial load, after the component mounts.
In your example, if you pass an empty array, it creates a closure. Hence, the total value will always be the initial value.
So you can pass Total into the dependency array, which will invoke the useEffect() to run only when Total value changes. In your example, where there is no dependency array passed to the second argument, the useEffect() will run every single time, which is not what we want.
A colleague came with this solution.
Using a reference for which as it is a reference is not enclosed.
const useSocketAmountReceivedState = (event: string): number => {
const count = useRef(0);
const [state, setState] = useState(0);
useEffect(() => {
Socket.on(event, () => {
count.current++;
setState(count.current);
});
return (): void => {
Socket.off(event);
};
}, []);
return state;
}
I have a list of warehouses that I pull from an API call. I then render a list of components that render checkboxes for each warehouse. I keep the state of the checkbox in an object (using the useState hook). when I check/uncheck the checkbox, I update the object accordingly.
My task is to display a message above the checkbox when it is unchecked. I tried simply using the object, however, the component was not re-rendering when the object changed.
I found a solution to my problem by simply adding another useState hook (boolean value) that serves as a toggle. Since adding it, the component re-renders and my object's value is read and acted on appropriately.
My question is: why did I have to add the toggle to get React to re-render the component? Am I not updating my object in a manner that allows React to see the change in state? Can someone explain to me what is going on here?
I've created a sandbox to demonstrate the issue: https://codesandbox.io/s/intelligent-bhabha-lk61n
function App() {
const warehouses = [
{
warehouseId: "CHI"
},
{
warehouseId: "DAL"
},
{
warehouseId: "MIA"
}
];
const [warehouseStatus, setWarehouseStatus] = useState({});
const [toggle, setToggle] = useState(true);
useEffect(() => {
if (warehouses.length > 0) {
const warehouseStates = warehouses.reduce((acc, item) => {
acc[item.warehouseId] = true;
return acc;
}, {});
setWarehouseStatus(warehouseStates);
}
}, [warehouses.length]);
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
setToggle(!toggle);
};
return warehouses.map((wh, idx) => {
return (
<div key={idx}>
{!warehouseStatus[wh.warehouseId] && <span>This is whack</span>}
<MyCheckbox
initialState
id={wh.warehouseId}
onCheckChanged={handleChange}
label={wh.warehouseId}
/>
</div>
);
});
}
Thanks in advance.
You are mutating state (don't mutate state)
this:
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
};
should be:
const handleChange = ({name,value}) => {
setWarehouseStatus({...warehouseStatus,[name]:value});
};
See the problem?
const newState = warehouseStatus; <- this isn't "newState", it's a reference to the existing state
const { name, value } = obj;
newState[name] = value; <- and now you've gone and mutated the existing state
You then call setState with the same state reference (directly mutated). React says, "hey, that's the same reference to the state I previously had, I don't need to do anything".
I have a Select custom component, that only have a select, options and listen for a onChange, then i have a useReducer code, that initialize with some variables, after select one option my state still have the initialized value
When i select ANOTHER the value in performSearch is ALL
const reducer = (state, newState) => ({ ...state, ...newState });
const [state, setState] = useReducer(reducer, {
filterStatus : 'ALL'
});
const performSearch = () => {
console.log(state.filterStatus) //<= first time is ALL, second time same value ALL, third, is another value
}
useEffect(() => {
performSearch()
},[])
<Select
onChange={(e) => {
const {value} = e.target
setState({filterStatus:value})
performSearch()
}}
items={[{key:"ALL",value:"ALL"},{key:"ANOTHER",value:"ANOTHER"}]}
/>
any idea?
If I had to guess I'd say that its because you are trying to call performSearch to log the console before the state is set. If you console log the state before you return your component you will probably be able to see the correct value in the state. I'm not sure what your use case is but if you want to use the value you can just return it in your function and not worry about the reducer and state at all. Like so:
const performSearch = (value) => {
console.log(value)
}
useEffect(() => {
performSearch('ALL')
},[])
<Select
onChange={(e) => {
const {value} = e.target
performSearch(value)
}}
items={[{key:"ALL",value:"ALL"},{key:"ANOTHER",value:"ANOTHER"}]}
/>
if you need to use the reducer then you can probably create a promise or I would just return the value to preformSearch and then set the state through your reducer from there like so:
const reducer = (state, newState) => ({ ...state, ...newState });
const [state, setState] = useReducer(reducer, {
filterStatus : 'ALL'
});
const performSearch = (value) => {
setState({filterStatus: value});
//Do your stuff with the value
console.log(value)
}
useEffect(() => {
//you can probably just set the value in preformSearch here manually or you can set it to the states value but setting to the states value would result in another render because you would set the state in performSearch again
performSearch(state.filterStatus)
},[])
<Select
onChange={(e) => {
const {value} = e.target
performSearch(value)
}}
items={[{key:"ALL",value:"ALL"},{key:"ANOTHER",value:"ANOTHER"}]}
/>
But like I said I'm not really sure what your end goal is with this component but for this use case I'm not sure you need to use the useReducer function at all.
The problem is that you are calling performSearch() inside the onChange function, so when you set the new state you will only see the value from the previous state. Try to put the performSearch function outside of the onSelect function and you will get the correct output.