brightec
Home Services Projects Blog About Contact

How to write more structured and testable React code using Custom Hooks

Steve working at his desk

We look at what's involved in creating and testing a Custom Hook

If you strive to write well structured, readable and testable code, then stick around as we explore how to separate out your React components’ business logic into Custom Hooks.

React has been holding court in the development world for over 8 years, and Hooks were introduced in early 2019 as a way to introduce smaller chunks of reusable and testable code to large projects; a much-needed evolution from the class components that would often bloat horribly with each passing sprint.

From personal experience, I was involved in a project which was proving difficult for the QA team to test reliably. After doing a little research they asked for all new business logic to be written within Custom Hooks so they could set up better unit testing.

There was a definite learning curve early on, but over time it became evident that we were writing much higher quality code. We were also writing better tests, and as such we now adopt this process across all of our React projects.

Out of the box

There are a number of Hooks baked into React which, among other things, allow you to recreate the functionality of a class component’s state variable and lifecycle methods. This article does not serve as an introduction to those hooks, so if you are unfamiliar with their usage, I would recommend you check out this documentation (https://reactjs.org/docs/hooks-reference.html) before continuing.

The rules

While there aren’t that many rules to follow when it comes to Hooks, the few that exist are pretty important:

  • A Hook must begin with ‘use’ (ie, useState, useMyComponentHook, etc); this being the special prefix that triggers the magic

  • Only call Hooks at the top level. All hooks must run in the same order every time, so you cannot place them inside loops, conditions or nested functions

  • Only call Hooks from React functions, ie: React Functional components, or Custom Hooks

For more information, see the official documentation (https://reactjs.org/docs/hooks-rules.html).

Creating your own hooks

At its heart, a Hook is just a function with an input and/or an output. Using React’s own useState Hook as an example:

const [value, setValue] = useState(0)

We pass zero into the Hook, and it returns a value (in this case zero) and a function that we can call to set the value to something else.

Something like useEffect, also one of the baked-in Hooks, takes one or two parameters but returns nothing.

useEffect(() => {
 // some code to do something
}, [dependency])

We pass in a function, and an optional array of dependencies. The function runs every time a dependency changes, runs once if an empty array of dependencies is supplied, or runs on every render if no array is supplied.

This simple set up means that you can create a Hook to do absolutely anything, provided it adheres to the rules mentioned above.

A quick example

Let’s say you’re building a website and you want to fetch some JSON from an API you’re working with. If you’re just loading the data at the root of your application, you’d probably put the following code in your App.js:

const[data, setData] = useState(null)

useEffect(() => {
 fetch('https://jsonplaceholder.typicode.com/users')
 .then(response => response.json())
 .then(setData)
}, [])

Now, let’s say you want to use a number of API endpoints in a number of areas in your application. You could hardcode the above into each component, changing just the URL each time, or you could create a Custom Hook that takes in just the endpoint and returns an object containing the data property, for example:

const { data } = useJSONPlaceholder({ endpoint: 'users' })

The Custom Hook code would look something like this:

const useJSONPlaceholder = ({
endpoint,
}) => {
const [data, setData] = useState(null)

useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/${endpoint}`)
.then(response => response.json())
.then(setData)
}, [endpoint])

return { data }
}

When our useJSONPlaceholder is first called, useEffect observes that ‘endpoint’ has been set (in this case to ‘users’) and fires off an HTTP request using the Javascript Fetch API. If the value of ‘endpoint’ never changes, this code will only run once. When the promise is resolved, the data property is updated using the setData method provided by our useState Hook, and this value is passed out into the parent component.

Any time you want to retrieve something from the JSONPlaceholder API within your code, you can add the Custom Hook we have just created, passing in the endpoint you wish to use. As a side-note, this API is incredibly useful if you ever need quick access to a restful API. Documentation can be found here (https://jsonplaceholder.typicode.com).

Evolving your hook

You are probably wondering why I contained the input and output properties inside an object rather than just exposing the properties themselves. That would certainly make the code simpler, but it would also require any existing code that uses the Hook to be refactored should we decide to add functionality, such as the refresh method we’re going to add now:

const useJSONPlaceholder = ({
endpoint,
}) => {
const [data, setData] = useState(null)

const refreshData = useCallback(() => {
fetch(`https://jsonplaceholder.typicode.com/${endpoint}`)
.then(response => response.json())
.then(setData)
}, [endpoint]) useEffect(() => {
refreshData()
}, [refreshData])

return { data, refreshData }
}

Even though we have updated the Hook, the original call to fetch the user data remains unchanged, but a new call to fetch a todo list, offers us a method to refresh that list should we need to.

const { data, refreshData } = useJSONPlaceholder({ endpoint: 'todos' })

By taking this approach to developing your Custom Hooks, you are able to write exactly what you need at the time, while safe in the knowledge your code can be expanded upon without introducing too much technical debt.

Reusable

Another advantage to using Hooks is the ability to share code between applications and even platforms. With the above useJSONPlaceholder example, we can not only fetch data from a number of endpoints within our web application, but we can use exactly the same code within a React Native project, even though the UI uses a different set of components. It’s that simple to share code between projects, and within a short space of time, you can build your own library of tools to help in your development of React applications.

Testable

Finally, let’s touch on the subject of testing. As frameworks and applications become more complex and teams get larger, the need for reliable testing will only increase. Fortunately, Custom Hooks make that side of things much easier to accomplish provided you get a handle on the basics.

Remember those three rules from earlier? Because you may only call a Hook from a React function, in order to write tests for a Custom Hook, we must wrap them inside a functional component.

Let’s use this ridiculously overwritten Hook that doubles its input value, as an example:

const useDouble = ({ value }) => {
 return {
 result: value * 2,
 }
}

And now let’s write a test to check that it works. First we create a function called testHook which contains our wrapper. The wrapper creates the output object, which is a model of the expected output from our Hook. It then creates an empty React component that calls our hook and assigns its output to the hookOutput property. We then mount this component using the properties that will be passed into our testHook function, and finally return the references to both the hookObject and the testElement so that we can use them in our test cases; in this instance, checking that the value 2 is doubled to 4.

describe('#useDouble', () => {
 const testHook = testProps => {
 const hookOutput = {
 result: undefined,
 }
 const TestComponent = props => {
 Object.assign(hookOutput, useDouble(props))
 return null
 }
 const testElement = mount(<TestComponent {...testProps} />)
 return [hookOutput, testElement]
 }

 it(‘should return double the input value’, () => {
 const [output] = testHook({
 value: 2,
 })
 expect(output.result).toEqual(4)
 })
}

Hurray, our test passes. Obviously as you write more complex Hooks, the tests will become more involved, but this basic structure should service most of your needs.

In summary

Creating Custom Hooks in your React applications allows you to separate out your business logic, makes it easier to test in a UI agnostic way, and can be reused in both web and mobile applications. There are nuances to using Hooks which are not discussed here, but provided you stick to the main three rules, you should find huge benefits over using the class based alternatives, or combining logic and UI within the same component.


Looking for something else?

Search over 300 blog posts from our team

Want to hear more?

Subscribe to our monthly digest of blogs to stay in the loop and come with us on our journey to make things better!