React View Switching with Custom Events

In my discussions with people in the Nashville community, I discovered that most teams are using some form of routing package that listens to URL changes to trigger view rendering. Because of this, we show students how to do view switching with the react-router-dom package.

Some don't use URL routing. Not every Single Page Application needs it.

While I've been playing around with React, slowly building up techniques for proper instruction to our students at Nashville Software School, I've tried my hand at several different mechanisms for view switching. Last night, while I was using the CustomEvent() constructor in another JavaScript project, a 💡 went off!

Instead of defining a function in the Matriarch component (that's what I call the component in every app that maintains application state) whose reference gets passed around, why not just listen for an event? Then any component that needs to switch the view based on user gesture, or other event, can simply dispatch an event with a data payload. The payload is optional, and contains any data the next view may need to do its job.

ViewManager

I created a simple module in my app whose responsibility is twofold - set up an event listener on a DOM element, and provide the ability to dispatch a custom event.

src/modules/ViewManager.js

const ViewManager = Object.create(null, {
    init: {
        value: function (selector, eventName, fn) {
            this.eventName = eventName
            this.element = document.querySelector(selector)
            this.element.addEventListener(this.eventName, fn)
        }
    },
    broadcast: {
        value: function (view, payload) {
            this.element.dispatchEvent(
                new CustomEvent(this.eventName, {
                    detail: {
                        view: view,
                        payload: payload
                    }
                })
            )
        }
    }
})

export default ViewManager

Main Application

In React, the main component is defaulted to App.js when using create-react-app. Your main component may be something else.

In the constructor, the ViewManager is initialzed with three things:

  1. The DOM element where the event listener will be attached.
  2. The name of the custom event that will be broadcasted.
  3. The local function reference to handle the event.

src/App.js

import React, { Component } from "react"
import ViewManager from "./modules/ViewManager"


class App extends Component {
    constructor(props) {
        super(props)

        // Initialize ViewManager for switching main view
        ViewManager.init("#root", "changeView", this.switch)
    }

    switch = event => {
        const _viewProps = Object.assign({
            notifications: notes,
            exampleAdditionalInfo: localStorage.getItem("preferences")
        }, event.detail.payload)

        // Update state to trigger the view change
        this.setState({
            currentView: event.detail.view,
            viewProps: _viewProps
        })
    }
    
    ...
    
    // Returns a component to be rendered in the JSX below
    ShowView = () => {
        switch (this.state.currentView) {
            case "profile":
                return <Profile {...this.state.viewProps} />
            case "results":
                return <SearchResults {...this.state.viewProps} />
            case "home":
            default:
                return <Home {...this.state.viewProps} />
        }
    }
    
    render() {
        return (
            <React.Fragment>
                <NavBar {...this.state.viewProps} />

                {this.ShowView()}
            </React.Fragment>
        )
    }
}

Any Component

Any component can now import ViewManager and use the broadcast() method to trigger a view change. In this stripped down component, when the user clicks on a View Profile hyperlink, a custom event is dispatched and the App component's listener triggers and switches the view.

src/search/SearchResults.js

import React, { Component } from "react"
import ViewManager from "../modules/ViewManager"

export default (props) => (
    <div className="searchResults">
        props.foundItems.users.map(user =>
            <a href="#" 
                className="btn btn-outline-success"
                onClick={() => {
                    // Switch to profile view with a data payload
                    ViewManager.broadcast("profile", {userId: user.id})
                }}
                >View profile</a>
        )
    </div>
)