Decouple Your Logic From UI By Creating Your Own React Hooks

Daniyal Khalid
The Startup
7 min readSep 27, 2020

--

Hooks are arguably one of the biggest paradigm shifts and game-changers in React’s short history. Although hooks are most frequently thought of and paraded around as some sort of harbinger of the death of class-based components, they have many other interesting uses. One of my favorite uses of hooks lately has been to use them as containers of logic and state, ensuring that their associated React components only have to deal with rendering UI as far as possible.

Hooks are commonly used for extracting common logic that is used across multiple components. Frequent use cases are API requests and form submissions. The approach I am advocating for in this article is to not just use hooks for reused logic, but for every logic in your app. In this way, your code will become significantly cleaner and manageable as your React components will only house the rendering code while your hook files will have all the logic related to that component. Consequently, your UI and logic become decoupled and you are free to reuse just the UI, the logic or both in other parts of your application.

To illustrate this, we’re going to use as an example a list view component in a React application. The component will show a list of cards, each card having one user, fetched from the free ReqRes API, which supports pagination. We will implement pagination in this component so the user can click a “Load More” button to load more users without refreshing the page.

A Primer On Creating Your Own Hooks

In this section, we’ll take a brief look at creating your own hooks. I’ll assume you are familiar with the basics of hooks as well common hooks like useState and useEffect. Creating and using your own hooks is straightforward as long as you remember the two golden rules of React hooks:

  1. Hooks can only be called in other hooks or functional components.
  2. Hooks should be called at the top level (not inside conditions or loops)

When writing custom hooks, I find it best to think of hooks as simply functions that can store state and operate on that state. The function (hook) will usually return to its caller the state that it monitors as well as some methods that operate on that state. Since hooks can use other hooks (rule 1), we can use useState to store our custom hook’s state.

Consider our scenario where we need to write the hook for pagination. What state will our hook keep? The most obvious one is how many of the items have already been loaded and what needs to be loaded next. Therefore, we’ll store the current page number in the state. Another thing we’ll need to store is the actual data that gets loaded from the API. Finally, we’ll keep track of whether there are further items to load or not.

What’s wrong with the traditional approach?

We’ll now look at two diagrams to see the comparison between two approaches. In the first one, we use the traditional approach and write our pagination logic inside the list component.

User List component without extracting pagination logic to a hook

Although this structure will get the job done perfectly fine, it has some drawbacks:

  • The code of the UI rendering and javascript logic for fetching posts will get mixed up in one file, making it difficult to comprehend and reason about the code.
  • It violates the single responsibility principle (SRP) of software engineering in which one unit of code should be responsible for one structured task (either rending UI or handling pagination logic)
  • If the same pagination logic is later required in other components of the app, we will end up writing the same code multiple times.
  • It will make our code files longer and tedious to navigate.

At this point, you might ask a very pertinent question:

Why not just move the handler functions to a separate file instead of creating hooks?

This will solve many of the problems we have outlined above as it will still separate the UI and pagination logic considerably. However there is one major limitation of this approach which hooks help us overcome: even if we move the function to a separate file and import it in the UserList component, UserList will still have to keep track of the page number.

UserList is just a UI component. It should only be responsible for rendering the list of users passed to it without any regard for the page number. The page number is purely a part of the pagination logic. It will be a violation of the SRP if UserList starts concerning itself with getting and setting page numbers. With a properly implemented pagination hook, any programmer using the UserList component will only have to worry about rendering the users. The details of the pagination mechanics will be abstracted away from the users of the pagination hook and they can focus on rendering the UI.

Hooks can manage their own state (as well as use other hooks). This means they can manage the page number and update it as required. That’s what sets hooks apart from ordinary functions.

The Hooks Approach

Now take a look at this second diagram in which we extract the pagination logic to a hook:

Improved version with pagination logic extracted to a hook.

Here you can immediately see that the four problems we outlined above are solved.

Getting Hands On

Now let’s implement our mini pagination example using hooks so you can get the hang of how to decouple code from UI.

  1. Initialize the app using CRA and set up folder structure.
  2. Setup App.js
App.js

3. Create a card component for showing a single user

User.jsx

4. Create the User List component for showing a list of users.

UserList.jsx

Here you will notice that we have used the usePaginationFromUrl hook (which we will create shortly). Recall that extracting the pagination logic to a hook means that our UserList component does not need to concern itself with any implementation detail. Therefore, we can simply go ahead and use the pagination hook as if it is already implemented and import from it the relevant variables and methods. This will serve as a guide for when we actually get to implementing the hook.

5. Create the pagination hook

usePaginationFromURL.js

Our pagination hook stores 3 important pieces of state:

pageNumber: The latest page number loaded by the hook. The user of the hook can increment this using the incrementPage method which then conveniently triggers the loading of the next page’s data, since pageNumber is a dependency of useEffect, inside which the next page is loaded.

hasMoreData: A boolean that indicates whether there are further pages to be loaded or not. The API tells us the total number of pages in each response, so we can easily update this each time another page is fetched.

data: The actual data loaded by the hook. This is an array containing all the users of the current and all previous pages loaded so far.

Note that the hook takes in a base url from the component that is using it. This will allow our hook to be reused with other API endpoints that also support pagination, assuming the key names in the data returned are consistent across the API. Even if the key names are not consistent, (for instance, you use another API instead that has the key totalPages instead of total_pages), you can pass these in as well to the hook, creating a completely reusable hook at the cost of some added parameters increasing code complexity.

Pagination In Action

That’s it! Time to see the end result.

Wrapping Up

React hooks are a great way to extract functionality that requires keeping track of state to a separate file. This leaves your React components with the sole responsibility of dealing with the UI instead of juggling between rendering the UI, keeping state, making API calls and implementing handler functions. This will make your code much easier to read and reason about and help you make cleaner mental models regarding your app’s functionality.

--

--

Daniyal Khalid
The Startup

Software Engineer with interests in web development, AI and learning new stuff.