DEV Community

Carlos
Carlos

Posted on

Using EventEmitter in React

I have an issue where handlers subscribed to a custom event see all the updated state (from calls to setSomething()) except the last one that was set. Specifically:

const [counter1, setCounter1] = useState(1);
const [counter2, setCounter2] = useState(1);

const increment = useCallback(async () => {
  events.emit('beforeIncrement');

  await someAsyncAction();  // <-- There's an async call.

  setCounter1((x: number) => x + 1);
  setCounter2((x: number) => x + 1);

  // React 17:
  //    Handlers of the following event will see counter1 updated value but not counter2.
  // React 18 with Automatic Batching:
  //    Handlers of the following event will not see see counter1 or counter2 updated value.
  // What is the best way to emit an event here and ensure that handlers see the updated state?
  //    setTimeout(() => events.emit('afterIncrement')) ??

  events.emit('afterIncrement');
}, []);
Enter fullscreen mode Exit fullscreen mode

The following repo demonstrates this behavior:

https://github.com/cmermingas/event-emitter-react-17

  • Why is this happening?
  • What is the right way to emit an event to ensure that handlers have access to all the updated state?

Thanks in advance!

[edit]

The example I provided is a simplified version of the real situation. A more generic description would be:

const doWork = useCallback(async () => {
  events.emit('beforeWork');

  // Asynchronous calls where React state gets updated.

  events.emit('afterWork');
}, []);
Enter fullscreen mode Exit fullscreen mode

The Asynchronous calls section triggers state updates, possibly outside of the body of this callback.

I want all afterWork handlers to have the most up-to-date state from React. I imagine an API like this:

const doWork = useCallback(async () => {
  events.emit('beforeWork');

  // Asynchronous calls / Promise chains here
  // where React state gets updated.

  // Work is done. When React has updated, emit an event:

  afterReactWork(() => events.emit('afterWork'));
}, []);
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
_bkeren profile image
'' • Edited

it's because of asynchronicity.

if you put
await someAsyncAction();
after second setCounters it will work as you expect.

The best way is to use useEffect with dependencies.

detect both values have changed and then emit the event
stackoverflow.com/questions/629747...

Collapse
 
cmermingas profile image
Carlos

Thank you for your reply and the link to SO. I updated my question with a bit more detail.

If you don't mind, I'll quote your suggestions and respond:

it's because of asynchronicity.

If there's no asynchronous call, then the afterIncrement handlers don't see any updated values:

const doWork = useCallback(async () => {
  events.emit('beforeWork');
  setA(...);
  setB(...);
  events.emit('afterWork');  // <-- Handlers see old values
}, []);
Enter fullscreen mode Exit fullscreen mode

Here, I would still want to emit afterWork at the end of doWork, decoupled with React's updating of A and B, and I would want the handlers to have access to React's updated state.

if you put
await someAsyncAction();
after second setCounters it will work as you expect.

Thank you for the suggestion.

Moving the async action is not an option in the real situation because it is more complicated. However, to understand better, I extended the example with an additional counter:

const [counter1, setCounter1] = useState(1);
const [counter2, setCounter2] = useState(1);
const [counter3, setCounter3] = useState(1);
const increment = useCallback(async () => {
  events.emit('beforeIncrement');

  await someAsyncAction();
  setCounter1((x: number) => x + 1);
  setCounter2((x: number) => x + 1);
  setCounter3((x: number) => x + 1);

  events.emit('afterIncrement');
}, []);
Enter fullscreen mode Exit fullscreen mode

Now, the afterIncrement handlers see the updated value for counter1 and counter2 but not counter3. **Why is it that only the last one is not updated?

The best way is to use useEffect with dependencies.

This will not be the best way for me because I don't want the event to be coupled with the state. Instead, I want it to be coupled with the fact that increment is done. I appreciate the suggestion.

Collapse
 
_bkeren profile image
''

“Currently (React 16 and earlier), only updates inside React event handlers are batched by default” , according to Dan Abramov.

increment method is async, it should be a normal function