# Handling external events in React React is basically a function converting application state to presentation. ```haskell type React = (state: State) => View ``` As you can see, there are no `Events` in this description. However, most real-life applications have at least some events arriving from outside world, and also produce some side-effects. The question is **how do we convert events to state?** Let's start with an example. We will simulate an event happening somewhere outside of our application. ```typescript= function triggerEvent(eventName: string, params?: any) { if (window.___event_triggered___) window.___event_triggered___(eventName, params) } function randomTrigger() { if (Math.random() < 0.5) return triggerEvent("alpha") else return triggerEvent("beta", { arg: Math.random() }) } setInterval(randomTrigger, 1000) ``` As you can see, a random event named `alpha` or `beta` is raised every second. When the event is raised, a global function named `___event_triggered___(eventName, params)` is called. Let's create a React component to display current state. ```typescript= const EventDisplay: React.FC = () => { let [lastEvent, setLastEvent] = React.useState("alpha") let [totalArg, setTotalArg] = React.useState(0) return ( <div> last event: {lastEvent}, total arg: {totalArg} </div> ) } ``` How can we subscribe to the external event and change the state in response to it? ## 1. `useState` + `useEffect` The first implementation that comes to mind is pretty straightforward: just **subscribe to the event in the useEffect hook and handle updates there**. Let's start with this naive implementation. ```typescript= const EventDisplay: React.FC = () => { let [lastEvent, setLastEvent] = React.useState("") let [totalArg, setTotalArg] = React.useState(0) React.useEffect(() => { window.___event_triggered___ = function(eventName, params) { let arg = params?.arg || 0 setLastEvent(eventName) setTotalArg(a => a + arg) } }, []) return ( <div> last event: {lastEvent}, total arg: {totalArg} </div> ) } ``` This approach will work if the code is actually that simple. However, it's very prone to errors due to changes in other components. Suppose we need two different `<EventDisplay>`s on one screen: ```typescript= const App: React.FC = () => <div> <EventDisplay /> <EventDisplay /> </div> ``` What will be displayed on the screen? You can check here: https://codesandbox.io/s/youthful-water-ufr0i Even if we have only one `<EventDisplay>` element, any mount/unmount will reset the `totalArg` count. We can solve this with the use of `useContext` hook. ## 2. `useContext` and lifting state up We need to create the context and also a provider component for convenience. ```typescript= // default state const GlobalState = React.createContext({ lastEvent: "", totalArg: 0 }) // state provider const GlobalStateProvider: React.FC = ({ children }) => { // initial values let [lastEvent, setLastEvent] = React.useState("") let [totalArg, setTotalArg] = React.useState(0) // same useEffect subscription React.useEffect(() => { window.___event_triggered___ = function (eventName, params) { let arg = params?.arg || 0 setLastEvent(eventName) setTotalArg((a) => a + arg) } }, []) return ( <GlobalState.Provider value={{ lastEvent, totalArg }}> {children} </GlobalState.Provider> ) } ``` Now we need to modify our `EventDisplay` component to use `GlobalState` context. ```typescript= const EventDisplay: React.FC = () => { let { lastEvent, totalArg } = React.useContext(GlobalState) return ( <div> last event: {lastEvent}, total arg: {totalArg} </div> ) } ``` And also wrap the application in the `GlobalStateProvider`. ```typescript= const App: React.FC = () => ( <GlobalStateProvider> <div> <EventDisplay /> <EventDisplay /> </div> </GlobalStateProvider> ) ``` Now both `EventDisplay` elements display correctly. https://codesandbox.io/s/vigilant-aryabhata-w9hsj However, while we lifted the state up from `EventDisplay` to `GlobalStateProvider`, we could have done the same within the `App` component and save a few keystrokes. How do we handle more complex states? That's where state managers come into play. While Redux, TESM or other state managers are probably the best choice for huge applications, we don't need all these features right now. We can get away with a single reducer and a `useReducer` hook. ## 3. `useReducer` and reducer function We finally got to the point where we explicitly define the types we were using since the beginning of the article. Usually this should be your first step in building application, but due to chaotic nature of frontend development this may not always be the case. Let's start with the global state itself. It consists of two fields: ```typescript= type GlobalState = { lastEvent: string // it's actually "" | "alpha" | "beta", but // let's keep it simple for now totalArg: number } ``` And all of the possible actions can be described as this type: ```typescript= type StateAction = { type: string arg?: number } // it might be a good idea to use more explicit types // but we skip it for the sake of this example // type StateAction = { type: "alpha" } | { type: "beta", arg: number } ``` Now we need a reducer that will handle state changes. It's fairly easy to extract from our previous code. ```typescript= // since a reducer is just a function, we can use any tools // like `combineReducers` from Redux or `Update` function // from MVU etc. const reducer = function(state: GlobalState, action: StateAction): GlobalState { return { lastEvent: action.type, totalArg: state.totalArg + (action.arg || 0) } } ``` The provider will now use `useReducer` instead of `useState`. ```typescript= const GlobalStateProvider: React.FC = ({ children }) => { let [state, dispatch] = React.useReducer(reducer, { lastEvent: "", totalArg: 0 }) React.useEffect(() => { window.___event_triggered___ = function (eventName, params) { dispatch({ ...(params || {}), type: eventName, }) } }, []) return ( <GlobalState.Provider value={state}> {children} </GlobalState.Provider> ) } ``` Full example: https://codesandbox.io/s/distracted-hill-gme1l So what happened here is more than just lifting state up like in the previous step. We actually moved the app state logic out of `GlobalStateProvider` into the `reducer(state, action)` function. This means that we can use all of the tools from Redux, TESM or other state libraries to manage global application state in a scalable way. The only purpose of `GlobalStateProvider` becomes to listen to external events and dispatch them to the reducer. It's now less of a `God object` and more of a `Proxy`. ## Appendix 1: handling state changes before React starts rendering We now have a solid architecture, but there was one important assumption that we had. We assumed that our React tree is rendered instantly, before the first external event is raised. What if it's not the case in our application? What if for some reason we need to delay the first React render and therefore `GlobalStateProvider` render and event subscriptions? All we need to do is just use our reducer manually until `GlobalStateProvider` is initialized. ```typescript= // create a variable with initial state in it let initialState: GlobalState = { lastEvent: "", totalArg: 0 } // check that GlobalStateProvider was not attached yet // (this check can be omitted in most cases) if (!window.___event_triggered___) { window.___event_triggered___ = function (eventName, params) { let action = { ...(params || {}), type: eventName, } // update initialState with an incoming action initialState = reducer(initialState, action) } } ``` Now we have to use this `initialState` in `GlobalStateProvider`: ```typescript= const GlobalStateProvider: React.FC = ({ children }) => { let [state, dispatch] = React.useReducer(reducer, { lastEvent: "", totalArg: 0 }) // ... } // becomes const GlobalStateProvider: React.FC = ({ children }) => { let [state, dispatch] = React.useReducer(reducer, initialState) // ... } ``` Full example: https://codesandbox.io/s/silly-ride-lpogk The app initialization is now as follows: 1. `initialState` is created. 2. `window.___event_triggered___` handler is added. 3. Any incoming events are handled manually at this stage, changing the `initialState`. 4. React is initialized at some point (maybe immediately). 5. `GlobalStateProvider` is renderred for the first time, attaching itself to `window.___event_triggered___`. 6. Any incoming events are now handled in the `dispatch` function from `useReducer` hook inside `GlobalStateProvider`. ## Conclusion We have used `useContext`, `useEffect` and `useReducer` hooks to manage global application state and handle external events. This solution is certainly not as feature complete as a state management library like Redux or TESM. But it's also good to know that you can hack a few React hooks together into a global state if you need to.