Add `get` function to `useState`
Do you want to request a feature or report a bug?
- feature
What is the current behavior? Code from Introducing Hooks:
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
// each time "count" changed, this arrow function will be created again.
// so that it can access the latest "count"
onClick={() => setCount(count + 1)}
I don't think it is good to create a fixed function many times, so I try to modify the code:
(Update on Jul 2022: No matter using the inline anonymous function or wrapping with useCallback, the function will always be created. The difference is that, in useCallback approach, the function reference will not be changed, which could be helpful if we use memo to wrap the component who receives the function as a property)
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(count + 1), []);
But obviously the callback in useCallback couldn't get the latest count because I pass in an empty inputs array to avoid this callback been generated again and again.
So, in fact, the inputs array decide two things:
- when to recreate the callback
- which state can be accessed in the callback
In most situation, the two things are one thing, but here they conflict.
So I think maybe it's good to add a get function to useState like this:
import { useState, useCallback } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount, getCount] = useState(0);
const handleClick = useCallback(() => setCount(getCount() + 1), []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>
Click me
</button>
</div>
);
}
Maybe it's confusing because getCount can totally replace count, but it brings the possible to avoid creating callbacks again and again.
Edited
https://github.com/facebook/react/issues/14543#issuecomment-452237355 exactly resolves the case above. But there‘re many other scenarios can't use updater to resolve. Here are some more code snippets:
1. Access states in a timer.
useEffect(() => {
// or setInterval
const id = setTimeout(() => {
// access states
}, period);
return () => clearTimeout(id);
}, inputs);
2. Access states in WebSocket callbacks
useEffect(() => {
// create a WebSocket client named "ws"
ws.onopen = () => {
// access states
};
ws.onmessage = () => {
// access states
};
return () => ws.close();
}, inputs);
3. Access states in Promise
useEffect(() => {
create_a_promise().then(() => {
// access states
});
}, inputs);
4. Access states in event callbacks
useEffect(() => {
function handleThatEvent() {
// access states
}
instance.addEventListener('eventName', handleThatEvent);
return instance.removeEventListener('eventName', handleThatEvent);
}, inputs);
We had to use some workaround patterns to resolve those cases, like https://github.com/facebook/react/issues/14543#issuecomment-452676760 https://github.com/facebook/react/issues/14543#issuecomment-453058025 https://github.com/facebook/react/issues/14543#issuecomment-453079958 Or a funny way:
const [state, setState] = useState();
useEffect(() => {
// or setInterval
const id = setTimeout(() => {
// access states
setState((prevState) => {
// Now I can do anything with state...🤮
...
return prevState;
});
}, period);
return () => clearTimeout(id);
}, inputs);
So let's discuss and wait... https://github.com/facebook/react/issues/14543#issuecomment-452713416
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
What is the expected behavior?
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
- React 16.7.0-alpha.2
@liyuanqiu you can use updater function in setCount
const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);
@liyuanqiu you can use updater function in setCount
const handleClick = useCallback(() => setCount(prevCount => prevCount + 1), []);
@Saranchenkov Thank you very much, it's all my fault haven't read the document carefully.
But I have another question. As I said before:
So, in fact, the inputs array decide two things:
- when to recreate the callback
- which state can be accessed in the callback
Sometimes I want to do some side effect in hooks like useEffect, for example:
const [count, setCount] = useState(0);
useEffect(() => {
// send count to server every 5 seconds
const id = setInterval(() => {
xhr(count);
}, 5000);
return () => clearInterval(id);
}, []);
If I pass [count] to useEffect, the interval will be cleared and recreated.
If I pass [] to useEffect, I can not get the latest count.
In this situation, maybe a get function is needed?
You could probably do something like this
const [count, setCount] = useState(0);
const countRef = useRef(count)
useEffect(() => {
countRef.current = count
}, [count])
useEffect(() => {
// send count to server every 5 seconds
const id = setInterval(() => {
xhr(countRef.current);
}, 5000);
return () => clearInterval(id);
}, []);
You could probably do something like this
const [count, setCount] = useState(0); const countRef = useRef(count) useEffect(() => { countRef.current = count }, [count]) useEffect(() => { // send count to server every 5 seconds const id = setInterval(() => { xhr(countRef.current); }, 5000); return () => clearInterval(id); }, []);
Thank you @escaton , useRef really can solve this problem.
And the official document thinks this is convoluted but bearable:
This is a rather convoluted pattern but it shows that you can do this escape hatch optimization if you need it. It’s more bearable if you extract it to a custom Hook
But I think it is more like a workaround, couldn't be a paradigm.
One component may have many states. Using three kinds of hooks and five lines of code(or using a custom hook to replace useState) to define an internal state will be a disaster.
I really like the suggestion for having pair of getter and setter returned from useState.. which would make it easier to keep things fresh.. if it doesn't end up in the official implementation, I think it can be implemented in user land using custom hook like:
const useGetterState = (initialState) => {
const [state, setState] = useState(initialState);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state
}, [state]);
return [() => stateRef.current, setState];
}
?
@liyuanqiu I see such options:
- use ref for holding the whole callback rather then single state
- create custom
useStateWithRef()hook which could decorate the originaluseStateand mirror value toref.current - reimplement timer logic so it would be able to resume after state changing
const [count, setCount] = useState(0);
const timerAdjustment = useRef(0)
useEffect(() => {
let id;
let absoluteTimeout;
function tick(firstTime) {
// call xhr only on subsequent calls
!firstTime && xhr(count)
// schedule timer considering previous call
const adjustment = timerAdjustment.current
const timeout = adjustment > 0 ? adjustment : 5000
// remember absolute time to calc adjusted timeout later
absoluteTimeout = Date.now() + timeout
// reset timer adjustment
timerAdjustment.current = 0
id = setTimeout(tick, timeout)
}
tick(true)
return () => {
clearTimeout(id)
// set timer adjustment
timerAdjustment.current = absoluteTimeout - Date.now()
}
}, [count])
@ignatiusreza there is the problem with your solution:
[getCount, setCount] = useGetterState(0)
return (
<button onClick={() => setCount(c => c+1)}>
{getCount()} - increment
</button>
)
on the first render in would be "0 - increment", but after click it would be still "0 - increment" and only on second click it will update. That's because you mutate the reference in useEffect which is fired after component renders.
And while it is fixable:
function useStateWithGetter(initial) {
const [state, setState] = useState(initial)
const ref = useRef(state)
const updater = value => {
if (typeof value === 'function') {
setState(prev => {
const result = value(prev);
ref.current = result
return result
})
} else {
ref.current = value
setState(value)
}
}
const getter = () => ref.current
return [state, updater, getter]
}
there are still issues, because now we referencing last scheduled state, not the current.
upd: Hmm, what if...
function useStateWithRef(initial) {
const [state, setState] = useState(initial)
const ref = useRef()
ref.current = state
return [state, setState, ref]
}
Just to set expectations, we’ve considered all these options a few months ago and decided against them at the time. I’ll keep this open so we can later provide a better response. I don't remember off the top of my mind what the problems were.
I think this is an elegant solution: https://codesandbox.io/s/m1y7vl0vp
function App() {
const [count, setCount] = useState(0);
// ***** Initialize countRef.current with count
const countRef = useRef(count);
const handleClick = useCallback(() => setCount(add1), []);
useEffect(() => {
// ***** countRef.current is xhr function argument
const intervalId = setInterval(() => xhr(countRef.current), 5000);
return () => clearInterval(intervalId);
}, []);
// ***** Set countRef.current to current count
countRef.current = count;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
@liyuanqiu I see such options:
- use ref for holding the whole callback rather then single state
- create custom
useStateWithRef()hook which could decorate the originaluseStateand mirror value toref.current- reimplement timer logic so it would be able to resume after state changing
@escaton Maybe only the second option is effective.
- use ref for holding the whole callback rather then single state
Do you mean https://codesandbox.io/s/jj40lz07l3 ?
/* code snippets */
const [count, setCount] = useState(0);
const repeat = useRef(() => xhr(count));
useEffect(() => {
const intervalId = setInterval(repeat.current, 1000);
return () => clearInterval(intervalId);
}, []);
Not working too.
- reimplement timer logic so it would be able to resume after state changing
It's not the timer's problem. When you use event dispatcher, WebSocket, ajax, promise, same problem. I think no one dares to reimplement them just for adopting React Hooks API.
@btraljic yeah, i suggested the same thing
function useStateWithRef(initial) { const [state, setState] = useState(initial) const ref = useRef() ref.current = state return [state, setState, ref] }
it should work, but it brings side effect countRef.current = count; in the component body rather then in useEffect and that confuses.
@liyuanqiu https://codesandbox.io/s/72jlzz1o86
+useEffect(
+ () => {
+ repeat.current = () => xhr(count);
+ },
+ [count]
+ );
-const intervalId = setInterval(repeat.current, 1000);
+const intervalId = setInterval(() => repeat.current(), 1000);
It's not the timer's problem. When you use event dispatcher, WebSocket, ajax, promise, same problem. I think no one dares to reimplement them just for adopting React Hooks API.
By the reimplementation i mean the proper restart of effect, not the setInterval itself. There is nothing wrong with it, you just want different behaviour.
Could you provide different example with WebSocket or promise?
@btraljic Thank you. I think this line of code been written in the function body is not encouraged by Hooks API:
countRef.current = count;
@see https://reactjs.org/docs/hooks-reference.html#useeffect
Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
Any state change will lead this line been executed. Although in this example, there's no wrong.
If doing so, I think maybe it's better to put it in useLayoutEffect:
useLayoutEffect(() => {
countRef.current = count;
});
And think deeper, here we rely on the layout update to update countRef. Actually, it does not rely on layout update, it relies on the change of state: count.
So finally, it turns back to: https://github.com/facebook/react/issues/14543#issuecomment-452676760
Is it ok now :)
useEffect(() => countRef.current = count);
@btraljic not really :)
now you have actual value in reference and can use it in next effects, but if once you decide to use it in markup, you would see lag between state and ref. Look at example in my answer to @ignatiusreza
@escaton Ok, but we are living in an asynchronous world. Aren't we? :)
Speaking about accessing state in setInterval — i came up with another idea.
It can be treated as two separate side effects: one is timer tick, another is xhr.
So it could be
const [count, setCount] = useState(0)
const [tick, setTick] = useState(0)
useEffect(() => {
const timerId = setInterval(() => {
setTick(t => t+1)
}), 5000)
return () => clearInterval(timerId)
}, [])
useEffect(() => {
xhr(count)
}, [tick])
Pros: easy to understand what happens, no refs
Cons: wasteful rerenders on tick updates every 5 seconds
I usually use this way with reducer
const [count, setCount] = useState(0)
const [commitIndex, commit] = React.useReducer(state => state +1, 0)
useEffect(() => {
const timerId = setInterval(commit, 5000)
return () => clearInterval(timerId)
}, [])
useEffect(() => {
xhr(count)
}, [commitIndex])
If we can use reft to hold state, why not just use an global object?
const obj = {}
function Comp() {
const [count, setCount] = useState(0)
useEffect(() => {
xhr(obj.count)
})
obj.count = count
}
So obj may hold multi state for one component.
Doing so, you can't use more than one Comp on the page, but it could be fixed
function CompFabric () {
const obj = {}
return function Comp() {
const [count, setCount] = useState(0)
useEffect(() => {
xhr(obj.count)
})
obj.count = count
}
}
Although it's almost same as useRef i strongly discourage you to use it. It is both non idiomatic and confuses other contributors.
Everybody here tells many solutions, but really like a sentence: "Life, Uh, Finds a Way." Just a joke, no offense :)
Look back, my requirement is so easy, just want to repeatedly send a state to server. But with React Hooks API, it became strange and complex.
Let's see how to program in old class style:
https://codesandbox.io/s/40p9qqr009
class App extends React.Component {
state = {
count: 0,
};
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
xhr = () => {
const { count } = this.state;
console.log(`Send ${count} to server.`);
// TODO send count to my server by XMLHttpRequest
};
componentDidMount() {
this.intervalId = setInterval(this.xhr, 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
const { count } = this.state;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={this.handleClick}>Click me</button>
</div>
);
}
}
Naturally and reasonable, right? No magic, no tricks.
I hope Hooks API will finally be so.
I complain a lot, don't diss me 🙏 XD
Sure, it looks familiar.
The only difference with hooks here is that component's state is explicitly bounded to instance and so could be accessed with this anywhere.
To achieve the same in functional component it needs to break the rule and to bound state to ref right in the component body without useEffect wrapping.
Let's wait for @gaearon asnwer.
I guess this is it https://overreacted.io/making-setinterval-declarative-with-react-hooks/ :)
I guess this is it https://overreacted.io/making-setinterval-declarative-with-react-hooks/ :)
In this post, a pattern is introduced to capsulate those APIs who has “impedance mismatch” with the React programming model.
So we have to write helper functions(custom hooks) to help us using these APIs. That's still annoying.
Although Dan's useInterval brings many great features like dynamic delay and pause and resume to setInterval, but that's not the first motivation to write useInterval. Those great features are just derivatives.
We may encounter many APIs that has “impedance mismatch” with the React programming model. Capsulating them one by one just like @types/xxx in Typescript is hard. Maybe I should create an organization named DefinitelyHooked 😀.
This is an early time for Hooks, and there are definitely still patterns we need to work out and compare. Don’t rush to adopt Hooks if you’re used to following well-known “best practices”. There’s still a lot to try and discover.
Ok so it seems like the solution to this is this:??
function useStateWithRef(initial) {
const ref = useRef();
const [state, setState] = useState(initial);
ref.current = state;
useEffect( () =>{
ref.current = state;
});
return [state, setState, ref];
}
I def see the value in having a getter on hooks.
On second thought.. why not just do this??
function useStateWithGetter(initial) {
const ref = useRef();
const [state, setState] = useState(initial);
ref.current = state;
const set = (value) => {
ref.current = value;
setState(value);
};
const get = () => {
return ref.current;
};
return [state, set, get];
}
mark
@gaearon You mentioned this was discussed previously and left it open. Can we start this conversation back up? Or at the very least, verify that the solution above is an ok practice.
https://overreacted.io/how-are-function-components-different-from-classes/ This article may help to understand the behavior of Hooks API(actually is Functional Component). It's highly recommended to spend half an hour to read it.
Just read that article... it basically reassures the solution above would work for adding a getter.
A Complete Guide to useEffect touches this subject quite a lot too. Really recommend reading it through, it was an eye opener for me.
Until now, Do we still have to do use ref as escape hatch to resolve those problem which have “impedance mismatch” with the React programming model? @gaearon
@joepuzzo I like the version with no useEffect! But I don't think your code snippet handles the setter being called with a function. Not hard to update it, looking at how React itself does it in https://github.com/facebook/react/blob/b53ea6ca05d2ccb9950b40b33f74dfee0421d872/packages/react-dom/src/server/ReactPartialRendererHooks.js#L251
function useStateWithGetter(initial) {
const ref = useRef(initial);
const [state, setState] = useState(initial);
const set = (valueOrUpdater) => {
if (typeof valueOrUpdater === 'function') {
setState((prev) => {
ref.current = valueOrUpdater(prev);
return ref.current;
})
} else {
ref.current = value;
setState(value);
}
};
const get = () => {
return ref.current;
};
return [state, set, get];
}
Yup That would do the trick @johnnysprinkles ! I have done that in the past as well. For anyone here that is saying that a getter is not a good pattern I disagree. I have been developing an OS form library for years and this pattern becomes valuable. The key is some things in JS might have a reference to a value in state and they cant use the outdated version. You either pass around a ref or you use a getter.
@joepuzzo I like the version with no
useEffect! But I don't think your code snippet handles the setter being called with a function. Not hard to update it, looking at how React itself does it inhttps://github.com/facebook/react/blob/b53ea6ca05d2ccb9950b40b33f74dfee0421d872/packages/react-dom/src/server/ReactPartialRendererHooks.js#L251
function useStateWithGetter(initial) { const ref = useRef(initial); const [state, setState] = useState(initial); const set = (valueOrUpdater) => { if (typeof valueOrUpdater === 'function') { setState((prev) => { ref.current = valueOrUpdater(prev); return ref.current; }) } else { ref.current = value; setState(value); } }; const get = () => { return ref.current; }; return [state, set, get]; }
I think set ref.current when setState will cause some issue in concurrent mode.