Proofpoint closes acquisition of Tessian. Read More ->

Request a demo
Request a demo
Request a demo
Request a demo
Request a demo

React Hooks at Tessian

Luke Barnard • Wednesday, June 16th 2021
React Hooks at Tessian

Tessian Cloud Email Security intelligently prevents advanced email threats and protects against data loss, to strengthen email security and build smarter security cultures in modern enterprises.

I’d like to describe Tessian’s journey with React hooks so far, covering some technical aspects as we go.

About two years ago, some of the Frontend guild at Tessian were getting very excited about a new React feature that was being made available in an upcoming version: React Hooks.

React Hooks are a very powerful way to encapsulate state within a React app. In the words of the original blog post, they make it possible to share stateful logic between multiple components. Much like React components, they can be composed to create more powerful hooks that combine multiple different stateful aspects of an application together in one place.

So why were we so excited about the possibilities that these hooks could bring?

The answer could be found in the way we were writing features before hooks came along. Every time we wrote a feature, we would have to write extra “boilerplate” code using what was, at some point, considered by the React community to be the de facto method for managing state within a React app ─ Redux. As well as Redux, we depended on Redux Sagas, a popular library for implementing asynchronous functionality within the confines of Redux. Combined, these two(!) libraries gave us the foundation upon which to do…very simple things, mostly API requests, handling responses, tracking loading and error states for each API that our app interacted with.

The overhead of working in this way showed each feature required a new set of sagas, reducers, actions and of course the UI itself, not to mention the tests for each of these. This would often come up as a talking point when deciding how long a certain task would take during a sprint planning session.

Of course there were some benefits in being able to isolate each aspect of every feature. Redux and Redux Sagas are both well-known for being easy to test, making testing of state changes and asynchronous API interactions very straight-forward and very ─if not entirely─ predictable. But there are other ways to keep testing important parts of code, even when hooks get involved (more on that another time).

Also, I think it’s important to note that there are ways of using Redux Sagas without maintaining a lot of boilerplate, e.g. by using a generic saga, reducer and actions to handle all API requests. This would still require certain components to be connected to the Redux store, which is not impossible but might encourage prop-drilling.

In the end, everyone agreed that the pattern we were using didn’t suit our needs, so we decided to introduce hooks to the app, specifically for new feature development.

We also agreed that changing everything all at once in a field where paradigms fall into and out of fashion rather quickly was a bad idea. So we settled on a compromise where we would gradually introduce small pieces of functionality to test the waters.

I’d like to introduce some examples of hooks that we use at Tessian to illustrate our journey with them.

Tessian’s first hook: usePortal

Our first hook was usePortal. The idea behind the hook was to take any component and insert it into a React Portal. This is particularly useful where the UI is shown “above” everything else on the page, such as dialog boxes and modals.

The documentation for React Portals recommends using a React Class Component, using the lifecycle methods to instantiate and tear-down the portal as the component mounts/unmounts. Knowing we could achieve the same thing with hooks, we wrote a hook that would handle this functionality and encapsulate it, ready to be reused by our myriad of modals, dialog boxes and popouts across the Tessian portal.

The gist of the hook is something like this:

[codestyle]

import { useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
export default function usePortal() {
    const portalContainer = useRef(document.createElement('div'))
    useEffect(() => {
        // Attatch the portal container to the DOM
        document.body.appendChild(portalContainer.current)
        // Remove from the DOM when unmounted
        return () => {
            try {
                document.body.removeChild(portalContainer.current)
            } catch (error) {
                // Ignore errors because it's possible that the child is
                // already removed
            }
        }
    }, [])
    return ({ children }) => createPortal(children, rootElemRef.current)
}

[/codestyle]

Note that the hook returns a function that can be treated as a React component. This pattern is reminiscent of React HOCs, which are typically used to share concerns across multiple components. Hooks enable something similar but instead of creating a new class of component, usePortal can be used by any (function) component. This added flexibility gives hooks an advantage over HOCs in these sorts of situations.

Anyway, the hook itself is very simple in nature, but what it enables is awesome!

Here’s an example of how usePortal can be used to give a modal component its own portal:

[codestyle]

const Portal = usePortal()

return (
   <Portal>
     <Modal>
       What's new with the Tessian Portal
       ...
     </Modal>
   </Portal>
     
   
)

[/codestyle]

Just look at how clean that is! One line of code for an infinite amount of behind-the-scenes complexity including side-effects and asynchronous behaviors!

It would be an understatement to say that at this point, the entire team was hooked on hooks!  

Tessian’s hooks, two months later

Two months later we wrote hooks for interacting with our APIs.

We were already using Axios as our HTTP request library and we had a good idea of our requirements for pretty much any API interaction. We wanted:

  • To be able to specify anything accepted by the Axios library
  • To be able to access the latest data returned from the API
  • To have an indication of whether an error had occurred and whether a request was ongoing

Our real useFetch hook has since become a bit more complicated but to begin with, it looked something like this:

[codestyle]

import request from './api.js' // encapsulates calls to our APIs

const useFetch ({ initialData, ...options }) {
    // Store the most recently fetched data
    const [data, setData] = useState(initialData)
    // Indicate whether a request is ongoing
    const [isLoading, setIsLoading] = useState(false)
    // Indicate any error that occured in the most recent fetch
    const [error, setError] = useState(null)

    useEffect(() => {
        setError(null)
        setIsLoading(true)
        request(options)
            .then(resp => {
                setData(resp)
                setIsLoading(false)
            })
            .catch(err => setError(err))
    }, [JSON.stringify(options)])

    return { data, isLoading, error }
}

[/codestyle]

To compare this to the amount of code we would have to write for Redux sagas, reducers and actions, there’s no comparison. This hook clearly encapsulated a key functionality that we have since gone on to use dozens of times in dozens of new features.

From here on out, hooks were here to stay in the Tessian portal, and we decided to phase out Redux for use in features.

Today there are 72 places where we’ve used this hook or its derivatives ─ that’s 72 times we haven’t had to write any sagas, reducers or actions to manage API requests!

Tessian’s hooks in 2021

I’d like to conclude with one of our more recent additions to our growing family of hooks.

Created by our resident “hook hacker”, João, this hook encapsulates a very common UX paradigm seen in basically every app. It’s called useSave.

The experience is as follows:

  • The user is presented with a form or a set of controls that can be used to alter the state of some object or document in the system.
  • When a change is made, the object is considered “edited” and must be “saved” by the user in order for the changes to persist and take effect. Changes can also be “discarded” such that the form returns to the initial state.
  • The user should be prompted when navigating away from the page or closing the page to prevent them from losing any unsaved changes.
  • When the changes are in the process of being saved, the controls should be disabled and there should be some indication to let the user know that: (a) the changes are being saved, (b) the changes have been saved successfully, or that (c) there was an error with their submission.

Each of these aspects require the use of a few different native hooks:

  • A hook to track the object data with the user’s changes (useState)
  • A hook to save the object data on the server and expose the current object data (useFetch)
  • A hook to update the tracked object data when a save is successful (useEffect)

A hook to prevent the window from closing/navigating if changes haven’t been saved yet (useEffect)

Here’s a simplified version:

[codestyle]

const useSave = ({ initialData, saveUrl }) => {
    // Track object state, with user's changes
    const [data, setData] = useState(initialData)

    // Save object data, retrieve data according to the server or any errors that occur
    const { data: savedData, isLoading, error: saveError, triggerFetch } = useFetch({
        url: saveUrl,
        method: 'PUT',
        triggerOnly: true,
        data
    })

    // When a save is successful, update the tracked state
    useEffect(() => savedData && setData(savedData), [savedData])

    // Indicate whether the user has made any changes vs. saved or initial data (using lodash)
    const hasChanges = !_.isEqual(data, savedData || initialData)

    // Prevent navigation if there are any unsaved changes
    useEffect(() => {
        const handleOnUnload = (e) => {
            if (hasChanges) {
                // Show a blocking popup to prevent navigation
                e.preventDefault()
                // Some browsers require setting this instead
                e.returnValue = ''
            }
        }
        window.addEventListener('beforeunload', handleOnUnload)
        return () => window.removeEventListener('beforeunload', handleOnUnload)
    }, [hasChanges])

    return {
        data,
        setData,
        isLoading,
        isSaved: savedData && !hasChanges,
        hasChanges,
        saveError,
        triggerFetch
    }
}

[/codestyle]

As you can see, the code is fairly concise and more importantly it makes no mention of any UI component. This separation means we can use this hook in any part of our app using any of our existing UI components (whether old or new).

An exercise for the reader: see if you can change the hook above so that it exposes a textual label to indicate the current state of the saved object. For example if isLoading is true, maybe the label could indicate “Saving changes…” or if hasChanges is true, the text could read “Click ‘Save’ to save changes”.

Tessian is hiring!

Thanks for following me on this wild hook-based journey, I hope you found it enlightening or inspiring in some way. If you’re interested in working with other engineers that are super motivated to write code that can empower others to implement awesome features, you’re in luck! Tessian is hiring for a range of different roles, so connect with me on LinkedIn, and I can refer you!

Luke Barnard