How do I use RXJS fromEvent in React?
Asked Answered
B

3

7

I'm trying to log the click event on a button in react:

const InputBox = () => {
  const clicky = fromEvent(
    document.getElementById('clickMe'),
    'click'
  ).subscribe(clickety => console.log({ clickety }));

  return (
    <button id="clickMe" type="button">
      Click Me
    </button>
  );
};

I get the following error 'Invalid event target'

enter image description here

The setup seems to be fine. If I replace document.getElementById('clickMe') with document then it logs the clicks. But that logs any click in the document and I just want the clicks on the button in question.

I tried using a ref instead...

const InputBox = () => {
  const buttonEl = React.useRef(null);

  const clicky = fromEvent(buttonEl.current, 'click').subscribe(clickety =>
    console.log({ clickety })
  );

  return (
    <button ref={buttonEl} type="button">
      Click Me
    </button>
  );
};

...but then I get the same 'Invalid event target' error.

Can someone help me understand why this is an invalid event target and how to fix this so that I can use fromEvent in react.


Update

The problem was that I did not register the observable when mounting the react components.

If you do this, you must also unmount the component when unmount.

This works for me now.

const InputBox = () => {
  React.useEffect(() => {
    const click$ = fromEvent(
      document.getElementById('clickMe'),
      'click'
    ).subscribe(clickety => console.log({ clickety }));
    return () => click$.unsubscribe();
  }, []);

  return (
    <button id="clickMe" type="button">
      Click Me
    </button>
  );
};
Baeda answered 14/10, 2019 at 3:26 Comment(2)
is the button on the DOM yet when you are registering?Thickwitted
Register the listener in componentDidMountSweeting
R
12

Wrap it in useEffect() that's when the dom is ready

const InputBox = () => {
  const buttonEl = React.useRef(null);

  useEffect(() => {
    const clicky = fromEvent(buttonEl.current, 'click').subscribe(clickety =>
      console.log({ clickety })
    );

    return () => clicky.unsubscribe();
  }, []);

  return (
    <button ref={buttonEl} type="button">
      Click Me
    </button>
  );
};
Rudder answered 14/10, 2019 at 4:38 Comment(4)
This worked thank you. Don't need to use refs, works with Id selector. For anyone else following this, you only need to register onMount, so keep your dependancy array empty, rather than mounting on every render. const InputBox = () => { React.useEffect( () => fromEvent(document.getElementById('clickMe'), 'click').subscribe( clickety => console.log({ clickety }) ), [] ); return ( <button id="clickMe" type="button"> Click Me </button> ); };Baeda
...and also dont forget to unsubscribe ``` const InputBox = () => { React.useEffect(() => { const click$ = fromEvent( document.getElementById('clickMe'), 'click' ).subscribe(clickety => console.log({ clickety })); return () => click$.unsubscribe(); }, []); return ( <button id="clickMe" type="button"> Click Me </button> ); }; ```Baeda
U can also use subjectRudder
I don't want to use a subject because that forces you to mess with the internals of your observable and handle your own errors, I would prefer an out of the box solution.Baeda
M
0

I'm making a couple of assumptions, hoping to present a more generic solution:

  • there's nothing special about a MouseEvent. We're trying to have an Observable of a certain type, owned by a component, and a way to update it.
  • using the Observable is a concern that should be separate from creating it, so it can be passed on to other components, combined with other observables, etc.
  • using refs to attach functionality to buttons is both confusing (since highly non-standard) and not very flexible (what happens if you want to pass that handler somewhere else?)

I'm combining two things in the snippet:

  • a createObservableWithNext function I described in another answer. It returns an Observable, and a next function to update it.
  • useObservable from the wonderful react-use package. Basically handles subscribing and unsubscribing to observables.
const FancyButton = () => {
  const { observable, next } = React.useRef(
    createObservableWithNext<MouseEvent<HTMLButtonElement>>()
  ).current;

  const latestClick = useObservable(observable);

  React.useEffect(() => {
    if (latestClick) {
      console.log(`clicked at ${latestClick.pageX} ${latestClick.pageY}`);
    }
  }, [latestClick]);

  return (
      <button onClick={next}>
        Next click
      </button>
  )
};
Maggie answered 31/5, 2022 at 6:11 Comment(0)
L
0

The solution above using references is elegant but it wasn't working for me when trying to hook into the 'onChange' event on a MUIv5 TextField component. I ended up with a different solution that I actually prefer as it is more direct, understandable and robust in my opinion. First create a RxJS subject and then just push next events from the event handler. Note that you don't need to be using MUI components for this to work. For example:

import TextField from '@mui/material/TextField'
import { Subject } from 'rxjs'
import { useEffect } from 'react'

const textSubject = new Subject<string>('')

const FancyTextInput = () => {

  useEffect(() => {
    const sub = textSubject.subscribe({next: text => console.log(text)})
    return () => sub.unsubscribe()
  }, [])
 
  return <TextField onChange={e => textSubject.next(e.target.value)}/>
}
Lovemaking answered 5/4, 2024 at 20:55 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.