I'm very familiar with ReactJS but just started to embrace Hooks.
What's difficult to me is sometimes how to wrap imperative APIs to hooks. I have the feeling that sometimes I'm using the wrong approach.
For example, take this greatly simplified example from a real-world application. Assume there is a global state API (that can't be changed):
server.getState(name)returns the last known state (let's say, it's a string) in a synchronous mannerserver.on(name, callback)registers an event handler for changes of that stateserver.off(name, callback)de-registers that event handler
I want to create a custom hook useServerState(name, defaultValue) that returns a live value of that state.
My implementation would look like this:
function useServerState(name, defaultValue) {
const [ curState, setCurState ] = useState(() => server.getState(name) || defaultValue);
useEffect(
() => {
const evHandler = payload => setCurState(payload);
// register on mount:
server.on(name, evHandler);
// de-register on unmount:
return () => server.off(name, evHandler);
},
[ name ]
);
return curState;
}
Usage:
function SomeComponent() {
const temperature = useServerState("temp_outside", "unknown");
return <div>
Current temperature: {temperature} degrees
</div>;
}
The problem
This works, but the problem is that useEffect() seems to invoke the function asynchronously. There is some delay until my useEffect() handler is run, like 50 ms or so. During that time my hook is blind for new events that might come in in the meantime (since the event handler is not registered yet), which means that the hook might return an old state even if it has been updated.
A (inelegant) workaround
I can work around the problem by calling getState() again inside the useEffect() handler (with other words, on "mount"):
function useServerState(name, defaultValue) {
const getInitialState = () => server.getState(name) || defaultValue;
const [ curState, setCurState ] = useState(getInitialState);
useEffect(
() => {
const evHandler = payload => setCurState(payload);
// register on mount:
server.on(name, evHandler);
// ----->
// anti race condition hack:
const nv = getInitialState();
if (nv !== curState)
setCurState(nv);
// <-----
// de-register on unmount:
return () => server.off(name, evHandler);
},
[ name ]
);
return curState;
}
Another (bad) workaround
Another solution could be to always use a fixed start value and read the current state on mount only:
function useServerState(name, defaultValue) {
const [ curState, setCurState ] = useState(null);
useEffect(
() => {
const evHandler = payload => setCurState(payload);
// register on mount:
server.on(name, evHandler);
// set initial state:
setCurState(server.getState(name) || defaultValue);
// de-register on unmount:
return () => server.off(name, evHandler);
},
[ name ]
);
return curState;
}
The downside of this solution is that it will always cause two renders: one with null and then another one with the actual state. This is not desired.
How to improve?
The workarounds seem awkward to me and make me wonder if there is a better way to do this that does not involve a workaround. I would expect that the React team has thought about such situations. What am I missing?