8

I'd like to "fire an event" in one component, and let other components "subscribe" to that event and do some work in React.

For example, here is a typical React project.

I have a model, fetch data from server and several components are rendered with that data.

interface Model {
   id: number;
   value: number;
}

const [data, setData] = useState<Model[]>([]);
useEffect(() => {
   fetchDataFromServer().then((resp) => setData(resp.data));
}, []);

<Root>
   <TopTab>
     <Text>Model with large value count:  {data.filter(m => m.value > 5).length}</Text>
   </TobTab>
   <Content>
      <View>
         {data.map(itemData: model, index: number) => (
            <Item key={itemData.id} itemData={itemData} />
         )}
      </View>
   </Content>
   <BottomTab data={data} />
</Root>

In one child component, a model can be edited and saved.

const [editItem, setEditItem] = useState<Model|null>(null);
<Root>
   <TopTab>
     <Text>Model with large value count:  {data.filter(m => m.value > 5).length}</Text>
   </TobTab>
   <ListScreen>
      {data.map(itemData: model, index: number) => (
          <Item 
             key={itemData.id} 
             itemData={itemData} 
             onClick={() => setEditItem(itemData)}
          />
      )}
   </ListScreen>
   {!!editItem && (
       <EditScreen itemData={editItem} />
   )}
   <BottomTab data={data} />
</Root>

Let's assume it's EditScreen:

const [model, setModel] = useState(props.itemData);

<Input 
   value={model.value}
   onChange={(value) => setModel({...model, Number(value)})}
/>
<Button 
   onClick={() => {
       callSaveApi(model).then((resp) => {
           setModel(resp.data);
           // let other components know that this model is updated
       })
   }}
/>

App must let TopTab, BottomTab and ListScreen component to update data

  • without calling API from server again (because EditScreen.updateData already fetched updated data from server) and
  • not passing updateData function as props (because in most real cases, components structure is too complex to pass all functions as props)

In order to solve above problem effectively, I'd like to fire an event (e.g. "model-update") with an argument (changed model) and let other components subscribe to that event and change their data, e.g.:

// in EditScreen
updateData().then(resp => {
   const newModel = resp.data;
   setModel(newModel);
   Event.emit("model-updated", newModel);
});

// in any other components
useEffect(() => {
   // subscribe model change event
   Event.on("model-updated", (newModel) => {
      doSomething(newModel);
   });
   // unsubscribe events on destroy
   return () => {
     Event.off("model-updated");
   }
}, []);

// in another component
useEffect(() => {
   // subscribe model change event
   Event.on("model-updated", (newModel) => {
      doSomethingDifferent(newModel);
   });
   // unsubscribe events on destroy
   return () => {
     Event.off("model-updated");
   }
}, []);

Is it possible using React hooks?

How to implement event-driven approach in React hooks?

glinda93
  • 3,643
  • 2
  • 16
  • 39
  • 1
    well, you can solve this problem using `redux`. and you can use 'useSelector' hook. According to the [docs](https://react-redux.js.org/api/hooks#useselector) useSelector() will also subscribe to the Redux store, and run your selector whenever an action is dispatched. – Naresh Jul 10 '20 at 04:45
  • @Naresh I don't know... is it event-driven? Can you show some example? – glinda93 Jul 10 '20 at 05:10
  • Redux uses actions in reducers, which is similar to events, but pure functional. You can also get something similar in plain React using `useReducer` but it's not as powerful. Finally you could just synthetic event handlers, which is most typical in React. – Nick McCurdy Jul 10 '20 at 05:19
  • 1
    https://codesandbox.io/s/9on71rvnyo?file=/src/components/Todo.js take a look at this. it's not completely implemented using hooks but you can update it by following this [docs](https://react-redux.js.org/api/hooks) – Naresh Jul 10 '20 at 05:40

6 Answers6

4

There cannot be an alternative of event emitter because React hooks and use context is dependent on dom tree depth and have limited scope.

Is using EventEmitter with React (or React Native) considered to be a good practice?

A: Yes it is a good to approach when there is component deep in dom tree

I'm seeking event-driven approach in React. I'm happy with my solution now but can I achieve the same thing with React hooks?

A: If you are referring to component state, then hooks will not help you share it between components. Component state is local to the component. If your state lives in context, then useContext hook would be helpful. For useContext we have to implement full context API with MyContext.Provider and MyContext.Consumer and have to wrap inside high order (HOC) component Ref

so event emitter is best.

In react native, you can use react-native-event-listeners package

yarn add react-native-event-listeners

SENDER COMPONENT

import { EventRegister } from 'react-native-event-listeners'

const Sender = (props) => (
    <TouchableHighlight
        onPress={() => {
            EventRegister.emit('myCustomEvent', 'it works!!!')
        })
    ><Text>Send Event</Text></TouchableHighlight>
)

RECEIVER COMPONENT

class Receiver extends PureComponent {
    constructor(props) {
        super(props)
        
        this.state = {
            data: 'no data',
        }
    }
    
    componentWillMount() {
        this.listener = EventRegister.addEventListener('myCustomEvent', (data) => {
            this.setState({
                data,
            })
        })
    }
    
    componentWillUnmount() {
        EventRegister.removeEventListener(this.listener)
    }
    
    render() {
        return <Text>{this.state.data}</Text>
    }
}
Muhammad Numan
  • 12,108
  • 1
  • 25
  • 50
  • React Native has built-in EventEmitter: https://stackoverflow.com/questions/36774540/eventemitter-and-subscriber-es6-syntax-with-react-native, https://github.com/facebook/react-native/issues/27413#issuecomment-572621322 – glinda93 Jul 14 '20 at 07:23
  • const Event = new EventEmitter(); wont work in react native but it work in react js. for expo or react native you can use this packagef – Muhammad Numan Jul 14 '20 at 07:28
  • `import EventEmitter from 'react-native/Libraries/vendor/emitter/EventEmitter'; const event = new EventEmitter();` - working code in my RN 0.62.2 project, although it shows declaration file missing warning in typescript – glinda93 Jul 14 '20 at 07:42
  • react-native-event-listeners this package is working on all the versions of react native and expo – Muhammad Numan Jul 14 '20 at 08:25
  • @bravemaster I think my answer support all the version of react-native what do you think? – Muhammad Numan Jul 18 '20 at 07:25
  • I was asking if I can achieve the same effect using React hooks, not how to use EventEmitter – glinda93 Jul 18 '20 at 08:09
  • @bravemaster you have edited your question after my answer. react-native did not provide any method before. the only way is to use event emitter. – Muhammad Numan Jul 18 '20 at 09:39
  • I've asked my question clearly in both OP and bounty description. And I already added a link to a react native solution and similar approach in my answer before yours. – glinda93 Jul 18 '20 at 10:08
  • yeah, it is not possible without event emitter. – Muhammad Numan Jul 18 '20 at 10:21
  • @bravemaster this bounty will be spoil because there is no other solution than EventEmitter. – Muhammad Numan Jul 18 '20 at 13:26
  • Your post does not answer my question. I know you got an idea but prove it in your post. Anyway, you will get half of the bounty, I don't mind the other half as long as I don't have a sufficient answer. – glinda93 Jul 18 '20 at 13:28
  • Thanks but can you explain about scope in second answer in detail? I don't understand it. – glinda93 Jul 18 '20 at 13:41
  • *useContext would be useful. so event emitter is best* - that doesn't make any sense – glinda93 Jul 18 '20 at 13:57
  • useContext or React hooks were two things I'd like to know whether could be an alternative approach to EventEmitter – glinda93 Jul 18 '20 at 13:59
  • for useContext we have to implement full context API with `MyContext.Provider` and `MyContext.Consumer` and have to wrap inside high order component – Muhammad Numan Jul 18 '20 at 13:59
  • You can improve your post instead of commenting. – glinda93 Jul 18 '20 at 14:00
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/218095/discussion-between-muhammad-numan-and-bravemaster). – Muhammad Numan Jul 18 '20 at 14:08
  • @bravemaster why you did not accept my answer? – Muhammad Numan Apr 16 '21 at 05:02
3

Not sure why the EventEmitter has been downvoted, but here's my take:

When it comes to state management, I believe using a Flux-based approach is usually the way to go (Context/Redux and friends are all great). That said, I really don't see why an event-based approach would pose any problem - JS is event based and React is just a library after all, not even a framework, and I can't see why we would be forced to stay within its guidelines.

If your UI needs to know about the general state of your app and react to it, use reducers, update your store, then use Context/Redux/Flux/whatever - if you simply need to react to specific events, use an EventEmitter.

Using an EventEmitter will allow you to communicate between React and other libraries, e.g. a canvas (if you're not using React Three Fiber, I dare you to try and talk with ThreeJS/WebGL without events) without all the boilerplate. There are many cases where using Context is a nightmare, and we shouldn't feel restricted by React's API.

If it works for you, and it's scalable, just do it.

EDIT: here's an example using eventemitter3:

./emitter.ts

import EventEmitter from 'eventemitter3';

const eventEmitter = new EventEmitter();

const Emitter = {
  on: (event, fn) => eventEmitter.on(event, fn),
  once: (event, fn) => eventEmitter.once(event, fn),
  off: (event, fn) => eventEmitter.off(event, fn),
  emit: (event, payload) => eventEmitter.emit(event, payload)
}

Object.freeze(Emitter);

export default Emitter;

./some-component.ts

import Emitter from '.emitter';

export const SomeComponent = () => {
  useEffect(() => {
    // you can also use `.once()` to only trigger it ... once
    Emitter.on('SOME_EVENT', () => do what you want here)
    return () => {
      Emitter.off('SOME_EVENT')
    }
  })
}

From there you trigger events wherever you want, subscribe to them, and act on it, pass some data around, do whatever you want really.

  • I learned that EventEmitters doesn't work well with React states, esp. when states are objects. You can use createRef for them to avoid that problem though. – glinda93 Oct 30 '20 at 13:11
  • of course, because state is asynchronous. My point is take sometimes you want to communicate **events** without data attached to it, or very little data, which can be useful when working on more "creative" apps. But if you do need to exchange data while using EventEmitter, refs are the way to go. – Joel Beaudon Dec 17 '20 at 14:03
  • 1
    Simple and straightforward. Much easier than Redux and Context. I don't know why not using this approach everywhere. Just wondering why creating a new object and freezing it. Exporting eventEmitter works fine too. – Eduardo May 12 '21 at 22:08
  • I like to freeze exported objects to prevent them from being manipulated. Although a deepFreeze might be more useful :) – Joel Beaudon May 14 '21 at 10:59
0

You can create use context in App.js using useContext, and then in you child component you can use values from it and update the context as soon as the context get updated it will update the values being used in other child component, no need to pass props.

Neelam Soni
  • 729
  • 5
  • 12
0

We had a similar problem and took inspiration from useSWR.

Here is a simplified version of what we implemented:

const events = [];
const callbacks = {};

function useForceUpdate() {
   const [, setState] = useState(null);
   return useCallback(() => setState({}), []);
}

function useEvents() {

    const forceUpdate = useForceUpdate();
    const runCallbacks = (callbackList, data) => {
       if (callbackList) {
          callbackList.forEach(cb => cb(data));
          forceUpdate();
       }
     
    }

    const dispatch = (event, data) => {
        events.push({ event, data, created: Date.now() });
        runCallbacks(callbacks[event], data);
    }

    const on = (event, cb) => {
        if (callbacks[event]) {
           callbacks[event].push(cb);
        } else {
          callbacks[event] = [cb];
        }

        // Return a cleanup function to unbind event
        return () => callbacks[event] = callbacks[event].filter(i => i !== cb);
    }

    return { dispatch, on, events };
}

In a component we do:

const { dispatch, on, events } = useEvents();

useEffect(() => on('MyEvent', (data) => { ...do something...}));

This works nicely for a few reasons:

  1. Unlike the window Event system, event data can be any kind of object. This saves having to stringify payloads and what not. It also means there is no chance of collision with any built-in browser events
  2. The global cache (idea borrowed from SWR) means we can just useEvents wherever needed without having to pass the event list & dispatch/subscribe functions down component trees, or mess around with react context.
  3. It is trivial to save the events to local storage, or replay/rewind them

The one headache we have is the use of the forceUpdate every time an event is dispatched means every component receiving the event list is re-rendered, even if they are not subscribed to that particular event. This is an issue in complex views. We are actively looking for solutions to this...

jramm
  • 5,405
  • 4
  • 24
  • 55
-1

I ended up using EventEmitter.

Javascript is an event-driven language after all.

Here is a working example: link

I tried to embed it in SO code snippet but EventEmitter does not work in browser.

// console.log("This example does not work in snippet because EventEmitter does not work in browser.\n You can see working example here: https://codesandbox.io/s/eventemitter-and-react-84eey?file=/src/Title.js");
const { useState, useEffect, useReducer } = React;
const DEFAULT_APP_STATE = "Initial state";
const Event = new EventEmitter();
const appReducer = (state = DEFAULT_APP_STATE, action) => {
  switch (action.type) {
    case "update":
      return action.payload;
    default:
      return action.payload;
  }
};

const Title = () => {
  const [reducerState, dispatch] = useReducer(
    appReducer,
    DEFAULT_APP_STATE
  );
  const [state, setState] = useState(reducerState);
  useEffect(() => {
    console.log("new state by Reducer", reducerState);
    setState(reducerState);
  }, [reducerState]);

  useEffect(() => {
    Event.addListener("update", newState => {
      console.log("new state by EventEmitter", newState);
      setState(newState);
    });
    return () => {
      Event.removeListener("update");
    };
  }, []);

  return <h1>Title state: {state}</h1>;
};

const Description = () => {
  const [reducerState, dispatch] = useReducer(
    appReducer,
    DEFAULT_APP_STATE
  );
  const [state, setState] = useState(reducerState);
  return (
    <React.Fragment>
      <p>Description State: {state}</p>
      <p>
        <button
          onClick={() => {
            const newState = "State updated by useReducer";
            setState(newState);
            dispatch({
              type: "update",
              payload: "State updated by useReducer"
            });
          }}
        >
          Dispatch using Reducer
        </button>
      </p>
      <p>
        <button
          onClick={() => {
            const newState = "State updated by EventEmitter";
            setState(newState);
            Event.emit("update", newState);
          }}
        >
          Emit by EventEmitter
        </button>
      </p>
      <p>
        <button
          onClick={() => {
            location.reload();
          }}
        >
          Refresh
        </button>
      </p>
    </React.Fragment>
  );
};

const App = () => {
  return (
    <div className="App">
      <Title />
      <Description />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);
<html>

<body>
  <div id="root">
     <p>This example does not work in snippet because EventEmitter does not work in browser.</p>
     <p>You can see working example <a target="_blank" href="https://codesandbox.io/s/eventemitter-and-react-84eey?file=/src/Title.js">here</a></p>
  </div>
  <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

</body>

</html>

UPDATE

React-redux is one of the best ways to handle this kind of problem. I'm using redux-saga and it's quite clean and easy compared to my original idea (event-driven approach).

glinda93
  • 3,643
  • 2
  • 16
  • 39
-1

EventEmitter goes against the fundamentals of the Flux architecture where data only flows down. There must never be a case where a state change in a component affects a sibling component.

The way to go is to use a global state management library, such as Redux. You can use the useSelector() hook to watch for a particular value, then use that value as the dependency of an effect.

const model = useSelector(store => store.model)

useEffect(() => doSomething(model), [model])

One thing to watch out when using this approach is that the effect will also run on component mount, even if model didn't change.

Mordechai
  • 13,232
  • 1
  • 32
  • 69
  • If you need a code example on how to set up Redux (my preferred, Redux Toolkit) let me know. – Mordechai Jul 13 '20 at 15:42
  • 1
    What's the point of conforming Flux architecture? In this case, different components should act differently according to model state change. They might even do something not related to new model state, but related event type itself. I think it's somewhat difficult to implement that behavior in redux. I tried to use Redux and manage global state, but it was awkward and won't do it (see the link in my answer). Why insisting an architecture, when other approach flawlessly can do work? – glinda93 Jul 13 '20 at 18:18
  • First of all, you can still do subscriptions in Redux as I said. Then, the way React will decide that the view is stale and requires a rerender is designed all the way with flux. – Mordechai Jul 13 '20 at 18:34
  • I'd be appreciated if you share your idea with code snippet – glinda93 Jul 13 '20 at 19:14
  • Edited my answer with a much simpler approach. Lt6 me know what you think – Mordechai Jul 14 '20 at 00:48
  • I have some basic knowledge in react(check my profile). That approach didn't solve my problem. Different components should do different actions according to model state(please look at my post carefully) and it's really uncomfortable compared to EventEmitter approach. – glinda93 Jul 14 '20 at 04:18