Skip to content

Instantly share code, notes, and snippets.

@mimshins
Created July 28, 2025 09:22
Show Gist options
  • Save mimshins/c8bb6afe311e9909266f743c46b7c257 to your computer and use it in GitHub Desktop.
Save mimshins/c8bb6afe311e9909266f743c46b7c257 to your computer and use it in GitHub Desktop.
Flush custom event dispatch
import { flushSync } from "react-dom";
/**
* Flush custom event dispatch.
*
* React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.
*
* Internally, React prioritises events in the following order:
* - discrete
* - continuous
* - default
*
* https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350
*
* `discrete` is an important distinction as updates within these events are applied immediately.
* React however, is not able to infer the priority of custom event types due to how they are detected internally.
* Because of this, it's possible for updates from custom events to be unexpectedly batched when
* dispatched by another `discrete` event.
*
* In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.
* This utility should be used when dispatching a custom event from within another `discrete` event, this utility
* is not nessesary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.
* For example:
*
* dispatching a known click 👎\
* `target.dispatchEvent(new Event(‘click’))`
*
* dispatching a custom type within a non-discrete event 👎\
* `onScroll={(event) => event.target.dispatchEvent(new CustomEvent(‘customType’))}`
*
* dispatching a custom type within a `discrete` event 👍\
* `onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(‘customType’))}`
*
* Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use
* this utility with them. This is because it's possible for those handlers to be called implicitly during render
* e.g. when focus is within a component as it is unmounted, or when managing focus on mount.
*
* @param target The target to dispatch its event.
* @param event The event to be dispatched.
*/
const flushDispatchCustomEvent = <E extends CustomEvent>(
target: E["target"],
event: E,
) => {
if (!target) return;
flushSync(() => {
target.dispatchEvent(event);
});
};
export default flushDispatchCustomEvent;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment