Does the upcoming concurrent-mode break the old guarantee, that setState updates within a click handler are flushed synchronously at the event boundary?
If i have e.g. a button, that should only ever be pressed once, a supposedly working pattern was to "just set the state to disabled in the click handler":
let counter = 0;
const C = () => {
const [disabled, setDisabled] = React.useState(false);
const handler = React.useCallback(
() => { setDisabled(true); counter++; },
[], // setDisabled is guaranteed to never change
);
return (<button onClick={handler} disabled={disabled}>click me</button>);
};
// Assert: `counter` can never be made >1 by clicking the button with one C
This pattern used to be guaranteed to work (at least given that setting the disabled-attribute prevents any further click events, which seems to be the case). The biggest related question i could find discusses this, and also shows a more or less obvious alternative (and easier to prove it works), of using a ref (unlike the answer in the linked question, maybe rather a boolean ref, but same idea, it's always sync).
Side questions: Is this information up-to-date, or did something change? It's more than three years old after all. It mentions "interactive events (such as clicks)", what are the others?
However, in concurrent-mode, rendering can be paused, which i interpret as "the js thread will be released", to allow potential key presses or whatever events to trickle in, and in that phase, additional click events could also happen, before the next render disables the button. Is therefore the way to go to use some kind of ref, or maybe explicitly adding ReactDOM.flushSync?
My current understanding of how concurrent mode works is this:
1 - a re-render starts
2 - hooks are called, they change internal state
3a - re-render is suspended
4a - internal state changes are rolled back
OR
3b - re-render is not suspended
4b - internal state changes are commited
useCallback is a thin wrapper over useMemo and uses "internal state" to save the cached value. (4a) is the key here, and from what I understand your solution is not guaranteed to work anymore.
The useRef (with a boolean flag value) solution has the same issue too because you're not guaranteed that the new value of the ref is actually going to be "commited" when re-rendering is suspended.
The useRef solution where you keep a ref to the DOM button element and directly manipulate the disabled attribute will still work even in concurrent mode. React has no way of blocking you from directly manipulating DOM.
"suspending" means reverting "internal state" + not applying the generated DOM manipulations, does not mean any side effects (like manipulating DOM directly) can be affected.
flushSync will not help either, it simply forces re-renders, does not guarantee that the current render won't be suspended.
As far as I know the setState call was always async, and you never had a way to warranty that the button will be disabled right after the click. Also there is no such thing as concurrency in JS, it has single thread, the problem is that the render can happen latter than you expect so you can receive another click until React made re-render for you.
If you need to fire the logic only once I would advice to use useRef hook and when you need to make sure that we have not clicked the button just check the value.
const isDisabled = useRef(false);
const onClick = () => {
if (!isDisabled.current) {
isDisabled.current = true;
}
}
Related
After some trial I discovered following problem occurs in strict mode.
I would be interested if someone can explain why.
Take this simple example where inside render I am just scheduling a timeout which updates state:
Example:
let firstRender = true; // Normally I would use ref but I was playing with example
export default function App() {
let [data, setData] = React.useState({ name: 'Nick' });
// Schedule a timeout on first render
if (firstRender) {
setTimeout(() => {
console.log('Running');
setData((ps) => ({
...ps,
name: 'Paul',
}));
}, 1000);
}
console.log('Running render');
firstRender = false;
return (
<div>
<h1>{data.name}</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
}
If you run this example without Strict mode, then you will see "Paul" on screen after one second, as I was expecting.
If you use Strict mode, it will always show "Nick" on screen. Idea why?
Note: it seems using useRef instead of the global variable firstRender fixes this problem also in Strict mode. This seems to happen because ref was set in first render, and its value also got discarded (see also the answer).
This is due to the fact that strict mode intentionally invokes your function component body twice (when in dev mode) to help spot unintended side effects.
On the second invocation, your firstRender variable is false so your setTimeout doesn't run.
Important to note that this second invocation isn't just a re-render like you'd get from a state update. It's a second invocation of the entire component body. State is not preserved. React invokes your component function once, discards the result, and invokes it a second time to get the output.
From the docs:
Because the above methods might be called more than once, it’s important that they do not contain side-effects.
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
Function component bodies
Q. it seems using useRef instead of the global variable firstRender
fixes this problem also in Strict mode. Curious why that's the case?
According to React Docs,
Ref is also useful to keep some mutable value around. The value stores in ref, will not change during subsequent re-renders, unless explicitly changed.
Therefore, for values you don't want to change (maybe some expensive calculation) or for other reasons, use useRef.
The “ref” object is a generic container whose current property is
mutable and can hold any value, similar to an instance property on a
class.
This works because useRef() creates a plain JavaScript object. The
only difference between useRef() and creating a {current: ...} object
yourself is that useRef will give you the same ref object on every
render.
Keep in mind that useRef doesn’t notify you when its content changes.
Mutating the .current property doesn’t cause a re-render. If you want
to run some code when React attaches or detaches a ref to a DOM node,
you may want to use a callback ref instead.
Imagine situation:
const [value, setValue] = useState(false);
const setSomething = (val) => {
setValue((prev) => {
fn(); dispatch(action); // or any other side effect
return prev + val;
});
};
Is it programmatically okey and fine with react principles to call side effects inside useState callback? May it affect the render process somehow?
It is not ok to use side effects inside the updater function. It might affect the render process, depending on the specific side effect.
It is not fine with react principles (separation of concerns, declarative code).
(I remember to have seen some exceptional use cases where putting some code inside the updater function was said to be the only solution, but I can't remember what it was. I'd appreciate an example in the comments.)
1. Consequences of using side effects
It is not ok to use side effects, basically for the same reasons why you shouldn't use side effects outside useEffect anywhere else.
Some side effects might affect the render process, other side effects might work fine (technically), but you are not supposed to rely on what happens inside the setter functions.
React guarantees that e.g. if you call setState( prev => prev + 1 ), then state would now be one more than before.
React does not guarantee what will happen behind the scenes to achieve that goal. React might call these setter functions multiple times, or not at all, and in any order:
StrictMode - Detecting unexpected side effects
... Because the above methods might be called more than once, it’s important that they do not contain side-effects. ...
2. following react principles
You should not put side effects inside the updater function, because it validates some principles, like separation of concerns and writing declarative code.
Separation of concerns:
setCount should do nothing but setting the count.
Writing declarative code:
Generally, you should write your code declarative, not imperative.
I.e. your code should "describe" what the state should be, instead of calling functions one after another.
I.e. you should write "B should be of value X, dependent on A" instead of "Change A, then change B"
In some cases React doesn't "know" anything about your side effects, so you need to take care about a consistent state yourself.
Sometimes you can not avoid writing some imperative code.
useEffect is there to help you with keeping the state consistent, by allowing you to e.g. relate some imperative code to some state, aka. "specifying dependencies".
If you don't use useEffect, you can still write working code, but you are just not using the tools react is providing for this purpose. You are not using React the way it is supposed to be used, and your code becomes less reliable.
Examples for problems with side effects
E.g. in this code you would expect that A and B are always identical, but it might give you unexpected results, like B being increased by 2 instead of 1 (e.g. when in DEV mode and strict mode):
export function DoSideEffect(){
const [ A, setA ] = useState(0);
const [ B, setB ] = useState(0);
return <div>
<button onClick={ () => {
setA( prevA => { // <-- setA might be called multiple times, with the same value for prevA
setB( prevB => prevB + 1 ); // <-- setB might be called multiple times, with a _different_ value for prevB
return prevA + 1;
} );
} }>set count</button>
{ A } / { B }
</div>;
}
E.g. this would not display the current value after the side effect, until the component is re-rendered for some other reason, like increasing the count:
export function DoSideEffect(){
const someValueRef = useRef(0);
const [ count, setCount ] = useState(0);
return <div>
<button onClick={ () => {
setCount( prevCount => {
someValueRef.current = someValueRef.current + 1; // <-- some side effect
return prevCount; // <-- value doesn't change, so react doesn't re-render
} );
} }>do side effect</button>
<button onClick={ () => {
setCount(prevCount => prevCount + 1 );
} }>set count</button>
<span>{ count } / {
someValueRef.current // <-- react doesn't necessarily display the current value
}</span>
</div>;
}
No, it is not ok to issue side-effects from a state updater function, it is to be considered a pure function.
The function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams), and
The function application has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).
You may, or may not, be using the React.StrictMode component, but it's a method to help detect unexpected side effects.
Detecting unexpected side effects
Conceptually, React does work in two phases:
The render phase determines what changes need to be made to
e.g. the DOM. During this phase, React calls render and then
compares the result to the previous render.
The commit phase is
when React applies any changes. (In the case of React DOM, this is
when React inserts, updates, and removes DOM nodes.) React also calls
lifecycles like componentDidMount and componentDidUpdate during
this phase.
The commit phase is usually very fast, but rendering can be slow. For
this reason, the upcoming concurrent mode (which is not enabled by
default yet) breaks the rendering work into pieces, pausing and
resuming the work to avoid blocking the browser. This means that React
may invoke render phase lifecycles more than once before committing,
or it may invoke them without committing at all (because of an error
or a higher priority interruption).
Render phase lifecycles include the following class component methods:
constructor
componentWillMount (or UNSAFE_componentWillMount)
componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
componentWillUpdate (or UNSAFE_componentWillUpdate)
getDerivedStateFromProps
shouldComponentUpdate
render
setState updater functions (the first argument) <--
Because the above methods might be called more than once, it’s
important that they do not contain side-effects. Ignoring this rule
can lead to a variety of problems, including memory leaks and invalid
application state. Unfortunately, it can be difficult to detect these
problems as they can often be non-deterministic.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState) <--
Functions passed to useState, useMemo, or useReducer
Take a cue from the two highlighted bullet points regarding the intentional double-invoking of state updater functions and treat the state updater functions as pure functions.
For the code snippet you shared, I see no reason at all for the functions to be called from within the updater callback. They could/should be called outside the callback.
Example:
const setSomething = (val) => {
setValue((prev) => {
return prev + val;
});
fn();
dispatch(action);
};
I would not
Just because it works doesn't mean it's a good idea. The code sample you shared will function, but I wouldn't do it.
Putting unrelated logic together will confuse the next person who has to work with this code; very often, that "next person" is you: you, six months from now, after you've forgotten all about this code because you finished this feature and moved on. And now you come back and discover that some of the silverware has been stored in the bathroom medicine cabinet, and some of the linens are in the dishwasher, and all the plates are in a box labeled "DVDs".
I don't know how serious you are about the specific code sample you posted, but in case it's relevant: if you're using dispatch that means you've set up some kind of reducer, either with the useReducer hook, or possibly with Redux. If that's true, you should probably consider whether this boolean belongs in your Redux store, too:
const [ value, setValue ] = useState(false)
function setSomething(val) {
fn()
dispatch({ ...action, val })
}
(But it might not, and that's fine!)
If you're using actual Redux, you'll also have action-creators, and that's generally the correct place to put code that triggers side effects.
Regardless of whatever state tech you're using, I think you should prefer to avoid putting side-effect code into your individual components. The reason is that components are generally supposed to be reusable, but if you put a side-effect into the component that is not essential to display or interaction of the thing being visualized by the component, then you've just made it harder for other callers to use this component.
If the side-effect is essential to how this component works, then a better way to handle this would be to call setValue and the side-effect function directly instead of wrapping them up together. After all, you don't actually depend on the useState callback to accomplish your side-effect.
const [ value, setValue ] = useState(false)
function setSomething(val) {
setValue(value + val)
fn()
dispatch(action)
}
The useEventListener hook and the useKeyPress seem to have a slightly different implementation, but I'm trying to figure out which is a better tool to use for my specific use case.
I have a custom dropdown select menu, and I want to listen to the arrow down, up and enter keys. My problem, or rather question, about the useKeyPress hook, is that there are two renders that happen + I'm not really sure why there is an intermediate useState.
For instance, using the useKeyPress hook, if a user click on a down arrow, the event listeners fire off twice, one would return true, and immediately return false onKeyUp:
useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, []);
Also, I'm not sure why it's doing,
const [keyPressed, setKeyPressed] = useState(false);
I'm just looking for some clarification on the difference between these two, and which one to use for my use case.
React works by updating state. If the useKeyPress hook didn't update some internal state then it would be incapable of returning an updated value reference and trigger a component rerender. useKeyPress listens for the keyDown event to toggle state to true, then listens for the keyUp event to clear that state (you don't want it stuck true.
The useEventListener is certainly a heavier tool; it's capable of monitoring many more events. It doesn't use internal state though, but rather requires callback handlers to be passed to it to be invoked when registered event occurs.
For a lightweight utility the onKeyPress is useful if you just need to trigger an effect callback in a component, but for larger projects or where you want more than to know a keyPress occurred, useEventListener can handle it.
Performance shouldn't be your primary concern as both are fast enough for nearly every use case they cover.
useKeyPress is probably not well suited for your use case as it only gives you information of when a key is pressed. It doesn't let you call a handler. It internally keeps track of the pressed state itself. But you likely want to call a handler, e.g. to change the currently selected item in your dropdown list.
Also, I'm not sure why it's doing,
const [keyPressed, setKeyPressed] = useState(false);
That's simply the use case. It keeps track of the pressed state of a button.
useEventListener is a more generic tool that lets you do the updating and state management yourself. It lets you decide what to update based on the subscribed events. You could in fact rewrite useKeyPress to internally use useEventListener.
Update: Here is an example Pen
https://codepen.io/anon/pen/vwzGYY?editors=0011
Preface
Based on my research, it seems like I need a completely different approach. Maybe you can suggest one?
Context
I'm using a Redux-Form (technically an older version, but the API's
in question seem really stable. We can burn that bridge when we get there.) to set some "filters" for a sort of search results list.
In particular, since I want the pages to be link-able, I'm also setting the form content in the URL query params, via React-Router, or initially setting it on page load via similar mechanism.
The only field so far is "organization_name", a text field, used to set the query param value, and trigger an API request for /endpoint?name={some_name}.
E.g.,
<Field
name="organization_name"
component="input"
type="text"
placeholder="Organization Name"
value={value}
/>
I've tried several things, but here's a recent shot:
I'm grabbing reset, change, and other things from default props. I'm passing in a handleSubmit as required.
handleSubmit works correctly, to do some state updating, set/push the URL query params with React Router, and then make a new API call/update display of new results! Woot!
What I want / expect
In the long run, I would like a "reset filters" button that sets all filter values back to defaults (e.g., set the "name" value to empty string), and re-submits the form (thus triggering handleSubmit).
What I first tried to implement was a button, as such:
<button
name="reset_filters_button"
type="button"
onClick={resetAndSubmit}
disabled={pristine || submitting}
>
Clear Search
</button>
Where resetAndSubmit is defined on the form container as such:
const resetAndSubmit = event => {
reset();
handleSubmit();
};
What actually happens... (submit takes precedence over dispatched events?)
Using the Chrome dev tools debugger, I can clearly see that the reset method is called, and returns it's dispatch(...)'d event. However, the form and state values are not updated before handleSubmit() runs and submits the form.
I think this might have to do with the submit event taking priority?
I have also tried something janky, like importing change (default prop for the container) and defining the reset button thus:
<button
name="reset_filters_button"
type="button"
onClick={() => {
change('organization_name', '');
methodThatDispatchesSubmitAction();
}}
disabled={pristine || submitting}
>
Clear Search
</button>
Which (if I remove methodThatDispatchesSubmitAction()) works correctly to set the field value back to blank, making the form technically "pristine" again as well.
methodThatDispatchesSubmitAction() (if it's not obvious) is bound on the parent via dispatchToProps, and passed in to the form container, where it uses the "remote submit" suggestion, e.g,
// organization_list_filter == name of the Redux-Form to submit.
dispatch(submit('organization_list_filter'));
TL;DR and final question:
How does one properly reset a form and submit its' default/empty values?
Every time I dispatch or directly call Redux Form 'submit', it ends up submitting the form before clearing values from state, or the UI. I have walked through this with a debugger and it's not skipping my call to reset or change. It's like an async/race issue, but I admit I am out of my league in this particular case for sure.
Am I just Straight Up Doing It Wrong?
It is most definitely a race condition issue (or since we aren't actually dealing with threads, an order of events issue).
The reason using a methodThatDispatchesSubmitAction works when your current example does not, is because a dispatched action has the benefit of reading data directly from the redux store. Your example is not reading from the redux store, it's reading from a property that is passed in. Yes, this property comes from the redux store, but the problem you are seeing is that it hasn't been updated in your component yet.
Bear with me as this next piece is not going to be entirely accurate but it should suffice to explain what you are seeing.
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is fired using values prop (old state data still)
Component is updated with new props from redux state
As you can see, the order of events don't allow for an updated state to be given to the property until our click code has finished running. If you've ever watched a video on the JS Event Loop (I highly recommend it), you'll know that our onClick handle will run in full before any other async operations (or sync operations that come after our click) have a chance to run.
There are good reasons why Components aren't given updated props right away but the primary one is performance. You can see that this order is in fact the problem by wrapping the handleSubmit in an async event that fires immediately (it doesn't actually fire immediately, all other sync/async operations queued before it will finish).
const resetAndSubmit = (event) => {
reset();
setImmediate(() => handleSubmit());
}
This changes the order of events as follows:
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is queued on the event loop (not run yet)
Component is updated with new props from redux state
Event loop reaches queued code and runs is
-> Handle submit is fired using values prop (new state data)
Hopefully, this helps you understand why the problem is occurring. As for solutions to fix it. Obviously, you can queue the handle submit as I've shown above. Another option would the one you've described as using a dispatch to perform the submit. A third option would be to use something a bit heavier like redux-thunk or redux-sagas that tie the resetAndSubmit action into a single dispatch. Although honestly, this is the same as option two, just reduced into a single dispatch. Option four, don't use redux for all your data. Obviously, this fourth option comes with trade-offs but my point being, just because you are using redux in a project doesn't mean every single piece of data needs to be in redux. Though it completely defeats the purpose of redux-forms.
I should also add, you are not alone in being confused by this. When you introduce redux, it messes with how you traditionally think about working with code. Normally you think, I do A then B. But with redux, you do A, wait for A's changes to make it through the system, and then you do B. That's where Sagas or Thunks can be nice. You move more logic to the store to act on the dispatch rather than wait for it to all make its way back down to a component via props.
I have researched synthetic events in React and I understand that React pools events to improve performance. I am also aware that events in React are not DOM events. I have read several posts and threads about this topic but I cannot find any mention of calling preventDefault after calling event.persist.
Many sites have mentioned that if we want to capture the value of event.target, for example, that one option is simply to cache it for later use but this does not apply to my use case.
I want to throttle an event handler that is listening for onDragOver events. In order to do this in React, I have to pass the event through 3 functions, calling event.persist on the first one so that the last one can see it.
However, event.preventDefault has no effect when I do call it. It's almost as if once we call event.persist, that's it and there's no turning back.
Below is some of the code but you may find it more helpful to experiment with it on StackBlitz.
import React, { Component } from 'react';
import { throttle } from 'throttle-debounce';
import DropItem from './DropItem';
class DropZone extends Component {
constructor(props) {
super(props);
this.onDragOverThrottled = throttle(500, this.onDragOver);
this.onDragStart = this.onDragStart.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
}
onDragStart(e, id) {
console.log('dragstart: ', id);
e.dataTransfer.setData("id", id);
}
onDragOver(e) {
e.preventDefault(); // this does nothing if event.persist fires before it
console.log('dragover...');
}
handleDragOver(e) {
e.persist();
this.onDragOverThrottled(e);
}
render() {
const items = this.props.items.map((item, index) => {
return <DropItem item={item} key={index} onDragStart={this.onDragStart} />;
});
return (
<div
className={this.props.class}
//onDragOver={this.handleDragOver} // See note 1 below
onDragOver={this.onDragOver} // See note 2 below
onDrop={(e) => {this.props.onDrop(e, this.props.location)}}>
<span className="task-header">{this.props.title}</span>
{items}
</div>
);
}
}
export default DropZone;
/*
NOTE 1
Commenting in this line shows that throttling works but preventDefault does not and we cannot drag and drop any box to another location.
NOTE 2
This skips throttling altogether but preventDefault does work which allows the box to be moved to a different area. Because throttling is disabled here, onDragOver fires a lot and, at times, keeps the user from moving boxes around quickly.
*/
All of the sources I have consulted have effectively implemented either a debounce or throttle to capture a value and then do something with that value but none of them have tried calling preventDefault after persist as I am attempting to do. Some of these sources are the following:
Blog post on throttling and debouncing
Example of throttling an input by peterbe
A fiddle that does exactly what I am aiming for but it is not written in React
After further research and experimentation, I found out how to resolve this.
TL;DR
My theory that event.persist() somehow prevented event.preventDefault() from working as expected was incorrect.
THE REAL PROBLEM
The reason that my drag-and-drop app did not work with throttling was because event.persist() does not forward the event to another handler but simply makes it available for other handlers to access. This means that event.preventDefault() must be called on each handler that uses the event. This seems very obvious now that I say it but because I had to send the event through multiple handlers to implement throttling, I mistakenly thought I was passing the event from one to the other.
DOCUMENTATION
What I state above is my observation and does not come from React's official documentation. But the React docs do say this:
If you want to access the event properties in an asynchronous way, you
should call event.persist() on the event, which will remove the
synthetic event from the pool and allow references to the event to be
retained by user code.
Although I had read this before, I skipped over it because I did not consider what I was doing to be asynchronous. But the answer is still here– it allows references to the event to be retained by user code.
LEARN MORE
For those who want to dig into this deeper, be sure to look at the README in my StackBlitz where I provide further details.