Related
I am doing a small React project.
I heard somewhere that using setState(()=>) is better than setState().
For instance, let [count, setCount] = useState(0)
setCount(prev=>prev+1) is better than setCount(count+1)
so I kept using the one having arrow function.
But, here in the code, I found that the arrow function one did not work, but the other one worked.
const addCard = () => {
setCards([
...cards,
{
topic: topicRef.current.value,
desc: descRef.current.value,
id: nanoid(),
done: false,
},
]);
topicRef.current.value = "";
descRef.current.value = "";
};
this code works fine, however if I start doing like setCards(prevState=>[...prevState, {}]), the first render works OK, but after that, the Ref value does not read the value I put properly. (I used useRef() for topic and desc.)
What is the difference between setState(()=>) and setState()?
The more classic example is when you use setInterval inside the useEffect. take a look at the following code:
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
let interval = setInterval(() => {
// setCount((c) => c + 1);
setCount(count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return (
<div className="App">
<h1>{count}</h1>
</div>
);
}
In this case, it renders 0 and then increments the count once after one second. The count value will remain at 1 after that.
The reason behind this is that count here represents your React state. This is a variable that holds a value. A closure is created over the outer scope where count is declared - which is the App component itself. Such variables can change value between function calls, but this is where React's immutability comes into play.
You can update the state using the second method as follows:
useEffect(() => {
let interval = setInterval(() => {
setCount((c) => c + 1); // this will update count value every second
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
There will always be a fresh state for that variable when using this method.
That's why you shouldn't jump into React without learning core JS first. React is not JavaScript, but rather a declarative JavaScript library that simplifies some things.
Codesandbox link of the above example: https://codesandbox.io/s/react-interval-gi4xl7
Great article by Dan referencing the same topic: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
When I use class component, I have code:
setTimeout(() => console.log(this.state.count), 5000);
When I use hook:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
If I trigger setTimeout then change the count to 1 before the timeout (5000ms), class component will console.log(1) (the newest value), and for useState it is console.log(0) (value when register timeout).
Why does this happen?
Updated Version:
Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?
Case 1: State variable in function component (stale closure):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Case 2: State variable in class component (no stale closure):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
Case 3: Let's try to create a stale closure around this
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
Case 4: Let's again try to create a stale closure in class component
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
Case 5: Let's again try to create a stale closure in class component
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
source-1, source-2
Case 6: Let's find another solution for function components
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Original Version:
The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using "updater" form of setState:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
}, [])
React.useEffect(() => {
setTimeout(() => {
let count
setCount(p => {
console.log('p: ', p)
count = p
return p
})
console.log('count after 5 secs: ', count, 'Correct')
}, 5000);
}, [])
return (<div>
<button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
<div>Latest count: {count}</div>
</div>)
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>
For the useState, it create a timeout using count in the first time. It accesses the count value through a closure. When we set a new value by setCount, the component rerender but does not change the value passed to timeout.We can use const count = useRef(0) and pass to timeout count.current. This will always use the newest value of count.
Check this link for more information.
Timeouts don't play along nicely with reacts declarative programming model. In functional components, each render is a single frame in time. They never change.
When state updates, all state variables are created locally anew and don't overwrite the old closed variables.
You can also think of effects in the same way, where an effect will run in its local realm with all its local state variables on each render and new renders don't affect their output.
The only way to break out of this model is refs. Or class components where state is effectively similar to refs where the instance (this) is the ref container. Refs allow cross-render communication and closure busting. Use sparingly and with caution.
Dan Abramov has a fantastic article explaining all this and a hook that solves this. As you correctly answered, the issue is caused by stale closures. The solution indeed involves using refs.
Explanation
With function components, every render is a function call, creating a new function closure for that specific call. The function component is closing over the setTimeout callback function, so that everything in the setTimeout callback is accessing only the specific render where it was called.
Reusable solution:
Using a Ref and accessing it only within the setTimeout callback will give you a value that is persistent across renders.
However, it isn't that convenient to use a React Ref with a value that is always updating, like a counter. You are in charge of both updating the value, and causing a rerender yourself. Updating a Ref doesn't entail a component render.
My solution, for easy use, is to combine both useState and useRef hooks into a single "useStateAndRef" hook. This way, you get a setter that gets both the value, and a ref for use in async situations such as setTimeout and setInterval:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
Working CodeSandbox link: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx
I'm finding these two pieces of the React Hooks docs a little confusing. Which one is the best practice for updating a state object using the state hook?
Imagine a want to make the following state update:
INITIAL_STATE = {
propA: true,
propB: true
}
stateAfter = {
propA: true,
propB: false // Changing this property
}
OPTION 1
From the Using the React Hook article, we get that this is possible:
const [count, setCount] = useState(0);
setCount(count + 1);
So I could do:
const [myState, setMyState] = useState(INITIAL_STATE);
And then:
setMyState({
...myState,
propB: false
});
OPTION 2
And from the Hooks Reference we get that:
Unlike the setState method found in class components, useState does
not automatically merge update objects. You can replicate this
behavior by combining the function updater form with object spread
syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
As far as I know, both works. So, what is the difference? Which one is the best practice? Should I use pass the function (OPTION 2) to access the previous state, or should I simply access the current state with spread syntax (OPTION 1)?
Both options are valid, but just as with setState in a class component you need to be careful when updating state derived from something that already is in state.
If you e.g. update a count twice in a row, it will not work as expected if you don't use the function version of updating the state.
const { useState } = React;
function App() {
const [count, setCount] = useState(0);
function brokenIncrement() {
setCount(count + 1);
setCount(count + 1);
}
function increment() {
setCount(count => count + 1);
setCount(count => count + 1);
}
return (
<div>
<div>{count}</div>
<button onClick={brokenIncrement}>Broken increment</button>
<button onClick={increment}>Increment</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
If anyone is searching for useState() hooks update for object
Through Input
const [state, setState] = useState({ fName: "", lName: "" });
const handleChange = e => {
const { name, value } = e.target;
setState(prevState => ({
...prevState,
[name]: value
}));
};
<input
value={state.fName}
type="text"
onChange={handleChange}
name="fName"
/>
<input
value={state.lName}
type="text"
onChange={handleChange}
name="lName"
/>
Through onSubmit or button click
setState(prevState => ({
...prevState,
fName: 'your updated value here'
}));
The best practice is to use separate calls:
const [a, setA] = useState(true);
const [b, setB] = useState(true);
Option 1 might lead to more bugs because such code often end up inside a closure which has an outdated value of myState.
Option 2 should be used when the new state is based on the old one:
setCount(count => count + 1);
For complex state structure consider using useReducer
For complex structures that share some shape and logic you can create a custom hook:
function useField(defaultValue) {
const [value, setValue] = useState(defaultValue);
const [dirty, setDirty] = useState(false);
const [touched, setTouched] = useState(false);
function handleChange(e) {
setValue(e.target.value);
setTouched(true);
}
return {
value, setValue,
dirty, setDirty,
touched, setTouched,
handleChange
}
}
function MyComponent() {
const username = useField('some username');
const email = useField('some#mail.com');
return <input name="username" value={username.value} onChange={username.handleChange}/>;
}
Which one is the best practice for updating a state object using the state hook?
They are both valid as other answers have pointed out.
what is the difference?
It seems like the confusion is due to "Unlike the setState method found in class components, useState does not automatically merge update objects", especially the "merge" part.
Let's compare this.setState & useState
class SetStateApp extends React.Component {
state = {
propA: true,
propB: true
};
toggle = e => {
const { name } = e.target;
this.setState(
prevState => ({
[name]: !prevState[name]
}),
() => console.log(`this.state`, this.state)
);
};
...
}
function HooksApp() {
const INITIAL_STATE = { propA: true, propB: true };
const [myState, setMyState] = React.useState(INITIAL_STATE);
const { propA, propB } = myState;
function toggle(e) {
const { name } = e.target;
setMyState({ [name]: !myState[name] });
}
...
}
Both of them toggles propA/B in toggle handler.
And they both update just one prop passed as e.target.name.
Check out the difference it makes when you update just one property in setMyState.
Following demo shows that clicking on propA throws an error(which occurs setMyState only),
You can following along
Warning: A component is changing a controlled input of type checkbox to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
It's because when you click on propA checkbox, propB value is dropped and only propA value is toggled thus making propB's checked value as undefined making the checkbox uncontrolled.
And the this.setState updates only one property at a time but it merges other property thus the checkboxes stay controlled.
I dug thru the source code and the behavior is due to useState calling useReducer
Internally, useState calls useReducer, which returns whatever state a reducer returns.
https://github.com/facebook/react/blob/2b93d686e3/packages/react-reconciler/src/ReactFiberHooks.js#L1230
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
...
try {
return updateState(initialState);
} finally {
...
}
},
where updateState is the internal implementation for useReducer.
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateReducer(reducer, initialArg, init);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
If you are familiar with Redux, you normally return a new object by spreading over previous state as you did in option 1.
setMyState({
...myState,
propB: false
});
So if you set just one property, other properties are not merged.
One or more options regarding state type can be suitable depending on your usecase
Generally you could follow the following rules to decide the sort of state that you want
First: Are the individual states related
If the individual state that you have in your application are related to one other then you can choose to group them together in an object. Else its better to keep them separate and use multiple useState so that when dealing with specific handlers you are only updating the relavant state property and are not concerned about the others
For instance, user properties such as name, email are related and you can group them together Whereas for maintaining multiple counters you can make use of multiple useState hooks
Second: Is the logic to update state complex and depends on the handler or user interaction
In the above case its better to make use of useReducer for state definition. Such kind of scenario is very common when you are trying to create for example and todo app where you want to update, create and delete elements on different interactions
Should I use pass the function (OPTION 2) to access the previous
state, or should I simply access the current state with spread syntax
(OPTION 1)?
state updates using hooks are also batched and hence whenever you want to update state based on previous one its better to use the callback pattern.
The callback pattern to update state also comes in handy when the setter doesn't receive updated value from enclosed closure due to it being defined only once. An example of such as case if the useEffect being called only on initial render when adds a listener that updates state on an event.
Both are perfectly fine for that use case. The functional argument that you pass to setState is only really useful when you want to conditionally set the state by diffing the previous state (I mean you can just do it with logic surrounding the call to setState but I think it looks cleaner in the function) or if you set state in a closure that doesn't have immediate access to the freshest version of previous state.
An example being something like an event listener that is only bound once (for whatever reason) on mount to the window. E.g.
useEffect(function() {
window.addEventListener("click", handleClick)
}, [])
function handleClick() {
setState(prevState => ({...prevState, new: true }))
}
If handleClick was only setting the state using option 1, it would look like setState({...prevState, new: true }). However, this would likely introduce a bug because prevState would only capture the state on initial render and not from any updates. The function argument passed to setState would always have access to the most recent iteration of your state.
Both options are valid but they do make a difference.
Use Option 1 (setCount(count + 1)) if
Property doesn't matter visually when it updates browser
Sacrifice refresh rate for performance
Updating input state based on event (ie event.target.value); if you use Option 2, it will set event to null due to performance reasons unless you have event.persist() - Refer to event pooling.
Use Option 2 (setCount(c => c + 1)) if
Property does matter when it updates on the browser
Sacrifice performance for better refresh rate
I noticed this issue when some Alerts with autoclose feature that should close sequentially closed in batches.
Note: I don't have stats proving the difference in performance but its based on a React conference on React 16 performance optimizations.
I find it very convenient to use useReducer hook for managing complex state, instead of useState. You initialize state and updating function like this:
const initialState = { name: "Bob", occupation: "builder" };
const [state, updateState] = useReducer(
(state, updates) => {...state, ...updates},
initialState
);
And then you're able to update your state by only passing partial updates:
updateState({ occupation: "postman" })
The solution I am going to propose is much simpler and easier to not mess up than the ones above, and has the same usage as the useState API.
Use the npm package use-merge-state (here). Add it to your dependencies, then, use it like:
const useMergeState = require("use-merge-state") // Import
const [state, setState] = useMergeState(initial_state, {merge: true}) // Declare
setState(new_state) // Just like you set a new state with 'useState'
Hope this helps everyone. :)
When I use class component, I have code:
setTimeout(() => console.log(this.state.count), 5000);
When I use hook:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
If I trigger setTimeout then change the count to 1 before the timeout (5000ms), class component will console.log(1) (the newest value), and for useState it is console.log(0) (value when register timeout).
Why does this happen?
Updated Version:
Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?
Case 1: State variable in function component (stale closure):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Case 2: State variable in class component (no stale closure):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
Case 3: Let's try to create a stale closure around this
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
Case 4: Let's again try to create a stale closure in class component
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
Case 5: Let's again try to create a stale closure in class component
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
source-1, source-2
Case 6: Let's find another solution for function components
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Original Version:
The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using "updater" form of setState:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
}, [])
React.useEffect(() => {
setTimeout(() => {
let count
setCount(p => {
console.log('p: ', p)
count = p
return p
})
console.log('count after 5 secs: ', count, 'Correct')
}, 5000);
}, [])
return (<div>
<button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
<div>Latest count: {count}</div>
</div>)
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>
For the useState, it create a timeout using count in the first time. It accesses the count value through a closure. When we set a new value by setCount, the component rerender but does not change the value passed to timeout.We can use const count = useRef(0) and pass to timeout count.current. This will always use the newest value of count.
Check this link for more information.
Timeouts don't play along nicely with reacts declarative programming model. In functional components, each render is a single frame in time. They never change.
When state updates, all state variables are created locally anew and don't overwrite the old closed variables.
You can also think of effects in the same way, where an effect will run in its local realm with all its local state variables on each render and new renders don't affect their output.
The only way to break out of this model is refs. Or class components where state is effectively similar to refs where the instance (this) is the ref container. Refs allow cross-render communication and closure busting. Use sparingly and with caution.
Dan Abramov has a fantastic article explaining all this and a hook that solves this. As you correctly answered, the issue is caused by stale closures. The solution indeed involves using refs.
Explanation
With function components, every render is a function call, creating a new function closure for that specific call. The function component is closing over the setTimeout callback function, so that everything in the setTimeout callback is accessing only the specific render where it was called.
Reusable solution:
Using a Ref and accessing it only within the setTimeout callback will give you a value that is persistent across renders.
However, it isn't that convenient to use a React Ref with a value that is always updating, like a counter. You are in charge of both updating the value, and causing a rerender yourself. Updating a Ref doesn't entail a component render.
My solution, for easy use, is to combine both useState and useRef hooks into a single "useStateAndRef" hook. This way, you get a setter that gets both the value, and a ref for use in async situations such as setTimeout and setInterval:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
Working CodeSandbox link: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx
Using hooks, I would like to execute a function only after a particular call to update state. For instance, I would like to achieve the same functionality that this does (assuming I already have already instantiated this piece of state.)
setState({name: Joe}, () => console.log('hi'));
I do not want this to log 'hi' every time that name changes, I only want to log 'hi' after this particular setState call has been executed.
This
const [name, setName] = useState('');
useEffect(() => {
console.log('hi');
}, [name]);
setName('Joe');
setName('Bob'); // only log 'hi' after this one!
setName('Joe');
setName('Bob');
will not work for my purposes because I don't want to log 'hi' every time name changes. The value in the setName call does not matter. The console.log must be executed only after this particular setName call has been executed.
Update: I was overthinking this. I was asking this question because I had a piece of state called "mode" that determined some conditional rendering through a switch statement:
switch(mode) {
case foo: return <Foo />
case bar: return <Bar />
}
I was only wanting to fire some logic when mode was a certain value (aka a certain component would be rendered). I simply moved this logic down a level into the lower-level component and used
React.useEffect(() => {
someLogic();
}, []);
in order to only fire this logic on component render.
State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
Just spitballing here
const useStateWithCallback = val => {
const callback = useRef();
const [state,setState] = useState(val);
useEffect(() => {
callback.current && callback.current(state);
callback.current = null;
},[callback,state]);
return [
state,
useCallback((arg,cb) => {
callback.current = cb;
return setState(arg);
},[callback,setState])) // these are safe to omit, right?
]
}
EDIT: not to be too verbose, but usage:
import { useStateWithCallback } from './useStateWithCallback';
const MyCmp = () => {
const [name,setName] = useStateWithCallback('');
...
setName('joe',state => console.log(`Hi ${state}`));
...
}