Josh Pittman
Josh Pittman

Reputation: 7324

How do I use RXJS fromEvent in React?

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>
  );
};

Upvotes: 7

Views: 6277

Answers (3)

Dre
Dre

Reputation: 2172

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)}/>
}

Upvotes: 0

panepeter
panepeter

Reputation: 4242

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>
  )
};

Upvotes: 0

Fan Cheung
Fan Cheung

Reputation: 11345

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>
  );
};

Upvotes: 12

Related Questions