When React announced that Hooks were coming I wanted to try them out. Part of my excitement came as I was trying to solve a particular problem for my personal project where I didn’t want to write a Component Class, really only required a Stateless Component but I did require access to State. I solved my problem by creating a Higher Order Component but felt quite over engineered for what I required and for people who don’t know the syntax is quite difficult to understand.

Another advanced pattern I use on my application are Render Props. Render Props allow you to also reuse or code between components or reuse logic. In my application I have used Render Props to wrap up my data fetching from my API so I can reuse code effectively.

If you are interested there is an excellent article I found on Free Code Camp that explains what preceded hooks and the motivation of why hooks were invented. In addition to reusability, hooks are also promising easier to read code and offer better code minification. Hooks are optional and the React team have no desire or intent to phase out Component Classes. The recommendation is not to rewrite all your existing components as hooks but use them when you need them going forward. I admire this because not everyone will like the syntax of hooks and there are too many projects where code relies on classes.

To better explain what hooks are I have decided to convert one of my existing Render Props to a custom hook. By converting to a custom hook we will be able to understand the use of the following two hooks:

  • useState;
  • useEffect.

We will also learn how to make use of this custom hook to present the data to display a list of communities retrieved from my API.

It should be noted that hooks are not intended for use of data fetching and future versions of React will expand the Suspense feature for this functionality. At the time of writing, Suspense is reserved for use React Lazy.

Lets Begin…

So with the tests all written to cover my Render Prop and it’s usage I was confident with testing out how to convert one of my render props to a hook and ensuring the expected behaviour will be sound. The full listing of the RenderProp is shown below:

import { FireStoreService, RtdbService } from 'js/services';
import React from 'react';
import DataRefreshContext from 'js/components/dataRefreshContext';
 
export default class GetSimpleList extends React.Component {
  fs = undefined;
  previousItem = undefined;
 
  constructor(props) {
    super(props);
    this.state = {
      data: {
        list: []
      },
      hasLoadedAll: false,
      isLoading: false,
      error: null,
    };
 
    this.updateData = this.updateData.bind(this);
  }
 
  componentDidMount() {
    console.time('Get Simple List');
    this.setState({ isLoading: true });
    this.getList().then(result =>
      this.setState({ data: result, isLoading: false })
    );
    console.timeEnd('Get Simple List');
  }
 
  getList = async () => {
    const { collection, orderBy, direction, pageSize } = this.props;
    let result = undefined;
    let updatedListCount = 0;
 
    try {
      if (this.previousItem) {
        result = await FireStoreService.getSimpleListPaged(collection, orderBy, direction, pageSize, this.previousItem);
        if (result && result.length > 0) {
          this.previousItem = result[result.length - 1][orderBy];
        }
      }
      else {
        result = await FireStoreService.getSimpleList(collection, orderBy, direction, pageSize);
        if (result && result.length > 0) {
          this.previousItem = result[result.length - 1][orderBy];
        }
      }
 
      //Get the number of records retrieved from query
      updatedListCount = result.length;
      //Append results of records retrieved to results array.
      result = [...this.state.data.list, ...result];
 
      return {
        list: result,
        hasLoadedAll: updatedListCount < pageSize
      }
 
    }
    catch (error) {
      this.setState({ error: "Unable to load data", isLoading: false });
    }
  }
 
  async updateData() {
    this.setState({ isLoading: true });
    this.getList().then(result => {
      this.setState({ data: result, isLoading: false })
    });
  }
 
  render() {
    return (
      <DataRefreshContext.Provider value={{ refreshList: this.updateData }}>
        {this.props.children(this.state)}
      </DataRefreshContext.Provider>
    )
  }
}

The Render Prop (GetSimpleList) allows us to retrieve some paged data from a collection, it is a fairly simple component that follows the standard Render Prop pattern.

The Render Prop is used in a list component as follows:

///...

return (
        <GetSimpleList key={refreshTimeStamp}
            authUser={authUser}
            collection={"communities"}
            orderBy={"communityName"}
            direction={"asc"}
            pageSize={10}>
 
            {/*Deconstruct the properties from state*/}
            {({ data, isLoading, error }) => {
                if (error) {
                    return <p>{error}</p>;
                }
 
                if (!data) {
                    return <p>No data yet ...</p>;
                }
                return (
                    <DataRefreshAgent.Consumer>
                        {context => (
                            <div>
                                <ul>
 
                                    {
                                        data.list.map(currentItem => {
                                            return (
                                                <CommunityItem communityName={currentItem.communityName}
                                                    communityId={currentItem.slug} />
                                            )
                                        }
                                        )
                                    }
                                </ul>
                                {isLoading && <p id="suspensePlaceholder">Loading ...</p>}
                                {!isLoading && !data.hasLoadedAll &&
                                    <button id="loadMore" onClick={context.refreshList} >Load More</button>
                                }
                                {!isLoading && data.hasLoadedAll && <p id="loadingComplete">Yay! All Items Are Loaded</p>}
                            </div>
                        )}
                    </DataRefreshAgent.Consumer>
 
                );
            }}
        </GetSimpleList>
    )

Converting to a Custom Hook

To begin we create a new file that is for our new hook. We will call the hook useSimpleList. It is encouraged by the React team to name the hooks beginning ‘use’. Following this convention allows React to know it is a hook and can check for violations against the rules of React.

To begin we start writing a Functional Component. The other name for these is Stateless Components but hooks allow us to access State therefore it made sense for the React team to name them as Functional Components.

export const useSimpleList = (props) => {
return <p>Hello World</p>;
}

The logic in GetSimpleList remains the same so it is possible to lift the methods getList and updateData over to the Hook. These methods allow us to retrieve and update the data from the service. We have some work to do with these later when we interact with State but we can copy these into our hook for now.

In our Class Component we initialise this.state in the Constructor. As we are now using functions we no longer have access to a “this” or the Constructor and need a new way to access and set state. This is where the first Hook comes in useState to set a state variable. This is the same as setting a variable via this.state in a constructor.

import { useState } from 'react';
export const useSimpleList = (props) => {
      const [list, setList] = useState([]);
}

A full explanation can be found on the React website about useState works but what we are doing in the above example is setting a state variable called “list”, a function to that can be used to update the list value called “setList” and the parameter we pass to the useState hook is the default value. In this case we are setting the list to an empty array.

The original Class Component set a number of initial values into State, to achieve this in a hook we simply call useState multiple times.

///...

   const [error, setError] = useState(null);
   const [isLoading, setIsLoading] = useState(false);
   const [list, setList] = useState([]);
   const [hasLoadedAll, setHasLoadedAll] = useState(false);
   const [previousItem, setPreviousItem] = useState(undefined); 

Now we have state variables all set up for our hook implementation. The next thing we need to work out is how to retrieve data when the component is mounted. In the component class we use the ComponentDidMount lifecycle method. However, just as we could not access State we also do not have access to the React lifecycle methods. The useEffect hook allows us to to hook in to component lifecycle and combines ComponentDidMount, ComponentDidUpdate and ComponentWillUnmount. The React documentation goes into a lot of depth about how useEffect works and the motivation behind this. Let's implement useEffect in our useSimpleList Hook.

///...
useEffect(() => {
 console.time('Get Simple List');
    this.setState({ isLoading: true });
    this.getList().then(result =>
      this.setState({ data: result, isLoading: false })
    );
    console.timeEnd('Get Simple List');
});

useEffect runs the function that is passed in as a parameter. If you are looking at the above snippet and notice something a bit odd you would be correct. this.setState() will not work because we don't have access to it. So how would you set the variables? The answer is to call the method declared in when you set up the isLoading state variable. Once we do this our effect looks like this:

///...

useEffect(() => {
        console.time('Get Simple List');
        setIsLoading(true);
        getList().then(result => {
            setList(result.list);
            setHasLoadedAll(result.hasLoadedAll);
            setIsLoading(false);
        }
        );
        console.timeEnd('Get Simple List');

    });

Thats better, but if we tried to run this effect we would run into an infinite loop. The problem occurs because useEffect is run on each render and if updating State variable will trigger a re-render. In order to get around this, we can pass an empty array as a parameter to the effect. Passing this array will only execute the effect on Mount and Unmount. The parameter can also be used further to only update if particular variables are modified.

///...

useEffect(() => {
        console.time('Get Simple List');
        setIsLoading(true);
        getList().then(result => {
            setList(result.list);
            setHasLoadedAll(result.hasLoadedAll);
            setIsLoading(false);
        }
        );
        console.timeEnd('Get Simple List');

    }, []);

The final task that remains is to go back to our data retrieval and update methods. As discussed we have a little work to do to make it work. We have to tidy up and update wherever we access this.setState() with the appropriate function we defined. With that done the final hook looks like the following:

import { useState, useEffect } from 'react'
import { FireStoreService } from 'js/services';

export default function useSimpleList(props) {
    const [error, setError] = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    const [list, setList] = useState([]);
    const [hasLoadedAll, setHasLoadedAll] = useState(false);
    const [previousItem, setPreviousItem] = useState(undefined);    

    useEffect(() => {
        console.time('Get Simple List');
        setIsLoading(true);
        getList().then(result => {
            setList(result.list);
            setHasLoadedAll(result.hasLoadedAll);
            setIsLoading(false);
        }
        );
        console.timeEnd('Get Simple List');

    }, []);

    async function updateData() {
        setIsLoading(true);
        getList().then(result => {
            setList(result.list);
            setHasLoadedAll(result.hasLoadedAll);
            setIsLoading(false);
        });
    }

    const getList = async () => {
        const { collection, orderBy, direction, pageSize } = props;
        let result = undefined;
        let updatedListCount = 0;

        try {
            if (previousItem) {
                result = await FireStoreService.getSimpleListPaged(collection, orderBy, direction, pageSize, previousItem);
                if (result && result.length > 0) {
                    setPreviousItem(result[result.length - 1][orderBy]);
                }
            }
            else {
                result = await FireStoreService.getSimpleList(collection, orderBy, direction, pageSize);

                if (result && result.length > 0) {
                    setPreviousItem(result[result.length - 1][orderBy]);

                }
            }

            //Get the number of records retrieved from query
            updatedListCount = result.length;
            //Append results of records retrieved to results array.
            result = [...list, ...result];
            
            return {
                list: result,
                hasLoadedAll: updatedListCount < pageSize
            }

        }
        catch (error) {
            setError("Unabled to load data");
            setIsLoading(false);
        }
    }

    return { list, hasLoadedAll, isLoading, error, updateData }
}

As this is simply a function we can return the variables we want to export as an object. We can see how these will be used when we use the hook.

import React from 'react';
import UseSimpleList from 'js/components/Hooks/useSimpleList';
import CommunityItem from 'js/components/Communities/List/communityItem';

const CommunityList = ({ props }) => {
    const { error, list, isLoading, hasLoadedAll, updateData } = UseSimpleList({ collection: 'communities', orderBy: 'communityName', direction: 'asc', pageSize: 3 });

    if (error) {
        return <p>{error}</p>;
    }

    if (!list)
        return (<p>No Data Yet</p>)

    return (
        <React.Fragment>
            <ul>
                {
                    list.map(currentItem => {
                        return (
                            <CommunityItem communityName={currentItem.communityName}
                                communityId={currentItem.slug} />
                        )
                    })
                }
            </ul>
            {isLoading && <p id="suspensePlaceholder">Loading ...</p>}
            {!isLoading && !hasLoadedAll &&
                <button id="loadMore" onClick ={updateData} >Load More</button>
            }
            {!isLoading && hasLoadedAll && <p id="loadingComplete">Yay! All Items Are Loaded</p>}
        </React.Fragment>
    )
}

export default CommunityList;

In order to use the hook we have to convert the communityList component to a Functional Component. This is because one of the rules of hooks is they cannot be called from classes. The hook is used by simply calling the function passing in the parameters as we did before. The returned object is deconstructed into variables using ES6 deconstruction so they can be used by the component for rendering.

Conclusion

Having played around and converted a render prop into a hook I can see how they are going to play a big part in the future of React. Hooks manage to solve one of the biggest problems with code reuse and as the FreeCodeCamp article explains it has been a bit of a journey from mixins, HoCs and Render Props. Hooks by far seem the simplest and more concise way to achieve this. It isn't going to be everyone's cup of tea and coming from classes it takes a little bit of a mind shift to understand. Having said that I didn't have too much difficulties converting my Render Prop and will make use of Hooks as I grow my application out. I would recommend going through the official documentation as there is a lot of information there that I have not covered.

I really enjoy working with React and the team at React have done an amazing job continuing to develop it.