How to solve dependent state updates "the react way"? - javascript

I stumbled upon a very interesting question and I would like to know how to best solve this in React. Assume the following code:
const [qrText, setQrText] = useState("")
...
const generateQrCode = () => {
// set other state inside the "then"
QRCode.toDataUrl(qrText).then(...)
}
const handleChange = (e) => {
setQrText(e.target.value)
generateQrCode()
}
This code is unsafe, since state updates are asynchronously, and by the time generateQrCode runs, qrText could still have the old value.
I always tended to solve this problem using a useEffect with dependency array:
const [qrText, setQrText] = useState("")
...
const handleChange = (e) => {
setQrText(e.target.value)
}
useEffect(() => {
const generateQrCode = () => {
// set other state inside the "then"
QRCode.toDataUrl(qrText).then(...)
}
generateQrCode()
}, [qrText])
However, I recently watched a YouTube video from a React conference, where a senior engineer said that useEffect is only supposed to be used to synchronize data with external services or the DOM. Instead, people should update state in event handlers only.
So is this the right way then to handle this scenario?
const [qrText, setQrText] = useState("")
...
// this now takes the qrText as argument
const generateQrCode = (qrTextArg) => {
// set other state inside the "then"
QRCode.toDataUrl(qrTextArg).then(...)
}
const handleChange = (e) => {
const value = e.target.value
setQrText(value)
generateQrCode(value) // pass the event value, instead of relying on the "qrText" state
}
This would equal the "event based" approached, but feels a bit imperative and not "react"-ish.
So I wonder, what is the intended way to do this?
Thanks for your answers!

Related

Is it a common practice in React to keep the same value in state and ref?

My React app uses setTimeout() and setInterval(). Inside them, I need to access the state value. As we know, closures are bound to their context once created, so using state values in setTimeout() / setInterval() won't use the newest value.
Let's keep things simple and say my component is defined as such:
import { useState, useEffect, useRef } from 'react';
const Foo = () => {
const [number, setNumber] = useState(0);
const numberRef = useRef(number);
// Is this common? Any pitfalls? Can it be done better?
numberRef.current = number;
useEffect(
() => setInterval(
() => {
if (numberRef.current % 2 === 0) {
console.log('Yay!');
}
},
1000
),
[]
);
return (
<>
<button type="button" onClick={() => setNumber(n => n + 1)}>
Add one
</button>
<div>Number: {number}</div>
</>
);
};
In total I came up with 3 ideas how to achieve this, is any of them a recognized pattern?
Assigning state value to ref on every render, just like above:
numberRef.current = number;
The benefit is very simplistic code.
Using useEffect() to register changes of number:
useEffect(
() => numberRef.current = number,
[number]
);
This one looks more React-ish, but is it really necessary? Doesn't it actually downgrade the performance when a simple assignment from point #1 could be used?
Using custom setter:
const [number, setNumberState] = useState(0);
const numberRef = useRef(number);
const setNumber = value => {
setNumberState(value);
numberRef.current = value;
};
Is having the same value in the state and the ref a common pattern with React? And is any of these 3 ways more popular than others for any reason? What are the alternatives?
2021-10-17 EDIT:
Since this looks like a common scenario I wanted to wrap this whole logic into an intuitive
useInterval(
() => console.log(`latest number value is: ${number}`),
1000
)
where useInterval parameter can always "access" latest state.
After playing around for a bit in a CodeSandbox I've come to the realization that there is no way someone else hasn't already thought about a solution for this.
Lo and behold, the man himself, Dan Abramov has a blog post with a precise solution for our question https://overreacted.io/making-setinterval-declarative-with-react-hooks/
I highly recommend reading the full blog since it describes a general issue with the mismatch between declarative React programming and imperative APIs. Dan also explains his process (step by step) of developing a full solution with an ability to change interval delay when needed.
Here (CodeSandbox) you can test it in your particular case.
ORIGINAL answer:
1.
numberRef.current = number;
I would avoid this since we generally want to do state/ref updates in the useEffect instead of the render method.
In this particular case, it doesn't have much impact, however, if you were to add another state and modify it -> a render cycle would be triggered -> this code would also run and assign a value for no reason (number value wouldn't change).
2.
useEffect(
() => numberRef.current = number,
[number]
);
IMHO, this is the best way out of all the 3 ways you provided. This is a clean/declarative way of "syncing" managed state to the mutable ref object.
3.
const [number, setNumberState] = useState(0);
const numberRef = useRef(number);
const setNumber = value => {
setNumberState(value);
numberRef.current = value;
};
In my opinion, this is not ideal. Other developers are used to React API and might not see your custom setter and instead use a default setNumberState when adding more logic expecting it to be used as a "source of truth" -> setInterval will not get the latest data.
You have simply forgotten to clear interval. You have to clear the interval on rendering.
useEffect(() => {
const id = setInterval(() => {
if (numberRef.current % 2 === 0) {
console.log("Yay!");
}
}, 1000);
return () => clearInterval(id);
}, []);
If you won't clear, this will keep creating a new setInterval with every click. That can lead to unwanted behaviour.
Simplified code:
const Foo = () => {
const [number, setNumber] = useState(0);
useEffect(() => {
const id = setInterval(() => {
if (number % 2 === 0) {
console.log("Yay!");
}
}, 1000);
return () => clearInterval(id);
}, [number]);
return (
<div>
<button type="button" onClick={() => setNumber(number + 1)}>
Add one
</button>
<div>Number: {number}</div>
</div>
);
};

Concurrent-safe version of useLatest in React?

There's a commonly used utility hook "useLatest", which returns a ref containing the latest value of the input. There are 2 common implementations:
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
From https://github.com/streamich/react-use/blob/master/src/useLatest.ts
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useEffect(() => {
storedValue.current = current
})
return storedValue
}
From https://github.com/jaredLunde/react-hook/blob/master/packages/latest/src/index.tsx
The first version isn't suitable for React 18's concurrent mode, the second version will return the old value if used before useEffect runs (e.g. during render).
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
Here's my attempt:
function useLatest<T>(val: T): React.MutableRefObject<T> {
const ref = useRef({
tempVal: val,
committedVal: val,
updateCount: 0,
});
ref.current.tempVal = val;
const startingUpdateCount = ref.current.updateCount;
useLayoutEffect(() => {
ref.current.committedVal = ref.current.tempVal;
ref.current.updateCount++;
});
return {
get current() {
// tempVal is from new render, committedVal is from old render.
return ref.current.updateCount === startingUpdateCount
? ref.current.tempVal
: ref.current.committedVal;
},
set current(newVal: T) {
ref.current.tempVal = newVal;
},
};
}
This hasn't been thoroughly tested, just wrote it while writing this question, but it seems to work most of the time. It should be better than both versions above, but it has 2 issues: it returns a different object every time and it's still possible to be inconsistent in this scenario:
Render 1:
ref1 = useLatest(val1)
Create function1, which references ref1
Commit (useLayoutEffect runs)
Render 2:
useLatest(val2)
Call function1
function1 will use val1, but it should use val2.
Here is what I think is correct:
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useLayoutEffect(() => {
storedValue.current = current
})
return storedValue.current
}
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
The question doesn't actually explain what "this" means, i.e. how is useLatest called, and what purpose it fulfills in the application. So I'll have to guess for that ;) A somewhat realistic example would be very helpful.
In any case, it's probably useful to take a step back and ask if useLatest is the most suitable solution. If you find you don't need it, you also won't have to fix it.
With the way it works (depending on an effect to capture the value), it indeed won't play well with concurrent features. But even without them, it's an unreliable approach as the ref theoretically can change at any point, making renders unpredictable.
My guess of the use case is something similar to the proposed (and partially accepted) useEvent hook (GitHub PR).
function Chat() {
const [text, setText] = useState('');
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
Its purpose is to capture the latest render's scope, like useCallback, but without the need for dependencies. It does this by using an unchanging callback that internally calls the latest created callback, in a ref that is re-assigned on every render. That way passing that callback as a prop won't cause any renders by itself.
You can implement this yourself already, but the RFC mentions some open questions about this approach.
export function useEvent(handler) {
const latestHandlerRef = useRef();
useLayoutEffect(() => {
latestHandlerRef.current = handler;
});
// Never changing callback.
return useCallback((...args) => {
latestHandlerRef.current(...args)
}, []);
}
I also tested setting latestHandlerRef.current = handler directly in render instead of the layout effect. For now this seems to work as expected but that's just my use case. In the PR some doubt is expressed over assigning to a ref during render, though possibly these concerns don't really apply here, as the ref is only ever accessed in the callback.

React listen to 1st state change only

I am using useEffect in react to listen to redux(easy-peasy) state change, but I want to listen to 1st value change only.
Because when my page loads the state has a default value and then API call is made and hence data changes but the API is a polling API, hence it keeps getting the data again and again in a short interval of time. But one of my requirement is to listen only to the 1st API data.
This is what I tried:
1st Approach with empty dependency
useEffect(() => {
// my code
},[])
In this case, I get the default value of my state and not the 1st API response.
2nd Approach with state property in the dependency
useEffect(() => {
// my code
},[myState])
In this case, I keep getting the updated response from the API
both of these approaches didn't work for me. So please suggest a better solution.
You can do so using a ref variable and comparing the state with initial state (which could be null, undefined, empty object depending on your implementation):
const hasRun = useRef(false)
useEffect(() => {
if (!hasRun.current && myState !== initialState) {
hasRun.current = true
// run my code
}
},[myState])
A ref variable won't participate in re-rendering.
What I usually do with this is to have a "previous" state.
You can use this hook for to do that one:
const usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]); // only re-run if value changes
// return previous value (happens before update in useEffect)
return ref.current;
}
You can then do:
// I usually create a distinct initial state
const [var, setVar] = useState(null);
const prevVar = usePrevious(var);
useEffect(() => {
if (var !== prevVar && var !== null) {
// logic here
}
}, [prevVar, var]);
Yers you can simplify this by not using the usePrevious hook, but the hook is quite handy and can be used to check the previous value and the current one.

Looking for assistance in getting proper results from .filter()

I am trying to filter an array with a string that is input by user. The results are not updating properly with the first key input, then if the box is cleared or characters removed/changed, results that may now pass the filter are not being displayed.
The goal is to have all results displayed on initial page render, then properly updated with each keystroke.
Apologies; I'm just learning to code. Thanks for all assistance.
searchCompUsers = () => {
const newState = {}
const filteredEmps = this.props.employees.filter(
user => user.name.includes(this.state.searchName)
)
console.log(filteredEmps)
`` newState.filterEmps = filteredEmps
this.setState(newState)
}
empSearch = evt => {
const stateToChange = {};
stateToChange[evt.target.id] = evt.target.value;
this.setState(stateToChange);
this.searchCompUsers()
};
These lines are being run in sequence:
this.setState(stateToChange);
this.searchCompUsers();
...
const filteredEmps = this.props.employees.filter(
user => user.name.includes(this.state.searchName)
)
...
this.setState(newState);
I am assuming in your example, evt.target.id is searchName.
Two things you're doing here which you shouldn't do:
Running two setStates in sequence. This isn't necessarily a problem, but there's generally no reason for it and it could mean your code is structured poorly.
Referencing the state immediately after setState. setState is run asynchronously, so you can't guarantee the state will be updated by the time you reach your filter.
The weird results you're getting are probably stemming from (2).
Something like this would work better, assuming the rest of your code is fine:
empSearch = evt => {
const key = evt.target.id;
const value = evt.target.value;
if (key === "searchName") {
const filteredEmps = this.props.employees.filter(
user => user.name.includes(value);
);
this.setState({
filterEmps: filteredEmps
});
}
};
This way, you're only calling setState once per event, and you're not relying on the results of an earlier setState.
If you need to keep searchName in the state for some reason (such as using a controlled component), then you can simply add it to the same setState.
this.setState({
filterEmps: filteredEmps,
searchName: value
});
The only places you can assume the state is up-to-date is in the render() function, and in certain React lifecycle functions. You can also provide a callback to setState if necessary, though this should be relatively rare: this.setState({ ...someState }, () => { ...someCodeToRun() });

React useState hook event handler using initial state

I'm still getting my head around react hooks but struggling to see what I'm doing wrong here. I have a component for resizing panels, onmousedown of an edge I update a value on state then have an event handler for mousemove which uses this value however it doesn't seem to be updating after the value has changed.
Here is my code:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
The problem is with the handleResize function, this should be using the latest version of activePoint which would be a string top|left|bottom|right but instead is null.
How to Fix a Stale useState
Currently, your issue is that you're reading a value from the past. When you define handleResize it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
There are a several ways to solve this. First let's look at the most simple solution.
Create your function in scope
Your event listener for the mouse down event passes the point value to your resizerMouseDown function. That value is the same value that you set your activePoint to, so you can move the definition of your handleResize function into resizerMouseDown and console.log(point). Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown in another context.
See the in-scope function solution live on CodeSandbox.
useRef to read a future value
A more versatile solution would be to create a useRef that you update whenever activePoint changes so that you can read the current value from any stale context.
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
See the useRef solution live on CodeSandbox.
Addendum
It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.
You have access to current state from setter function, so you could make it:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
useRef for the callback
A similar approach to Andria's can be taken by using useRef to update the event listener's callback itself instead of the useState value. This allows you to use many up-to-date useState values inside one callback with only one useRef.
If you create a ref with useRef and update its value to the handleResize callback on every render, the callback stored in the ref will always have access to up-to-date useState values, and the handleResize callback will be accessible to any stale callbacks like event handlers.
function handleResize() {
console.log(activePoint);
}
// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;
// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());
With this in mind, we can also abstract away the creation and updating of the ref into a custom hook.
Example
See it live on CodeSandbox.
/**
* A custom hook that creates a ref for a function, and updates it on every render.
* The new value is always the same function, but the function's context changes on every render.
*/
function useRefEventListener(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
}
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
// We can use the custom hook declared above
const handleResizeRef = useRefEventListener((event) => {
// The context of this function will be up-to-date on every re-render.
console.log(activePoint);
});
function resizerMouseDown(event, point) {
setActivePoint(point);
// Here we can use the handleResizeRef in our event listener.
function handleResize(event) {
handleResizeRef.current(event);
}
window.addEventListener("mousemove", handleResize);
// cleanup removed for clarity
window.addEventListener("mouseup", cleanup);
}
return (
<div className="interfaceResizeHandler">
{resizePoints.map((point) => (
<div
key={point}
className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
onMouseDown={(event) => resizerMouseDown(event, point)}
/>
))}
</div>
);
});
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
Just small addition to the awe ChrisBrownie55's advice.
A custom hook can be implemented to avoid duplicating this code and use this solution almost the same way as the standard useState:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
For those using typescript, you can use this function:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
And call it like that:
const [
recordingState,
recordingStateRef,
setRecordingState,
] = useReferredState<{ test: true }>();
and when you call setRecordingState it will automatically update the ref and the state.
You can make use of the useEffect hook and initialise the event listeners every time activePoint changes. This way you can minimise the use of unnecessary refs in your code.
When you need to add event listener on component mount
Use, useEffect() hook
We need to use the useEffect to set event listener and cleanup the same.
The use effect dependency list need to have the state variables which are being used in event handler. This will make sure handler don't access any stale event.
See the following example. We have a simple count state which gets incremented when we click on given button. Keydown event listener prints the same state value. If we remove the count variable from the dependency list, our event listener will print the old value of state.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const clickHandler = () => {
console.log({ count });
setCount(c => c + 1);
}
useEffect(() => {
document.addEventListener('keydown', normalFunction);
//Cleanup function of this hook
return () => {
document.removeEventListener('keydown', normalFunction);
}
}, [count])
return (
<div className="App">
Learn
<button onClick={clickHandler}>Click me</button>
<div>{count}</div>
</div>
);
}
export default App;

Categories