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
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?
An event-driven architecture uses events to trigger and communicate between decoupled services and is common in modern applications built with microservices. An event is a change in state, or an update, like an item being placed in a shopping cart on an e-commerce website.
An event is an action that could be triggered as a result of the user action or system generated event. For example, a mouse click, loading of a web page, pressing a key, window resizes, and other interactions are called events.
An Event Bus is a design pattern that allows PubSub-style communication between components while the components remain loosely coupled. A component can send a message to an Event Bus without knowing where the message is sent to.
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:
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 eventsuseEvents
wherever needed without having to pass the event list & dispatch/subscribe functions down component trees, or mess around with react context.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...
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>
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With