RFC: useEvent by gaearon · Pull Request #220 · reactjs/rfcs

18 min read Original article ↗

Conversation

@gaearon

fdev, kachkaev, kimjoar, dan-lee, pr0da, Janpot, Bnaya, bvaughn, eps1lon, nikparo, and 808 more reacted with thumbs up emoji fidaay reacted with thumbs down emoji focux, MateusAndrade, electron-space, IsaiahByDayah, presto2116, alejandrovasta, OlegLustenko, DanAndreasson, pascalgagneur, mrousavy, and 46 more reacted with laugh emoji sophiebits, hamlim, nikparo, gioragutt, Thebarda, MakChan, hamatoyogi, ergenekonyigit, airjp73, slorber, and 199 more reacted with hooray emoji ryota-murakami, Temirtator, cgarrovillo, jvzaniolo, and soujvnunes reacted with confused emoji sophiebits, hamlim, gioragutt, Thebarda, MaeIg, heyitsarpit, slorber, btiwaree, gortron, eelkevdbos, and 267 more reacted with heart emoji mzaien, gupta-ji6, TSMMark, itsMapleLeaf, focux, cb-jake, vojtech-dobes, ahmetuysal, PointSingularity, bc-shawnyap, and 137 more reacted with rocket emoji worldwidebaby, coderitual, bradpurchase, homam, gregbty, tgrassl, Tom-Bonnike, leenephi, ceremonious, coder-shanks, and 96 more reacted with eyes emoji

This was referenced

May 4, 2022

@windmaomao

Finally something easy to think of is implemented as it seems. 👍🏻

@ds300

Great idea ❤️

My main suggestion would be a different name. useEvent is not a very precise way to convey what this hook does. I've seen it implemented in other codebases as useStableCallback, which I slightly prefer, although I see that you've already dismissed it as being too verbose in the RFC.

third774, so1ua, ljharb, benduran, sompylasar, BigDog1400, drcmda, NachoCasta, dan-codes-16, hanabalon, and 23 more reacted with thumbs up emoji Glinkis, koenoe, nahumzs, fegan104, youphenrique, thecodesalim, frankzang, mayconmesquita, mfaridzia, smithbm2316, and 64 more reacted with thumbs down emoji

@Merri

Or maybe just name this useCallback and make it work for both cases, but deprecate the passing of dependencies and remove some version later.

Unless someone can figure out any valid use case for the old useCallback functionality with deps.

panta82, jpage-godaddy, conradob, villesau, so1ua, ljharb, petercr, daradermody, flq, FezVrasta, and 72 more reacted with thumbs up emoji flaviendelangle, mayconmesquita, crutchcorn, jchitel, Pajn, chabbershaw, juanchomdiaz, bnsngltn, and Joh4nnesHartl reacted with thumbs down emoji flq, focux, scottrippey, panta82, luxferoo, stv8, and daniele-orlando reacted with heart emoji

@windmaomao

As for the name, IMHO, react hook is never named for tech purpose, more in the business direction. If i really want to be picky, i can think of useGlobal or useStatic.

pBread and benknab reacted with thumbs up emoji prichodko, andrewpaulino, dmahajan980, RexGalilae, dutziworks, Asjas, TchernyavskyDaniil, fmoliveira, MatthijsKok, IndyVC, and 55 more reacted with thumbs down emoji

@panta82

I've been using this all over the place. I call it useLatestClosure. It's definitely the missing piece of React.

I don't like useEvent naming. IMO using these as events will be a very minor part of usage pattern, just an optimization in the largest codebases. Mostly it will be used when you need to pass a function somewhere where you don't want to trigger re-render.

Give the useEvent behavior to useCallback. We don’t want to do this because they have sufficiently different semantics.

I think this would be a better change. I almost always want to use useLatestClosure (or useEvent) and almost never useCallback with an argument list.

As Merri said, useCallback should just adopt this functionality when called without deps. And deps usage should be an advanced optimization use case.

colemars, MaeIg, sompylasar, AlexPoirier1, scottrippey, dmail, mpenney99, Bunkerbewohner, sarimarton, hednowley, and 76 more reacted with thumbs up emoji

@nikparo

Great to see a proposal for this! I'm left wondering though what type of footguns you have identified related to using event functions during render? Out of date callbacks?

Same proposal, but allow calling event handlers during rendering. We think this creates too many footguns.

If calling event handlers during rendering was allowed, then would the event handler be up to date or not? I.e. would isCurrent in the example below always be true? If that is a possibility, then I think it warrants serious consideration.

  const [value, setValue] = useState(0);
  const getCurrentValue = useEvent(() => value);

  // always true or sometimes false?
  const isCurrent = value === getCurrentValue();

@kocyigityunus

I didn't like the useEvent naming too. maybe split it into two different hooks.

  1. useChange => with dependencies, since it re-runs when one of the dependencies changes.
  2. useFunction or useFunctionRef => without dependencies. since it's basically useRef for a function.

@franciscop

To be implementation-clear, the behaviour proposed here is not the same as this, right?

// NOT the proposal? Since it would have stale props
const useEvent = (cb) => {
  const wrapper = useCallback(cb, []);
  return wrapper;
};

Nevertheless, text inside useEvent will reflect its latest value

It'd be more like this (+some err handling), which means the wrapper is always stable but inside keeps the latest ref to the callback and calls that when invoked:

const useEvent = (cb) => {
  const ref = useRef(cb);
  useEffect(() => {
    ref.current = cb;
  }, [cb]);
  const wrapper = useCallback((...args) => {
    ref.current(...args);
  }, []);
  return wrapper;
};

@alvarlagerlof

Please Keep the name. It's far less abstract than the other proposals, which will help a lot of non-native speakers.

stefanprobst, judicaelandria, koenoe, TkDodo, bancek, lubieowoce, nahumzs, bradpurchase, blvdmitry, fegan104, and 92 more reacted with thumbs up emoji larixk, alisey, daxaxelrod, lucien-perouze-cezembre, janigowski, DesignByOnyx, floroz, jwalton, and alimertcakar reacted with thumbs down emoji andycarrell, nalejandroveron, and Joh4nnesHartl reacted with heart emoji

@rubennorte

@franciscop

What happens when the event is asynchronous?

This is a big usecase IMHO and a big pain point in current React's useEffect where you cannot do useEffect(async () => {...});. I see little mention of this very typical usecase (only "events should usually not be asynchronous"), what happens here? Is this valid?

const onClick = useEvent(async e => {
  ...
});

I think for a Hook that is going to be used in a highly-async environment, it'd be nice to have some async help in-place. Like, at the very least allow the callback to be async by design.

Disclaimer: I've written part of use-async-effect and all of use-async 😬

kocyigityunus, voluntadpear, xdumaine, focux, reitlepax, incepter, silverhero13, dmahajan980, RexGalilae, liamfd-titan, and 35 more reacted with thumbs up emoji RexGalilae, cpatti97100, and johnson-liang-appier reacted with rocket emoji

@rubennorte

Sorry to continue with the bikeshedding on the name of the hook, but I think my perspective is slightly different from what has been shared already.

I think the concept of "event" aligns well with what these functions are intended for. Given they're not meant to be used during render (they can only be used when the tree has been committed) the only moment when they can actually execute is during an event (a user interaction event, a network event, a timer event, etc.).

My only concern is about calling it just useEvent because the name might suggest this is to subscribe to events, like:

useEvent('click', () => {
  // do something
});

Which isn't what it does.

In order to avoid this confusion, I think we should use something like useEventHandler or useEventCallback.

tefkah, mihkeleidast, koenoe, sompylasar, anna-kryva, Macil, xdumaine, focux, jordandobson, blvdmitry, and 126 more reacted with thumbs up emoji

@so1ua

useFunction is better :) Especially that as I understand useCallback will almost not be used later

franciscop, cncolder, and babur001 reacted with thumbs up emoji jamiebuilds, xdumaine, TSMMark, tobias-tengler, flaviendelangle, himself65, connor-baer, dprgarner, SferaDev, mayconmesquita, and 44 more reacted with thumbs down emoji tefkah, voluntadpear, lucien-perouze-cezembre, and cncolder reacted with heart emoji

@rubennorte

What happens when the event is asynchronous?

This is a big usecase IMHO and a big pain point in current React's useEffect where you cannot do useEffect(async () => {...});. I see little mention of this very typical usecase (only "events should usually not be asynchronous"), what happens here? Is this valid?

const onClick = useEvent(async e => {
  ...
});

I think for a Hook that is going to be used in a highly-async environment, it'd be nice to have some async help in-place. Like, at the very least allow the callback to be async by design.

Disclaimer: I've written part of use-async-effect and all of use-async 😬

@franciscop what kind of help are you thinking about here? What do you think React should do differently when passing an async function instead of a sync one, considering React just returns a stable version of the function but doesn't really call that function itself?

@ScottAwesome

First and foremost, this solves a huge pain point for the community. I love it so much. The over proliferation of useCallback and associated bugs are solved pretty concisely in this addition.

In terms of naming: I think you could call it useEventHandler. I see the purpose of useCallback being useful for things where you need data invalidation to change the function signature, like when interacting with results from an API. I does become more of an edge case tool but that's kind of the point? Arguably, one could just use useMemo for complex cases too, but the simplicity of useCallback for non event handling cases is nice.

Perhaps clarify guidance around async functions, e.g. if you are using an async action it might be better to treat it as a Promise rather than using async and await?

dpobel, blvdmitry, breytex, electron-space, landisdesign, dmahajan980, PupoSDC, lifeisfoo, StephenHaney, mrsekut, and 2 more reacted with thumbs up emoji

@franciscop

I'm thinking to at least allow the function to be async, since if it's async in useEffect even when the return value is not used officially it'll give you a strong warning an error. Like, as far as it doesn't get in the way of that I'd be happy already.

What I was thinking as active help is that if this function (which doesn't seem to be the case from the RFC, but the name is slightly confusing) was supposed to be to manage only events like onClick it could also help by calling event.persist() by default. Though I'm just reading that it seems that event.persist() is useless since React 17 and I didn't know that, so there might be nothing else to do here!

Edit: I should re-read the docs more often, React keeps getting better in small ways (not needing e.persist()) and I didn't even notice.

@sompylasar

I'd also argue strongly against useEvent name because an "event" is something that happens, but what we're making with this hook is a callback function which is supposed to handle/process the happened event. Suggest more specific useEventCallback or useEventHandler instead, even though it's longer to type. I would avoid going as abstract as useLatestClosure or useFunction.

landisdesign, dmahajan980, yangshun, isocroft, lifeisfoo, LankyLou, simondriesen, rykala, andycarrell, StephenHaney, and 12 more reacted with thumbs up emoji

@mrcljx

In particular, we're worried that this proposal creates a strong incentive to wrap every function that currently uses useCallback into useEvent. However, that wouldn't lead to right semantics where the function identity is important.

In case this datapoint is helpful for the other RFCs, we implemented a slightly modified version of the proposed useEffect to help with the semantics.

Implementation:

  1. The implementation is the user-space implementation.
    1. Future: We'll probably switch to the experimental implementation.
  2. Our signature requires that the callback's return type must be (async) void.
    function useEvent<T extends (...args: any[]) => void | Promise<void>>(callback: T): T;
    
    1. A useEventWithResult variant exists that allows non-void results. It was deliberately picked to be "longer" to write and comes with a big "be sure that this isn't a case for useCallback" warning.
    2. Alternatives considered: We originally planned to write an ESLint rule to ensure that the variables that useEvent results are stored in are named {on,handle,set}XYZ, however solving this through seemed to better fit the semantics.
    3. Future: we might be a bit less strict about Promise and actually allow void | Promise<unknown>.
  3. We forked eslint-plugin-react-hooks to treat the return values of these new hooks as stable when it comes to exhaustive-deps.
  4. We then went through our codebase and replaced each const {on,handle,set}XYZ = useCallback with useEvent. A few cases were pointed out where the callback actually returned a value and needed to be replaced useEventWithResult.

Learnings:

  1. Our codebase now roughly has a 1:4 share of useCallback to useEvent (120 vs 430).
    1. It means in those 430 cases we were able to remove the deps array.
    2. Because of the void-constraint we are very sure that this was correct semantically.
  2. The term useEvent is still a bit odd.
    1. Intent is a bit unclear.
    2. Conflict with the concept of "events" (we had to renamed a different hook to useDOMEvent).
    3. Future: If React would not implement this RFC, we'd consider switching to useHandler (or useEventHandler) and useHandlerWithResult as a nod to the class-based component convention of naming event handlers (fun fact: in C# an EventHandler is required to be void as well).

@jampy

Our signature requires that the callback's return type must be (async) void.

IMHO the question useCallback vs useEvent is not about the return type.

As @gaearon says:

In most cases, only the final consuming side (e.g. the Effect from which the event function is called) can be "sure" whether it "cares" about the reactivity of the function value.

I second that. It's not about the implementation. It's about the consumer.

rickhanlonii added a commit to facebook/react that referenced this pull request

Oct 5, 2022
This commit adds a new hook `useEvent` per the RFC [here](reactjs/rfcs#220), gated as experimental. 

Co-authored-by: Rick Hanlon <rickhanlonii@gmail.com>
Co-authored-by: Rick Hanlon <rickhanlonii@fb.com>
Co-authored-by: Lauren Tan <poteto@users.noreply.github.com>

@yolio2003

what about useNonReactiveHandler

@sgarcia-dev

@mrcljx

@sgarcia-dev Yes, this is our patch (we also patched in useLatest which returns a Ref like useRef).

         }
         const id = def.node.id;
         const { name } = callee;
-        if (name === "useRef" && id.type === "Identifier") {
+        if (name === "useLatest" && id.type === "Identifier") {
+          // useLatest() return value is stable.
+          return true;
+        } else if (name === "useEvent" && id.type === "Identifier") {
+          // useEvent() return value is stable.
+          return true;
+        } else if (name === "useRef" && id.type === "Identifier") {
           // useRef() return value is stable.
           return true;
         } else if (name === "useState" || name === "useReducer") {

pawelgrimm added a commit to Doist/typist that referenced this pull request

Jan 13, 2023
The `useEvent` function was being called during rendering,
which throws an error when React's Strict Mode is enabled. This
type of usage is incorrect because it makes the rendering result
non-determinisitic.

See reactjs/rfcs#220 (comment)

davismcphee added a commit to elastic/kibana that referenced this pull request

Jan 18, 2023
…146352)

## Summary

This PR includes a number of updates to Unified Histogram to prepare for
sharing with solutions, as well as updating its usage in Discover:

Unified Histogram:
- Adds a `disableAutoFetching` prop, similar to Unified Field List, to
disable automatic Unified Histogram refetching based on prop changes.
- Accepts an `input$` observable prop that can be used to control manual
refetches.
- Removes `refetchId` used internally and replaces it with an
observables based approach to simplify refetch logic.
- Introduces a `use_stable_callback` utility hook to create callback
functions with stable identities which simplifies `useCallback` logic —
should be replaced with `useEvent` or whatever the React team comes up
with to solve this specific issue when available:
reactjs/rfcs#220.
- Eliminates debouncing logic in Unified Histogram since it was hacky —
manual refetching should be used instead if debouncing is needed.
- Accepts `query`, `filters`, and `timeRange` props to remove
dependencies on specific services.
- Updates `use_request_params` to export `getTimeRange` and
`updateTimeRange` functions for easier absolute time range handling.
- Exposes some Lens embeddable props to allow further customizing
Unified Histogram behaviour.

Discover:
- Exports a `fetch$` observable from `use_saved_search` to allow
listening for when data fetches should be triggered.
- Uses new manual refetching support in Unified Histogram to simplify
refetching logic.
- Passes `query`, `filters`, and `timeRange` props to Unified Histogram.

### Checklist

- [ ] ~Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)~
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] ~Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard
accessibility](https://webaim.org/techniques/keyboard/))~
- [ ] ~Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [ ] ~This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))~
- [ ] ~This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)~

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>

wayneseymour pushed a commit to wayneseymour/kibana that referenced this pull request

Jan 19, 2023
…lastic#146352)

## Summary

This PR includes a number of updates to Unified Histogram to prepare for
sharing with solutions, as well as updating its usage in Discover:

Unified Histogram:
- Adds a `disableAutoFetching` prop, similar to Unified Field List, to
disable automatic Unified Histogram refetching based on prop changes.
- Accepts an `input$` observable prop that can be used to control manual
refetches.
- Removes `refetchId` used internally and replaces it with an
observables based approach to simplify refetch logic.
- Introduces a `use_stable_callback` utility hook to create callback
functions with stable identities which simplifies `useCallback` logic —
should be replaced with `useEvent` or whatever the React team comes up
with to solve this specific issue when available:
reactjs/rfcs#220.
- Eliminates debouncing logic in Unified Histogram since it was hacky —
manual refetching should be used instead if debouncing is needed.
- Accepts `query`, `filters`, and `timeRange` props to remove
dependencies on specific services.
- Updates `use_request_params` to export `getTimeRange` and
`updateTimeRange` functions for easier absolute time range handling.
- Exposes some Lens embeddable props to allow further customizing
Unified Histogram behaviour.

Discover:
- Exports a `fetch$` observable from `use_saved_search` to allow
listening for when data fetches should be triggered.
- Uses new manual refetching support in Unified Histogram to simplify
refetching logic.
- Passes `query`, `filters`, and `timeRange` props to Unified Histogram.

### Checklist

- [ ] ~Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)~
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] ~Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard
accessibility](https://webaim.org/techniques/keyboard/))~
- [ ] ~Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [ ] ~This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))~
- [ ] ~This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)~

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>

pawelgrimm added a commit to Doist/typist that referenced this pull request

Feb 22, 2023
The `useEvent` function was being called during rendering,
which throws an error when React's Strict Mode is enabled. This
type of usage is incorrect because it makes the rendering result
non-determinisitic.

See reactjs/rfcs#220 (comment)

@zhangfisher

Every additional API increases the mental burden on users

Now, users have to worry about when to use useCallback and when to use useEvent.

The two functions are quite similar, why not upgrade useCallback to have the useEvent function by default or through option switches?

I suggest considering enhancing the functionality of existing APIs while maintaining compatibility and asymptotic behavior.

xamgore reacted with thumbs up emoji dantman, VdustR, BasixKOR, elken, Cside, HealGaren, vilgeforc5, Glinkis, nathggns, elson-currentcorp, and 7 more reacted with thumbs down emoji

@jappp jappp mentioned this pull request

Dec 14, 2023

@skyboyer

has there been new RFC created? Can it be linked to this one? A lot of materials - quite outdated, yes - refer this current RFC. And without follow-up link it's just a dead-end

yolio2003, jampy, dhlolo, dante01yoon, lscheibel, richardscarrott, ganor38, fallaciousreasoning, silverwind, adamalfredsson, and 2 more reacted with thumbs up emoji

@dante01yoon

Where can I find further discussion? Any updates here? and what's the progress?

@huynhducduy

@AlessioGr

I see that the new useEffectEvent hook should only be called inside a useEffect, which makes me wonder: what benefits does useEffectEvent provide compared to simply extending useEffect to support “ignored dependencies?” For instance, consider a hypothetical hook like:

useIgnoredEffect(() => {
  console.log('Runs only when dependency1 changes. Still has the latest dependency2 value:',
    dependency2
  );
},
[dependency1], // Dependencies
[dependency2]); // "Ignored" / Secondary dependencies

Here, dependency1 triggers the effect, while dependency2 is accessible but doesn’t cause the effect to re-run. Wouldn't such an API be a lot easier and more intuitive to use?

In comparison, the useEffectEvent version requires calling two hooks, which adds complexity and makes it more difficult to refactor existing useEffect hooks:

const logDependency2 = useEffectEvent(() => {
  console.log('This event always has the latest dependency2 value:',
    dependency2
  );
});

// This effect runs only when dependency1 changes
useEffect(() => {
  console.log('Effect runs because dependency1 changed:', dependency1);
  logDependency2();
},
[dependency1]);
jacobsfletch, stevemeisner, nickadamson, mogelbrod, Oleg2tor, huynhducduy, GermanJablo, marcospgp, rahul-parmar-bsh, hednowley, and Zhouyashi reacted with thumbs up emoji nathggns reacted with thumbs down emoji

@marcospgp

This comment was marked as off-topic.

Reviewers

13 more reviewers

@EECOLOR EECOLOR EECOLOR left review comments

@sunderls sunderls sunderls left review comments

@iamakulov iamakulov iamakulov left review comments

@Haroenv Haroenv Haroenv left review comments

@satya164 satya164 satya164 left review comments

@eps1lon eps1lon eps1lon left review comments

@adibfirman adibfirman adibfirman left review comments

@cmlttnts cmlttnts cmlttnts left review comments

@Nick-bond Nick-bond Nick-bond approved these changes

Reviewers whose approvals may not affect merge requirements

Labels