React Context and Hooks: An Open Source Project to Understand How They Work

React Context and Hooks: An Open Source Project to Understand How They Work

Intermediate level article

There are different approaches regarding the best ways to learn something new, and one of them is by doing. I agree with that approach, as long as the basics are already clear, and you have a general mental model that gives you the right context about what you are learning.

For instance, if you are going to learn how to use Context and Hooks from the React API, you already need to be familiar with the following topics, otherwise you will be totally lost:

  • Functional components
  • React Life-cycle Events
  • The concept of State and State Management in JavaScript
  • The concept of a Hook
  • Context and Scope JavaScript concepts
  • The DOM
  • JavaScript modern features

If you feel comfortable with the above topics, keep reading; otherwise, you can always come back to this later.

This time, I want to share with you my experience building a React App from the ground up using the Context React object and React Hooks, no Class Components included, just Functional Components.

The Project

A simple blog with a React App in the front end that allows you to search and read blog articles (built with the Context and Hooks React features). The articles are retrieved from a back end application built in NodeJS, fetching the data via API calls.

You can find the open source project here.

The Objective

My objective with this project is to create a simple web app that serves as a reference for those having trouble grasping the concepts and practical aspects of using the React Context object and hooks to build React Apps.

The App Architecture

The Front End

The front end is a React App built using Context, Hooks and Functional Components.

Remember that a Context object is a JavaScript object that allows you to manage the state (data) of your application. In this project, we have a Context object that helps us handle the article's data fetched from the back end (Context.js) and another Context that helps us handle the articles that should be available to some components in order to be shown to the user after a search has been requested (SearchContext.js).

The Back End

The back end is built with NodeJS and Express. Its only purpose is to make an end-point available to serve the articles data in JSON format when requested from a client, in our case, from the React App.

The Data

For this version, I did not include any database, but I used the file system to save the articles. Why? Because the focus of this project is mainly the front end, and this approach to store data is good enough to make our NodeJS API work.

Why Use Context and Hooks

There are pros and cons regarding the use of these React API new features. Nevertheless, here are the ones I found the most relevant during this project:

  • Pros: Using Context allows you to pass data to any component in your app without having to pass it manually every level down the DOM tree. For this specific project, the Context feature allowed me to manage the state of the blog posts in a single component (the context provider) that could be imported in any other component, in order to give it access to the data that has been previously retrived from the back end via an API call.

  • Cons: Right now, it is harder to test components that use data from the Context providers when using Jest than testing them the traditional way. The other aspect is that using Hooks makes it "more magical" when managing the state of your application data than when you are using the tradition life cycle methods from a Class Component.

React Hooks vs Traditional Life Cycle Methods

I assume you are familiar with the componentDidMount, componentDidUpdate, and the other life cycle methods of React. In brief, and being simplistic for learning purposes, some of the Hooks allow you to do the same as the life cycle methods, but from within Functional Components, there is no need to write a Class Component to initialize and handle the state of the component.

Let's see an example from the project using the useState() and useEffect React Hooks. Check the following code, including the commented code which explains what every line is written for:

// Context.js

import React, { useState, useEffect } from "react"; // imports React, and the useState and useEffect basic hooks from react library
import axios from "axios"; // imports axios from the axios package to make the API call to the back-end

const Context = React.createContext(); // creates a Context object from the React.createContext() method. You will reference this Context object when the blog posts data fetched from the NodeJS API needs to be accessible by other components at different nesting levels.

function ContextProvider() {} // Functional component definition for a component named ContextProvider. This Functional Component will be in charged of fetching the data from the back end and handle the state (blog articles) data of the application

export { ContextProvider, Context }; // export the ContextProvider functional component, and the Context object to make them available to other modules in the React app

With the previous code, we have created a file Context.js whose only responsability will be to give to other components access to the articles' data, which is retrieved from the back end. To do so, we need to create a new Context (const Context = React.createContext()), and a Functional Component that allows us to provide that Context to other components (function ContextProvider( ) {})

Now that we have the basic structure of our file to handle the articles' state using our own Context, let's write the code inside the ContextProvider Functional Component, which will set the initial state and handle any changes:

import React, { useState, useEffect } from "react";
import axios from "axios";

const Context = React.createContext();

function ContextProvider({ children }) {
  const [articles, setArticles] = useState([]); // useState() hook call, that initializes the state of the articles to an empty array

  useEffect(() => {
    // useEffect hook call which will be invoked the first time the DOM mount. it is like using componentDidMount in Class Components
    fetchArticles(); // the function that will be called as soon as the DOM mounted
  }, []);

  async function fetchArticles() {
    // the asyncronous definition of the fetchArticles function that will retrieve the articles from the NodeJS api
    try {
      const content = await axios.get("/api/tutorials"); // the API call to fetch the articles from the back end
      setArticles(content.data); // the setArticles function allows us to update the state of the component via the useState() hook
    } catch (error) {
      console.log(error);
    }
  }

  return <Context.Provider value={{ articles }}>{children}</Context.Provider>; // the returned value from the component
}

export { ContextProvider, Context };

Let's take a closer look at every line written above.

The ContextProvider Component

function ContextProvider({ children }) {...} : This is the Functional Component definition that accepts a parameter called children. The children parameter is any Functional Component that will be receiving the state being handled by this ContextProvider function, and are children components of the ContextProvider component. Check out this example.

The curly braces included in {children} , may appear strange to you. This is the way the new JavaScript features allow us to deconstruct an object or array. For example:

const fullName = { firstName: "Nayib", lastName: "Abdalá" };
const { firstName, lastName } = fullName; // JS object deconstruction

console.log(firstName); // Nayib
console.log(lastName); // Abdalá

In brief, the const [articles, setArticles] = useState([]); line helped us initialize and handle the state of the articles that will be fetched from the back end. Let's see how.

The Initialization of the App State with the useState() Hook

const [articles, setArticles] = useState([]);: Does this line look strange to you? It is simple. The const keyword allows us to declare a constant called articles , and one called setArticles. The values assigned to each of these constants are the returned values from calling the useState() hook, which returns an array with 2 elements, and the deconstruct JavaScript feature allows us to assign each of those elements to each constant we have defined on the left side of the expression const [articles, setArticles] = useState([]);.

The array returned by the useState() hook is an array containing the current state for a given variable, and a function that updates that state and can be used at any time in your Functional Component in order to update that state. In this case, we are initializing the value of articles to an empty array (when passing [] to the useState([]) function).

You can learn more about the useState() hook here.

Listening for State Changes with the useEffect() Hook

useEffect(() => { ... }, []):

The useEffect() hook will run after every completed render, but you can set it to only run if a certain value has changed. useEffect() receives two parameters: a function, and the second argument is the configuration of when the first parameter function should be called.

If you pass an empty array as a second parameter, the function should be called only the first time the complete render happens. If you pass one or more variables names as elements of the array passed as the second argument to useEffect(), every time there is a change in the value of any of those variables, the function passed as a first argument to useEffect() will be called.

In our case, the function passed as a first argument to useEffect() , will be called only the first time the DOM renders, as we are passing an empty array as a second argument to useEffect(() => { ... }, []). You can learn more about the useEffect() hook here.

Every time the useEffect(() => { ... }, []) hook is called, the fetchArticles() function will be called, which will fetch the articles' data from the back end NodeJS API of this project.

Once the fetchArticles() is called, the program in the body of this fuction will call the setArticles(content.data); function, which receives, as an argument, the content.data data fetched from the API, and will set the returned value from content.date as the updated value of articles.

This is how the useEffect() hook allow us to listen to new renders of the DOM, and execute an action once or every time there is a change in the mounted DOM, or any specific variable that we want to pass to the useEffect() hook as a second argument.

Returning the Context Provider that will Give Access to the State to Other Components

Once we have a clear understanding of how to handle the state of our articles, we now need to return what is required so that we can make the articles state available to other components. To do so, we need to have access to our Provider React component, so that we can share the data that is initialized and handled in the ContextProvider component with other components.

Every React Context object has two components as methods when creating it by using the React API React.createContext() function:

  • The Provider method - A component that provides the value
  • The Consumer method - A component that is consuming the value

The Provider React component allows children components to consume any data the Provider has access to.

The way you make the state of the ContextProvider component available is by returning a Context.Provider React component, and passing a value prop containing the articles data, in order to make it available to any consuming components that are decendants of this Provider.

What?! I know, it seems confusing, but it is actually simple. Let's go trough the code in chunks to make it clearer:

When calling the <Context.Provider /> component, and passing the variables you include in the value props to that Provider component, which in our case is the articles variable, you will give any descendant component that might be wrapped by the Provider access to that variable.

If we log the <Context.Provider /> component to the console for our project example, you will see the following:

[Click to expand] <Context.Provider />
  Props: {value: {…}, children: {…}}
    value: {articles: Array(2)}
    ...
  Nodes: [div.wrapper]

Do not get scared about the details; what you see above is basically the Provider component which has access to the data you have given access to via the value prop.

To sum it up, you need to return a Provider component from your ContextProvider component, with the data that you need to make available to other children components: return <Context.Provider value={{ articles }}>{children}</Context.Provider>;

For instance, all the components wrapped in the <ContextProvider /> component below, will have access to the Context data (check out the file in the repo) :

<ContextProvider>
  /* all the children components called here will have access to the data from
  the ContextProvider component */
</ContextProvider>

If the above is overwhleming, don't worry. Read it again. The take-away is that you need to wrap all the children elements that will need access to the data from your Provider in the Context.Provider component.

Take a break...

The next section is similar to this one, but it explains the <ContextProviderSearch /> component I created to handle the data of a given search.

The Use of Context as a Way to Separate Concerns and Handle Data

As a separate concern in our application, we will need a new Context that handles the state of the articles that should be shown to the user when a given search query takes place.

I have called this new Context the ContextProviderSearch. It depends on the articles data from the Context.js.

Let's take a look at the SearchContext.js file to understand how the Context object from the previous section is used to access the articles in this case:

import React, { useState, useContext } from "react";
// code omitted
import { Context as AppContext } from "./Context"; // imports the Context provided by Context.js
const Context = React.createContext();
// code omitted

function ContextProviderSearch({ children }) {
  // code omitted
  const { articles } = useContext(AppContext); // Access the articles array from the Context.js file

  // code omitted

  return (
    <Context.Provider
      value={
        {
          /*all the props that will be required by consumer components*/
        }
      }
    >
      {/* any consumer component*/}
    </Context.Provider>
  );
}

export { ContextProviderSearch, Context };

The most important lines of this file for our purpose are import { Context as AppContext } from "./Context" and const { articles } = useContext(AppContext).

The import { Context as AppContext } from "./Context" helps us import the context from our Context,js file.

The const { articles } = useContext(AppContext) expression uses the useContext() React hook, which accepts the AppContext as an argument, and returns the current context value we imported from Context.js. Using the deconstruct JavaScript feature, we create a constant with the articles array, to which the AppContext has access to.

This way, our ContextProviderSearch now has access to the Context from Context.js.

In brief, you can use the useContext React hook to have access to any Context you have created in your application in order to access the state that the given Context manage.

The SearchContext.js file includes some logic that is out of the scope of this article. If you have any questions about it, just ask me.

Things to be Improved in This Project

I created this project with an educational objective. There are several things that could be improved. I'm going to list some of them below, in case you are curious or have alrady identified them while checking the repo:

  • Testing: Additional unit tests should be added to check that the contexts data management is well. Also, adding tests to the back end NodeJS API would be a good idea.
  • Data Storage: For educational purposes, it is ok to store the articles in the file system. Nevertheless, it would be a better idea to integrate an SQL or NoSQL database to the project. Some options are Posgres with Squelize as a ORM, or MongoDB with Mongoose as a DRM.
  • Browser data Storage: The articles data is temporarly stored in the Window.localStorage storage object once it is fetched from the Context.js via the NodeJS API. The Window.localStorage has a storage size limit that might be not enough when handling several articles.
  • Lazy load: You could add the Lazy Loading utility to improve the size of the files created by webpack.
  • Add API authentication
  • Implement error boundaries
  • Implement type-checking for the React application

If you are not familiar with the concepts from the list above, check them out and try to implement them by cloning the repository. The exercise will strengthen your React skills.

The Repository

You can find the Open-Source project here.

I hope this article and project will serve as a reference for you to understand how to use Context and Hooks in your React apps.

Article originally posted at personal-blog-nayib-node.herokuapp.com