FullStack Labs

Please Upgrade Your Browser.

Unfortunately, Internet Explorer is an outdated browser and we do not currently support it. To have the best browsing experience, please upgrade to Microsoft Edge, Google Chrome or Safari.
Upgrade
Welcome to FullStack Labs. We use cookies to enable better features on our website. Cookies help us tailor content to your interests and locations and provide many other benefits of the site. For more information, please see our Cookies Policy and Privacy Policy.

Production-Level Patterns for React Hooks 🎣🦈

Written by 
,
Production-Level Patterns for React Hooks 🎣🦈
blog post background
Recent Posts
Six Ways to Handle Concurrency in the JVM
Is there value in learning Vanilla JavaScript?
How to be efficient in remote design work in multicultural teams

In my last post, An Introduction to React.js Hooks, I provided a general introduction to hooks with a real world example. In this post, I’ll dive into patterns for architecting production-level React hook apps.

Table of contents

Separation of Concerns

Unlike class components which organize all lifecycle methods into a single class, hooks distribute them into reusable functions that can be used across more than one component or file.

Initially, it is tempting to add all required hooks to the top of a giant function and return JSX at the bottom. Check out this example to-do app code below:

-- CODE language-jsx keep-markup --
import React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { useNavigation } from 'react-navigation-hooks';
import { useDispatch, useSelector } from 'react-redux';

const Todos = () => {
    const { setParams, getParam, navigate } = useNavigation();  
     const
initSearch = getParam('initSearch', '');  
    const [searchText, setSearchText] = useState(initSearch);  
    const {    
        todos,    
        loading,  
    } = useSelector(({ todos: todosData }) => ({
        todos: todosData.todos,    
        loading: todosData.loading,  
    }));  
    const dispatch = useDispatch();  
    useEffect(() => {    
        dispatch(getTodos());  
    }, [dispatch]);    
    
    /* Functions for handling data and responding to events */  
    onItemPressed = (item) => navigate('TODO_DETAIL', { todo })  
        {...}    

    return loading ? (    
        <loadingindicator>   </loadingindicator>
    ) : (    
        <view></view>
            
                value={searchText}              
                onChangeText={setSearchText}      
            />
            <list data="{todos}" onitempressed="{onItemPressed}/"></list>
      
);}

This method works, but it is not very readable and creates unnecessary complexity.

To clean things up, refactor each hook that accesses data into a custom hook in a separate file.

-- CODE language-jsx keep-markup --
import { useState, useEffect, useCallback } from 'react';
import { useNavigation } from 'react-navigation-hooks';
import { useDispatch, useSelector } from 'react-redux';

const useTodosHooks = () => {  
    const { setParams, getParam, navigate } = useNavigation();  
    const initSearch = getParam('initSearch', '');  
    const [searchText, setSearchText] = useState(initSearch);  
    const {    
        todos,    
        loading,  
    } = useSelector(({ todos: todosData }) => ({    
        todos: todosData.todos,    
        loading: todosData.loading,  }));  
     const
dispatch = useDispatch();  
    useEffect(() => {    
        dispatch(getTodos());  
    }, [dispatch]);    
    
    return {    
        state:{ searchText, setSearchText },    
        navigate,    
        todos,    
    loading  
    }
}

This provides a separation of the data and the view, but the component function is still very long, and contains a lot of business logic.

Additionally, testing with this structure is difficult. Without a class instance, state changes can only be executed by interacting with components of a view. There is no way to spy on the setters when using useState.

To address the issues above, separate the logic from the view.

Logic

-- CODE language-jsx keep-markup --
import React from 'react';
import useTodosHooks from './hooks';
import TodosScreen from './components/TodosScreen';

const Todos = () => {  
    const { navigate, ...screenProps } = useTodosHooks();    
 
    screenProps.onItemPressed = (item) => navigate('TODO_DETAIL', { todo })  

    return<todosscreen {...screenprops}=""></todosscreen>
}

View

-- CODE language-jsx keep-markup --
import React from 'react';
Import { View } from ‘react-native;
Import Searchfield from ‘./SearchField’;
Import List from ‘./List’;
Import LoadingIndicator from ‘./LoadingIndicator;

const TodoScreen = ({ loading, searchText, setSearchText, todos,  onItemPressed }) => {  
    return loading ? (    
        <loadingindicator>   </loadingindicator>
    ) : (    
        <view></view>
            <searchfield  value="{searchText}" onchangetext="{setSearchText}"></searchfield>
            <list data="{todos}" onitempressed="{onItemPressed}/"></list>
          
    );
}

With the separation above, we have emulated the popular MVC architecture using hooks.

Model

This is the file where state and functional hooks are defined, as shown above.

View

This is the rendered JSX. The props of this functional component are only the necessary state and functions for displaying and interacting with the view.

Controller

This is the initial, giant function. It now de-structures only what it needs from the model, some of which may be passed down to the view. Notice that all state and setters are wrapped in a state object for ease of transport and again being able to destructure only what is necessary.

While there are now more files, each of these three functions has a specific job and references only the data that is necessary. Each file is more readable, and therefore easier to understand and review. The code is also much easier to cover with tests. The controller can be mounted, with mocks for any library hooks, and can cover most of the three files. When state needs to be altered or a setter needs to be spied on, the view component can be mounted with mocked props inputs.

Making it Modular

In a production-level application, the same data and logic is going to be used in several places. This provides an opportunity to eliminate repetition in the models.

A prime example is the react-redux useSelector hook. Many views will rely on the same data, which mean selectors and their getter actions wind up implemented in several models. A file can be made for a redux wrapper, and another can be made for a combination of selectors and dispatch hooks per data type.

-- CODE language-jsx keep-markup --
import { getTodos } from '../actions/todos';

exportconst useGetTodos = () => {  
    const dispatch = useDispatch();  

    const {    
        todos,    
        loading,  
   
    } = useSelector(({ todos: todosData }) => ({    
       todos: todosData.todos,    
       loading: todosData.loading,  
    }));  

    useEffect(() => {    
        dispatch(getTodos());  
    }, [dispatch]);

   return {    
        todos,    
        todosLoading: loading,  
    };
};

Understanding Hook Dependencies

Using hooks incorrectly can easily cause “unnecessary” re-renderings. Although this has become a common concern surrounding hooks, it can be mitigated by gaining a better understanding of how hooks activate based on their dependencies.

The key lies in the secondary argument in useEffect, useMemo, and useCallback. In the to-do example above, the useEffect hook depends on dispatch and is passed in as the second parameter. This means the function will not redefine dispatch on every render because the context value of dispatch does not change. Therefore, useEffect will not rerun on every render.

If the dependency wasn't present, the call to getTodos would occur on every render. This would cause an infinite render loop because the redux selector would get a new state every render and then dispatch would fire again causing another render. In the class lifecycle this would be akin to unconditionally dispatching on every componentDidUpdate or, more accurately, in the render function itself.

Recommended Reading

Forget about component lifecycles and start thinking in effects - Sebastian De Deyne

---

At FullStack Labs, we are consistently asked for ways to speed up time-to-market and improve project maintainability. We pride ourselves on our ability to push the capabilities of these cutting-edge libraries. Interested in learning more about speeding up development time on your next form project, or improving an existing codebase with forms? Contact us.

Written by
People having a meeting on a glass room.
Join Our Team
We are looking for developers committed to writing the best code and deploying flawless apps in a small team setting.
view careers
Desktop screens shown as slices from a top angle.
Case Studies
It's not only about results, it's also about how we helped our clients get there and achieve their goals.
view case studies
Phone with an app screen on it.
Our Playbook
Our step-by-step process for designing, developing, and maintaining exceptional custom software solutions.
VIEW OUR playbook
FullStack Labs Icon

Let's Talk!

We’d love to learn more about your project.
Engagements start at $75,000.

company name
name
email
phone
Type of project
How did you hear about us?
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.