DEV Community

Iliuta Stoica
Iliuta Stoica

Posted on

React (Context API + Hooks) Redux Pattern

React Context API + Hooks

Demo APP

We will be building a simple recipe app and show to hold state inside the react APP

Application Code

We will hold the data into a jsonblob here

https://jsonblob.com/api/jsonBlob/fddd0cec-8e0e-11ea-82f0-13fba022ad5b

The index.js file is just the main file to start our application.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById('root')
);

Inside the App.js file we will have the apiURL to hold the data source, a component for the recipes named RecipeList, a component for each recipe named Recipe and the main component App which will the wrapper for the recipes. We will have an empty header and footer. We will add bootstrap for styling the app.

const apiURL = `https://jsonblob.com/api/jsonBlob/fddd0cec-8e0e-11ea-82f0-13fba022ad5b`;

const RecipeList = ({ recipes }) => (
    <div className="container my-2">
        <div className="row">
            <div className="container-fluid text-center text-uppercase mb-3">
                <h2 className="text-slaned text-center">Recipe List</h2>
            </div>
        </div>
        <div className="row">
            {recipes.map((recipe) => <Recipe key={recipe.id} recipe={recipe} /> )}
        </div>
    </div>
);

const Recipe = ({ recipe }) => {
    const { readyInMinutes, title, id, sourceUrl } = recipe;
    return (
        <div className="col-10 mx-auto col-md-6 col-lg-4 my-3">
            <div className="card">
                <div className="card-body text-capitalize">
                    <h6>{title}</h6>
                    <h6 className="text-warning">
                        ready In Minutes: {readyInMinutes}
                    </h6>
                </div>
                <div className="card-footer">
                    <a style={{ margin: `0.25em` }}
                        data-id={id}
                        className="btn btn-primary text-center"
                        href={sourceUrl}
                        target="_blank"
                        rel="noopener noreferrer external">More Info</a>
                </div>
            </div>
        </div>
    )
};

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            apiResponse: [],
            loading: true,
        };
    }
    componentDidMount() {
        fetch(apiURL, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        })
            .then((response) => {
                return response.json();
            })
            .then((apiResponse) => {
                this.setState({ apiResponse });
                this.setState({ loading: false });
            })
            .catch((e) => console.error(e));
    }
    render() {
        let recipes = this.state.apiResponse.results;
        let loading = this.state.loading;

        return (
            <>
                <div className="container">
                    <div className="jumbotron py-4">
                        <h1 className="text-center">header</h1>
                    </div>
                </div>
                {loading ? (
                    <h3 className="text-center">loading recipes ...</h3>
                ) : (
                    <RecipeList recipes={recipes} />
                )}
                <div className="container">
                    <div className="jumbotron py-4">
                        <h3 className="text-center">footer</h3>
                    </div>
                </div>
            </>
        );
    }
}

export default App;

As you can see the state for the application is in the App component, which is a class component. If you want to have state inside your components, you need a class component.

So each class component can have independent state and can inherit state from a parent component through props.
This is called prop drilling and can be avoided with the context API.

Prop drilling

Prop drilling (also called "threading") refers to the process you have to go through to get data to parts of the React Component tree.
Prop drilling at its most basic level is simply explicitly passing values throughout the view of your application.

Context API

The Context API was introduction in React version 16.3.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

alt text

Context API uses createContext() to create a store that holds the context (the state).

React.createContext

const MyContext = React.createContext(defaultValue);

Creates a Context object. When React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree.

Context.Provider

<MyContext.Provider value={/* some value */}>

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.
Accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.

// Use the context decribed above 
class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.

Application Code with Context

Getting back to our application let's use the context API.
Create a context folder inside the src folder and add a index.js file with the following code:

src/context/index.js

import React, { Component } from 'react';

const apiURL = `https://jsonblob.com/api/jsonBlob/fddd0cec-8e0e-11ea-82f0-13fba022ad5b`;
const RecipeContext = React.createContext();

class RecipeProvider extends Component {
    state = {
        loading: true,
        recipes: [],
        search: '',
    };

    fetchRecipe = async () => {
        const recipeData = await fetch(apiURL, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
            },
        })
            .then((response) => {
                return response.json();
            })
            .catch((e) => console.error(e));

        const { results } = await recipeData;

        this.setRecipes(results);
        this.setLoading(false);
    };

    setLoading = (loadingState) => this.setState({ loading: loadingState });
    setRecipes = (list) => this.setState({ recipes: list });

    componentDidMount() {
        this.fetchRecipe();
    }

    render() {
        return (
            <RecipeContext.Provider value={this.state}>
                {this.props.children}
            </RecipeContext.Provider>
        );
    }
}
const RecipeConsumer = RecipeContext.Consumer;
export { RecipeProvider, RecipeConsumer, RecipeContext };

And now the main index.js file will look like this:

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { RecipeProvider } from './context/index';

ReactDOM.render(
    <React.StrictMode>
        <RecipeProvider>
            <App />
        </RecipeProvider>
    </React.StrictMode>,
    document.getElementById('root')
);

And inside the App.js we will import the new context RecipeContext in order to pass down the recipes.

import React, { useContext } from 'react';
import './App.css';
import RecipeList from './components/RecipeList';
import { RecipeContext } from './context/index';

function App() {
    const appContext = useContext(RecipeContext);
    const { loading, recipes } = appContext;

    return (
        <div>
            {loading ? (
                <h2 className="text-center">loading recipes ...</h2>
            ) : (
                <RecipeList recipes={recipes} />
            )}
        </div>
    );
}

export default App;

We will move the components inside the components folder, the Recipe.js and RecipeList.js files.

React Hooks

With React 16.8 we can use hooks to hold state also with functional components.

What are Functional Components?

There are two main types of components in React. Class Components and Functional Components. The difference is pretty obvious. Class components are ES6 classes and Functional Components are functions. The only constraint for a functional component is to accept props as an argument and return valid JSX.

Demo, a functional component

function Hello(props){
   return <div>Hello {props.name}</div>
}

or a simpler version

const Hello = ({name}) => <div>Hello {name}</div>

and here is the same component written as a class component

class Hello extends Component{
   render(){
      return <div>Hello {this.props.name}</div>
   }
}

What is a Hook?

A Hook is a special function that lets you “hook” into React features. For example, useState is a Hook that lets you add React state to function components.

In a functional component, we have no this, so we can’t assign or read this.state. Instead, we call the useState Hook directly inside our component.

What does useState do?

  • It declares a “state variable” and a function to update that variable. useState is a new way to use the exact same capabilities that this.state provides in a class. Normally, variables “disappear” when the function exits but state variables are preserved by React.

  • The only argument to the useState() Hook is the initial state. Unlike with classes, the state doesn’t have to be an object.

  • The useState hook returns a pair of values: the current state and a function that updates it. This is why we write const [count, setCount] = useState(). This is similar to this.state.count and this.setState in a class, except you get them in a pair.

In the example below the variable is called count and the function to update the variable is setCount.

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

The state starts as { count: 0 }, and we increment the count variable when the user clicks a button by calling setCount().

<button onClick={() => setCount(count + 1)}>Click me</button>

And you can simply call {count} to display the variable.

So useState lets us add local state to React function components, now let's move to other hooks.

What does useEffect do?

The Effect Hook, useEffect, adds the ability to perform side effects from a function component. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes, but unified into a single API.

By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates.
In this effect, we set the document title, but we could also perform data fetching or call some other imperative API.

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

We declare the count state variable, and then we tell React we need to use an effect. We pass a function to the useEffect Hook. This function we pass is our effect. Inside our effect, we set the document title using the document.title browser API. We can read the latest count inside the effect because it’s in the scope of our function. When React renders our component, it will remember the effect we used, and then run our effect after updating the DOM. This happens for every render, including the first one.

Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects. Whether or not you’re used to calling these operations “side effects” (or just “effects”), you’ve likely performed them in your components before.

If we would want to do the same effect with a class component, we would do it like this:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  ...

Now we have to duplicate the code between these two lifecycle methods in class. This is because in many cases we want to perform the same side effect regardless of whether the component just mounted, or if it has been updated.

Does useEffect run after every render?
Yes! By default, it runs both after the first render and after every update.

Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.

In some cases, cleaning up or applying the effect after every render might create a performance problem.

Re-rendering will re-run the useEffect() and the app will be inside a loop

You can tell React to skip applying an effect if certain values haven’t changed between re-renders. To do so, pass an array as an optional second argument to useEffect:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.

While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often.

useContext hook

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest above the calling component in the tree.

const value = useContext(MyContext);

A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization.

What is memoization?

Memoization is a powerful optimization technique that can greatly speed up your application, by storing the results of expensive function calls or a react component and returning the cached result when the same inputs occur again.

Our component would still re-execute, but React wouldn't re-render the child tree if all useMemo inputs are the same.

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

Or you can still optimize rendering by using React.memo.
React.memo is a higher order component (a component that returns another component). It’s similar to React.PureComponent but for function components instead of classes.

const ThemedButton = memo(({ theme }) => {
  // The rest of your rendering logic
  return <ExpensiveTree className={theme} />;
});

Another way to use memoization is to use the:

useCallback hook

Returns a memoized callback.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

useMemo hook

Returns a memoized value. Different from useCallback

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.

Remember that the function passed to useMemo runs during rendering. Don’t do anything there that you wouldn’t normally do while rendering. For example, side effects belong in useEffect, not useMemo. If no array is provided, a new value will be computed on every render.

useReducer hook

An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Here’s the counter example from the useState section, rewritten to use a reducer:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

React guarantees that dispatch function identity is stable and won’t change on re-renders.

State Lazy initialization

You can also create the initial state lazily. To do this, you can pass an init function as the third argument. The initial state will be set to init(initialArg).
It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action:

function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

Application Code with React Hooks

Getting back to our recipe application, we will update the files to use hooks.
Let's update the context index.js file

src/context/index.js

import React, { useState, useEffect } from 'react';

const apiURL = `https://jsonblob.com/api/jsonBlob/fddd0cec-8e0e-11ea-82f0-13fba022ad5b`;
const RecipeContext = React.createContext();

const RecipeProvider = (props) => {
    const [recipes, setRecipes] = useState([]);
    const [loading, setLoading] = useState(true);

    const fetchRecipe = async () => {
        try {
            const recipeData = await fetch(apiURL, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                },
            });
            const { results } = await recipeData.json();
            setRecipes(results);
            setLoading(false);
        } catch (e) {
            if (e) {
                console.log(e.message, 'Try updating the API key in App.js');
            }
        }
    };

    useEffect(() => {
        fetchRecipe();
    }, []);

    return (
        <RecipeContext.Provider value={{loading,recipes}} >
            {props.children}
        </RecipeContext.Provider>
    );
};
const RecipeConsumer = RecipeContext.Consumer;
export { RecipeProvider, RecipeConsumer, RecipeContext };

We updated the RecipeProvider component to be a functional component, we used the new hooks useState and useEffect to update the recipes and loading variables and we removed the methods setRecipes and setLoading that were updating the internal state with this.setState().

And now the <RecipeContext.Provider value={this.state}> is sending an object holding the variables value={{loading,recipes}}.

Building a Store - Redux Pattern

Let's update our recipe application to have a global store. First we create a store folder.

We need a reducer

We create a Reducer.js file inside the store folder.

import { SET_RECIPES, SET_ERROR } from './actionTypes';

const Reducer = (state, action) => {
    switch (action.type) {
        case SET_RECIPES:
            return {
                ...state,
                recipes: action.payload,
                loading: false,
            };
        case SET_ERROR:
            return {
                ...state,
                error: action.payload,
                loading: true,
            };
        default:
            return state;
    }
};

export default Reducer;

We created a reducer function that takes the state and an action as arguments designed for accessing and managing the global state of the application. This function works with conjunction with React’s own hook: useReducer().

ActionTypes

export const SET_RECIPES = 'SET RECIPES';
export const SET_ERROR = 'SET ERROR';

We create the action types just like the redux pattern inside the actionTypes.js file.

We need a Store

To create a global state we need a central store. The store is a higher-order component (HOC) which holds the context (the state).

Let's create a Store.js file inside the store folder.

import React, { createContext, useEffect, useReducer } from 'react';
import Reducer from './Reducer';
import { SET_RECIPES, SET_ERROR } from './actionTypes';

const initialState = {
    recipes: [],
    error: null,
    loading: true,
};

const apiURL = `https://jsonblob.com/api/jsonBlob/fddd0cec-8e0e-11ea-82f0-13fba022ad5b`;
const StoreContext = createContext(initialState);

const Store = ({ children }) => {
    const [state, dispatch] = useReducer(Reducer, initialState);

    const fetchRecipe = async () => {
        try {
            const recipeData = await fetch(apiURL, {
                method: 'GET',
                headers: {'Content-Type': 'application/json'},
            });
            const { results } = await recipeData.json();
            dispatch({ type: SET_RECIPES, payload: results });
        } catch (error) {
            if (error) {
                console.log(error);
                dispatch({ type: SET_ERROR, payload: error });
            }
        }
    };

    useEffect(() => {
        fetchRecipe();
    }, []);

    return (
        <StoreContext.Provider value={[state, dispatch]}>
            {children}
        </StoreContext.Provider>
    );
};

const StoreConsumer = StoreContext.Consumer;

export { Store, StoreConsumer, StoreContext };

We pass an initial default state object and the reducer function to React’s useReducer() as arguments then deconstruct its values.

const [state, dispatch] = useReducer(Reducer, initialState);

The state value points to the state object and the dispatch method is the reducer function that manages the state.

Then we pass the state and dispatch method to the context.

<StoreContext.Provider value={[state, dispatch]}>

To use the store and access its global state from anywhere in our application we need to wrap it around our main index.js file. We now use the Store component from the store folder.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Store } from './store/Store';

ReactDOM.render(
    <React.StrictMode>
        <Store>
            <App />
        </Store>
    </React.StrictMode>,
    document.getElementById('root')
);

Inside our App.js file all the children of the App component will have access to the store and its values.

This is our App.js file:

import React, { useContext } from 'react';
import './App.css';
import RecipeList from './components/RecipeList';
import { StoreContext } from './store/Store';

function App() {
    const appContext = useContext(StoreContext);
    const { loading, recipes } = appContext[0];

    return (
        <div>
            {loading ? (
                <h2 className="text-center">loading recipes ...</h2>
            ) : (
                <RecipeList recipes={recipes} />
            )}
        </div>
    );
}

export default App;

In order to use the {loading,recipes} we have to change the code:

const { loading, recipes } = appContext[0];

because in the Provider we are sending an array with the state as the first element <StoreContext.Provider value={[state, dispatch]}>.

Thank you for watching this tutorial!

Oldest comments (0)