Scalable Frontend #4 – Custom Hooks to the Rescue

Decoupling from the state layer

This post is part of the Scalable Frontend series. You can see the other parts here: “#1 — Architecture”, “#2 — Common Patterns”, and “#3 — The State Layer”.

And we’re back! After some time since the last post of this series, we’re back to the subject we promised we would talk about at the end of the previous post: a way to have the view layer depend on the state layer while still keeping them decoupled. Even better, an impressive fact happened since when we began to prepare to write this post that made us even more eager to write it: the React community embraced the use of hooks, and we believe the Vue community is following the same path with its Composition API! We’ve been having very good results by using hooks as a way to decouple the view and state layers, so today we’re gonna focus on that. Without further ado, to move the heavy lifting out of your components, custom hooks to the rescue!

What are hooks and custom hooks?

First of all, a hook in the context of this post is a feature in which a UI library allows you to have a better way to isolate, reuse and compose stateful logic between pieces of UI components. This feature allows the developer to handle stateful logic without having to worry about rendering complexities, stale data, among others, and every time you use this feature to create a new hook, we call it a custom hook. We’re not going to give a whole introduction on hooks, types of hooks, or how to use them with React, Vue, or whatever. For React, we suggest reading the introduction to hooks and its FAQ, and the Vue documentation has a good introduction to Composition API as well.

We’ll add more useful links about the subject at the end of this post as well but it’s important to understand that if you’re using any other UI library (be it Angular, Svelte, you name it) that has some feature that matches the description above, the techniques here apply.

But why?

In the last post, all of our examples used Redux’s connect function to decouple the components from the state layer, which was and still is totally fine. The thing is, since then we began to experiment with exposing the state to the view layer through the use of hooks and we noticed there was an expressive gain in modularization, separation of concerns, and composability. So in this post, we’ll show how to apply all the common patterns from the previous post with hooks, and more! Here’s an important detail, we’re not saying using connect is bad, it’s actually easier to test code that uses connect, but we believe the tradeoff of hooks is worth it.

Also, even though we never mentioned it directly, a lot of the approaches we suggest here are highly influenced by a technique called Domain-Driven Design (or DDD). The reason we never mentioned it here is that we didn’t want people to confuse DDD with software architecture. Besides the fact DDD is often used with some scalable architecture, it’s important to mention that DDD is not an architecture. You can use DDD with a variety of good architectures without making it less or more domain-driven. If you want to know more about DDD we’ll add some links about it at the end of this post as well.

So back when we used Redux’s connect to plug the state layer into the view layer, we didn’t have a standardized way to separate the access to our state into domain-oriented pieces; everything our component had access to was via props injected by connect. We admit that this approach has the advantage of keeping our components even more ignorant about the state but the tradeoff of using hooks for that overcomes it. Separating the access to our state into domain-oriented custom hooks made it all so easier to compose and reuse it across the board that we’ve been using a lot now.

So if you need some reasons for using custom hooks as a way to decouple the view layer from the state layer, here they are:

  • Better composability and reusability: composing hooks just works;
  • Clearer separation of the state access into domain-oriented units: you can have custom hooks to represent aggregates and bounded contexts;
  • Good enough testability: even though testing simple non-connected components was better with connect, remember how cumbersome it was to test a non-connected component that rendered a connected component inside of it? We do! Using custom hooks standardizes it making all testing easy enough.

Ok, but how?

Do you remember that in the last post we mentioned a pattern known as ducks? Seriously, if you don’t, go back there and read it, it’s gonna be needed here.

Ok, now that you’ve done it, you’ll know that the ducks were loosely divided into domain-oriented branches which exposed selectors and action creators for the components to import and have them injected with mapStateToProps and mapDispatchToProps. The gist of what we’re gonna show here is that instead of components importing those things directly from the ducks, we’ll have each duck exposing one or more custom hooks that constitute a clear interface for the component to interact with that branch of the state tree. And more than that, those custom hooks will also be the home of pre-processed domain-logic that needs to be called with data coming from the state and the component will not even need to be aware of that!

Custom hooks to the rescue, step by step

We’ll take a step-by-step approach here in order to achieve a good separation of what should be in a single custom hook and how that would be exposed to the component.

1. The rules

Let’s begin by settling some important ground rules for those custom hooks:

  1. Custom hooks should obey every other hook rule dictated by the UI library or framework you’re using;
  2. A custom hook should not leak or reveal what technology we’re using. A consumer of this hook should not know you’re using Redux, context API, MobX, GraphQL, or whatever, it should always and only expose an object with data and functions that don’t reveal the technology underneath;
  3. A consumer of a hook should not know a lot about the shape your state is stored, which means the functions exposed by a hook capable of triggering changes in the state should not be simply setters, they should reflect domain commands;
  4. A custom hook should not cover too much of the state, it should be focused on a domain-oriented piece and be orthogonal to other custom hooks as much as possible;
  5. As a consequence of one of the pitfalls we talked about in the last post, a customer should not depend on the return of an action exposed by a custom hook;
  6. If you need pre-processed data derived from the state across the board, including the ones resulted from domain logic that can be exposed, it should be exposed through a custom hook;
  7. Not every domain logic needs to be exposed by a hook; you can import domain logic directly into the component files if needed, while your hooks should focus on exposing data and actions.

2. Where to cut a line

Let’s use the same imaginary blog application from the previous post where we have three state branches: articles, tags, and user. From that, we take that we’ll have at least three custom hooks – one for each of those branches: useArticles, useTags, and useUser. We might add more in case any of them gets too general.

Considering the articles branch, let’s say it contains an object with a list key with all the articles from the current page, a currentPage key, and a currentArticle key with the current article and its comments in case the user is accessing an article page. From that we can assume that we need to split useArticles into two hooks: useArticles and useCurrentArticle, so that we don’t make the original useArticles lose its focus.

3. What goes in a custom hook’s public interface

As much as it can sound weird, we’re gonna plan the public interface of a custom hook first! You don’t have to do it every time you create a custom hook but it’s a good way to understand the process of designing a custom hook.

Let’s use these last two hooks as examples to know what we could expose in a custom hook. Let’s consider our articles state branch looks something like this:

{
  list: [
    { id: 1, title: 'Frontend architecture' },
    { id: 2, title: 'Common patterns' }
  ],
  currentPage: 2,
  currentArticle: {
    id: 2,
    title: 'Common patterns',
    content: "Let's continue...",
    comments: {
      status: 'LOADED',
      list: [
        {
          userId: 42,
          content: "Isn't 'Common pattern' a redudant title?"
        }
      ]
    }
  }
}

From this state shape we could make the keys list and currentPage responsibilities of the useArticles hook, and the currentArticle the responsibility of the useCurrentArticle hook. What should we expose in the useArticles hook?

We begin by looking at what data we want to expose. This one should be straightforward: it’s these two pieces of information we just mentioned, list and currentPage. We can even rename list to articles when exposing it to make it clearer. So disregarding the implementation details for a moment, we want our custom hook to be used this way at this point:

const { articles, currentPage } = useArticles()

The consumer shouldn’t care if Redux is being used or any other approach, which should be reflected in the name of the keys of the returned object.

Is there some other data we might want to expose for the convenience of the UI? Possibly! We could also return the data regarding if we’re on the first page; if we’re on the last page, those could be derived from some metadata that is stored but not fully exposed to the UI. We could also expose if the current page is still loading: remember state machines? Their status can be returned as well, and even though it’s stored as a string we can return it as a set of booleans that details each specific state to make the job of the UI easier:

const {
  articles,
  currentPage,
  isFirstPage,
  isLastPage,
  isLoading,
  isLoaded,
  loadError
} = useArticles()

Ok, what about the actions? As mentioned before, the actions should not just look like setters, they should be meaningful domain-oriented instructions! So instead of exposing actions like setCurrentPage(number), loadPage(number), and setIsLoaded(boolean), we could have a single action, goToNextPage(), which will change the value of the exposed data and the UI won’t have to orchestrate it. Calling goToNextPage() would increment the current page number by 1, load it, and manage the value of the loading status, so this whole thing would be abstracted by the hook. Following the same idea, we could create a goToPreviousPage() as well where we could even prevent the consumer from causing an error if the function is called from the first page. Let’s take a look at what we have as the public interface of our hook at this moment:

const {
  articles,
  currentPage,
  isFirstPage,
  isLastPage,
  isLoading,
  isLoaded,
  loadError,
  goToNextPage,
  goToPreviousPage
} = useArticles()

Notice that even though we have a considerable amount of attributes returned by the hook, they all belong to the same concept and the consumer can destructure only what it needs.

The process to design useCurrentArticle is similar and will be left as an exercise for you!

4. How to implement custom hooks internally

We’re all set with the public interface we want for our custom hooks, but how do we actually implement it? As a custom hook is an abstraction, it’s important to mention that the internal implementation depends on the UI library and state management approach you’re following. Here we’re gonna show an example with React with both Redux and Context API but you can extrapolate it to use with whatever combination of UI and state management library you’re using.

So for Redux, we’ll use two hooks provided by react-redux to give our custom hooks access to the state: useSelector and useDispatch. As the names suggest, the first one will receive a selector as an argument and return that slice of the state provided by the selector, and the second one will return the dispatch function. It’s important to mention that your custom hook should not return the dispatch function, it should be encapsulated. Let’s take a look at how to do it:

import { useSelector, useDispatch } from 'react-redux'
import {
  getArticles,
  getCurrentPage,
  getPageMetadata,
  getPageStatus,
  getLoadError,
  goToNextPage as goToNextPageAction,
  goToPreviousPage as goToPreviousPageAction,
  PageStatus
} from './articles' // << this file is a duck
import * as ArticleFeedPage from '../domain/ArticleFeedPage'

export const useArticles = () => {
  const pageMetadata = useSelector(getPageMetadata)
  const pageStatus = useSelector(getPageStatus)
  const loadError = useSelector(getLoadError)
  const dispatch = useDispatch()

  const goToNextPage = () => dispatch(goToNextPageAction())
  const goToPreviousPage = () => dispatch(goToPreviousPageAction())

  return {
    articles: useSelector(getArticles),
    currentPage: useSelector(getCurrentPage),
    isFirstPage: ArticleFeedPage.isFirst(pageMetadata),
    isLastPage: ArticleFeedPage.isLast(pageMetadata),
    isLoading: pageStatus === PageStatus.LOADING,
    isLoaded: pageStatus === PageStatus.LOADED,
    loadError,
    goToNextPage,
    goToPreviousPage
  }
}

Notice that the functions that dispatch actions are pretty similar to the ones injected by connect but they are returned from a hook instead of being passed as props. Even though it looks like a small example, it covers most of the aspects of what we would have in a custom hook that encapsulates state coming from Redux. If you’re wondering if this hook is too simple and short, that’s correct! Your custom hooks should not be complex or contain too much logic.

But how would we do it if our application didn’t use Redux but Context API with the state contained in the provider instead? It wouldn’t be too different, actually! The first thing we would need is to move the logic of state management and action dispatching inside a custom context provider:

import { createContext, useState, useCallback, useMemo } from 'react'
import * as ArticleFeedPage from '../domain/ArticleFeedPage'

export const ArticlesContext = createContext()

const PageStatus = {
 LOADING: 'LOADING',
 LOADED: 'LOADED',
 IDLE: 'IDLE'
}

// the container prop here is our dependency container
// we talked about it in the first post of the series
export const ArticlesProvider = ({ children, container }) => {
  const [articles, setArticles] = useState([])
  const [currentPage, setCurrentPage] = useState(-1)
  const [pageMetadata, setPageMetadata] = useState(null)
  const [pageStatus, setPageStatus] = useState(PageStatus.IDLE)
  const [loadError, setLoadError] = useState(null)

  const goToNextPage = useCallback(async () => {
    const nextPage = currentPage + 1
    setCurrentPage(nextPage)
    const { articles, pageMetadata } = await container.loadPage(nextPage)
    setArticles(articles)
    setPageMetadata(pageMetadata)
  }, [currentPage])

  const goToPreviousPage = useCallback(/* similar to goToNextPage */)

  const contextValue = useMemo(() => {
    return {
      articles,
      currentPage,
      isFirstPage: ArticleFeedPage.isFirst(pageMetadata),
      isLastPage: ArticleFeedPage.isLast(pageMetadata),
      isLoading: pageStatus === PageStatus.LOADING,
      isLoaded: pageStatus === PageStatus.LOADED,
      loadError,
      goToNextPage,
      goToPreviousPage
    }
  }, [articles, currentPage, pageMetadata, pageStatus, loadError])

  return <ArticlesContext.Provider value={contextValue}>{children}</ArticlesContext.Provider>
}

Notice that the content of the provider is very similar to the custom hook with Redux and the context value has exactly the same interface as the one returned by the Redux hook. Now you just need to have this custom provider wrapping your root component so the whole application can access it, and your custom hook to give access to it will look like this:

import { useContext } from 'react'
import { ArticlesContext } from './context'

export const useArticles = () => useContext(ArticlesContext)

With that, we have the exact same hook implemented with both Redux and Context API. We could even do this in a real application and we would not have to rewrite too much code because the consumer of the hook doesn’t care about the change as long as the public interface is stable. The advantage of this technique is that if we change our state management strategy, the only part that needs code change is the custom hook.

What if we need both global and local isolated state?

You won’t always want to keep your state global. For cases like forms, it’s usually recommended to keep the state local until you have all the data ready to be submitted and then you submit straight from the local state and store the result in the global state. We might even want to reuse the definition and manipulation of this mix of local and global state, but how do we do it with custom hooks? It’s actually similar to what we’ve been doing already.

Let’s say we want a custom hook for the form to submit a new comment to an article and keep the content of this new comment in the local state and once it’s finished it’ll trigger a call to a use case and store the result in the global state. We could do something like this:

import { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
  getComments,
  submitComment as submitCommentAction
} from '../comments' // it's a duck

export const useComments = () => {
  const dispatch = useDispatch()
  const submitComment = (comment) => dispatch(submitCommentAction(comment))

  return {
    comments: useSelector(getComments),
    submitComment
  }
}

export const useCommentForm = () => {
  const [commentBody, updateCommentBody] = useState('') // local state
  const { submitComment } = useComments() // reusing the other custom hook for global state

  return {
    commentBody,
    updateCommentBody,
    submitComment: (userId) => submitComment({ userId, commentBody })
  }
}

Bear in mind that by using local state inside a custom hook you’re changing its reusability model – that’s why we separate it from the part that only handles global state so you can reuse it without causing the creation of local state.

How to test components that use these custom hooks?

With connect, testing a component was a matter of exporting the component non-connected as well as passing the props that acted like actions and values from the store and call it a day, and this is one of the greatest strengths of using connect. Let’s discuss how to test it when we’re using custom hooks.

There are two main ways to test a component that uses this technique. Even though we advocate against mocking imports most of the time, mocking the import for a custom hook inside the file that tests a component is the one approach we use the most. The second one is to fake the data accessed by the custom hooks, like creating fakes of the contexts or the Redux state provider while rendering in the tests. This second one also works well but we feel it couples the component tests too much to the state layer.

For the first approach, if you’re using Jest, you can use the module mocking functions and change the implementation of a custom hook for testing purposes:

import { render } from '@testing-library/react'
import { Article } from '../../components/article/Article'
import { useCurrentArticle } from '../../state/article/hooks'

jest.mock('../../state/article/hooks')

describe('<Article />', () => {
  describe('when article does not have comments', () => {
    it('renders a message about not having comments', () => {
      useCurrentArticle.mockReturnValue({
        id: 42,
        title: 'Something',
        content: 'This is the content',
        comments: []
      })

      const { getByText } = render(<Article />)

      expect(getByText('No comments yet, be the first')).toBeInTheDocument()
    })
  })
})

For the second approach, you’ll use React Testing Library’s wrapper feature. The approach will be similar to the above example regarding faking out the implementation, but you’ll fake the provider of the state directly instead of mocking it:

import { render } from '@testing-library/react'
import { Article } from '../../components/article/Article'
import { CurrentArticleProvider } from '../../state/article/providers'

describe('<Article />', () => {
  describe('when article does not have comments', () => {
    it('renders a message about not having comments', () => {
      const FakeProvider = ({ children }) => {
        return (
          <CurrentArticleProvider
            value={{
              id: 42,
              title: 'Something',
              content: 'This is the content',
              comments: []
            }}
          >
            {children}
          </CurrentArticleProvider>
        )
      }

      const { getByText } = render(<Article />, { wrapper: FakeProvider })

      expect(getByText('No comments yet, be the first')).toBeInTheDocument()
    })
  })
})

We believe this second approach can affect the decoupling between your component and the origin of the state, especially if your state is stored in Redux instead of a context. But it might be a matter of preference, so pick what feels right to your team.

Common pitfalls

As with any programming technique, using custom hooks bears the need to pay attention not to break any rule by going through an anti-pattern accidentally. We’re gonna list some here.

Don’t break rule 1

Your hooks should still have the guarantees provided by the UI library in use. For this to happen you have to obey the rules dictated by it and pay attention not to break them accidentally. Let’s see an example of this rule being broken implicitly:

import { useSelector } from 'react-redux'
import { getComment } from '../comments'

export const useComments = () => {
  return {
    getComment: (commentId) => useSelector(getComment(commentId))
  }
}

Even though it looks correct, this code breaks the React hooks rule that says a hook should always be called in the top-level scope of the component or custom hook it’s called inside, never inside a nested function. How do we fix that? We invert it by moving the hook call to the top-level scope:

import { useSelector } from 'react-redux'
import { getComment } from '../comments'

export const useComments = () => {
  return {
    // notice that now, useSelector is called at the same time useComments is called
    getComment: useSelector((commentId) => getComment(commentId))
  }
}

Don’t make business or state rules be enforced by the component calling a hook

We often see actions being called from a component spreading the current value to generate the argument that the action will receive, or using domain methods in the component to create the argument, like this:

import * as Article from '../../domain/article'
import { useArticleForm } from '../../state/article/hooks'

export const ArticleForm = () => {
  const { article, setArticle } = useArticleForm()

  return (
    <div>
       <input
         value={article.title}
         onChange={(e) => setArticle({ ...article, title: e.target.value }) }
       />
       <input
         value={article.tags}
         onChange={(e) => setArticle({ ...article, tags: Article.generateTags(e.target.value) }) }
       />
    </div>
  )
}

We should avoid that. Not only does this put too much responsibility on the component, it also spreads the application of state and business rules all around. To avoid it we can create new actions that have these specific responsibilities and expose them from the custom hook:

import { useArticleForm } from '../../state/article/hooks'

export const ArticleForm = () => {
  const {
    article,
    updateArticleTitle,
    updateArticleTags
  } = useArticleForm()

  return (
    <div>
       <input
         value={article.title}
         onChange={(e) => updateArticleTitle(e.target.value) }
       />
       <input
         value={article.tags}
         onChange={(e) => updateArticleTags(e.target.value) }
       />
    </div>
  )
}

Wrapping up

We think this article covers all of our recommendations when using custom hooks as a way to decouple the view layer from the state layer. I hope you find it useful and comment about alternative or similar approaches you use while developing!

Links

We want to work with you. Check out our "What We Do" section!