Getting state old value on React custom hook callback - javascript

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

Related

Fixing hook call outside of the body of a function component

I made a custom ReactJS hook to handle a couple of specific mouse events, as below:
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const handleSelectedItem = (service: Filter) => {
service.selected = !service.selected;
setHealthcareServices([...healthcareServices]);
onChange(healthcareServices);
};
const handleSingleClick = (service: Filter) => {
console.log('single-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxSingleClick />;
}
handleSelectedItem(service);
};
const handleDoubleClick = (service: Filter) => {
console.log('double-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxDoubleClick />;
}
handleSelectedItem(service);
};
const handleClick = (service: Filter) =>
useSingleAndDoubleClick(
() => handleSingleClick(service),
() => handleDoubleClick(service)
);
...
return (
<div className={classes.filter_container}>
...
<div className={classes.filter_subgroup}>
{filters.map((filter) => (
<div key={`${filter.label}-${filter.value}`} className={classes.filter}>
<Checkbox
label={filter.label}
className={classes.checkbox}
checked={filter.selected}
onChange={() => handleClick(filter)}
checkedIcon={filter.checkedIcon}
/>
</div>
))}
</div>
...
</div>
);
};
When I click on my <Checkbox />, the whole thing crashes. The error is:
The top of my stacktrace points to useState inside my hook. If I move it outside, so the hook looks as:
const [click, setClick] = useState(0);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
The problem still happens, only the stacktrace points to the useEffect hook. The code is based on another answer here.
Any suggestions?
You've defined your useSingleAndDoubleClick hook inside of a component. That's not what you want to do. The idea of custom hooks is that you can move logic outside of your components that could otherwise only happen inside of them. This helps with code reuse.
There is no use for a hook being defined inside a function, as the magic of hooks is that they give you access to state variables and such things that are usually only allowed to be interacted with inside function components.
You either need to define your hook outside the component and call it inside the component, or remove the definition of useSingleAndDoubleClick and just do everything inside the component.
EDIT: One more note to help clarify: the rule that you've really broken here is that you've called other hooks (ie, useState, useEffect) inside your useSingleAndDoubleClick function. Even though it's called useSingleAndDoubleClick, it's not actually a hook, because it's not being created or called like a hook. Therefore, you are not allowed to call other hooks inside of it.
EDIT: I mentioned this earlier, but here's an example that could work of moving the hook definition outside the function:
EDIT: Also had to change where you call the hook: you can't call the hook in a nested function, but I don't think you need to.
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
// your other handlers
// changed this - don't call the hook inside the function.
// your hook is returning the handler you want anyways, I think
const handleClick = useSingleAndDoubleClick(handleSingleClick, handleDoubleClick)

Back to back useState not updating first state, only the second one

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

Keep state in sync with fast occuring events

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

Why is my setState updating state only once?

const [invalidateCacheKey, setInvalidateCacheKey] = useState(0);
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey + 1);
mutate();
};
const selectOrder = () => {
dispatch(
showModal('SHOOTING_OPERATIONAL_VIEW', {
modalType: 'OPERATIONAL_VIEW',
modalProps: {
content: <ShootingActionsView updateOrders={mutate} onChangeAssignee={onChangeAssignee} />,
},
})
);
};
I have a functional component, I'm using useState to update the state of my invalidateCacheKey counter.
Then I have a dispatch method (react-redux) that displays a modal, I pass to the modal the callback (onChangeAssignee).
The problem is that: when the callback is fired the state (invalidateCacheKey) doesn't change inside the onChangeAssignee method (it is 0 after and before run the callback logging state inside the onChangeAssignee method), while inside the functional component (logging the state after useState declaration) the state (invalidateCacheKey) is 0 before the callback and is 1 after the callback.
I think that problem is dispatch method, it "stores" my state and it doesn't update it.
How to fix that?
Ciao, unfortunately hooks in react are async so if you try to write something like:
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey + 1);
console.log(invalidateCacheKey)
...
};
you will log an old value of invalidateCacheKey, because setInvalidateCacheKey is async as I told you. To get updated value in react hooks you could use useEffect hook like:
useEffect(() => { // this will be triggered every time invalidateCacheKey changes
console.log(invalidateCacheKey) // this shows the la st value of invalidateCacheKey
}, [invalidateCacheKey])
As an alternative, you could use a use-state-with-callback library. With this library you could write something like:
import useStateWithCallback from 'use-state-with-callback';
...
const [invalidateCacheKey, setInvalidateCacheKey] = useStateWithCallback(0, invalidateCacheKey => {
console.log(invalidateCacheKey) // here you have the last value of invalidateCacheKey
});
Note: set state reading state itself is always discouraged in react hooks. I suggest you to use this way:
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey => invalidateCacheKey + 1);
...
};
or
const onChangeAssignee = () => {
let appo = invalidateCacheKey;
setInvalidateCacheKey(appo + 1);
...
};
EDIT
Now lets say you need to use invalidateCacheKey in onChangeAssignee function. Lets suppose you worte code like this:
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey + 1);
dostuff(invalidateCacheKey) // dostuff takes an old value of invalidateCacheKey so it doesn't work
};
You can solve this by moving dostuff into useEffect hook like:
useEffect(() => {
dostuff(invalidateCacheKey) // here dostuff works because it takes last value of invalidateCacheKey
}, [invalidateCacheKey])
const onChangeAssignee = () => {
setInvalidateCacheKey(invalidateCacheKey => invalidateCacheKey + 1);
};

How do I get updated state after useEffect has set an onChange event to an element?

I have an input field that needs an onChange event to be bound on mounting the component. The onChange event needs to update the state. However, the state never gets updated because the onChange event always has the initial state. What is the best way to make this work?
Here is my code:
const someComponent: FC<someComponent> = props => {
const [count, setCount] = useState(0);
const someFunction = () => {
console.log(count) // 0
setCount(count+ 1);
console.log(count) // Also 0
}
useEffect(() => {
inputField.onChange = () => {
someFunction();
}
}, [])
}
You can tweek your useEffect to get the updated state. State setting is async in nature. -
const someComponent: FC<someComponent> = props => {
const [count, setCount] = useState(0);
const someFunction = () => {
console.log(count) // 0
setCount(count+ 1);
console.log(count) // Also 0
}
useEffect(() => {
inputField.onChange = () => {
someFunction();
}
}, [])
useEffect(() => {
console.log(count) //updated value//
}, [count]) // adding count to dependency array will cause this useEffect to run on every count change //
}

Categories

Resources