React Context API: Managing State with Ease

August 20, 2018 0 Comments

React Context API: Managing State with Ease

 

 

TL;DR: The React Context API isn't a new thing on React's ecosystem. However, the React's 16.3.0 release brought a lot of improvements to this API. These improvements are so overwhelming that they greatly reduce the need for Redux and other advanced state management libraries. In this article, you will learn, through a practical tutorial, how the new React Context API replaces the need for Redux on small React applications.

"Learn how to migrate from Redux to the new React Context API in this practical tutorial."


Quick Review on Redux

Before diving into the React Context API, we need to do a quick review on Redux, so we can compare both. Redux is a JavaScript library that facilitates state management. Redux is not tied to React itself. Developers from all around the world have been using Redux with popular JavaScript frontend frameworks such as React and Angular.

To be clear, in this context, state management means handling changes that occur upon a particular event that occurs on a Single Page App (SPA). For example, events like the click of a button or an async message coming from the server can trigger changes to the app's state.

In Redux, particularly, there are a few things that you have to keep in mind:


  1. The state of the entire app is stored in a single object (known as the source of truth).

  2. To change the state, you need to dispatch actions that describes what needs to happen.

  3. You cannot change properties of objects or make changes to existing arrays. In Redux, you must always return a new reference to a new object or a new array.

If you are not familiar with Redux and you want to learn more, please, check this practical tutorial on Redux.

React Context API Introduction

The React Context API provides a way to pass data through the component tree without having to pass props down manually to every level. In React, data is often passed from a parent to its child component as a property.

Using the new React Context API depends on three main steps:


  1. Passing the initial state to React.createContext. This function then returns an object with a Provider and a Consumer.

  2. Using the Provider component at the top of the tree and making it accept a prop called value. This value can be anything!

  3. Using the Consumer component anywhere below the Provider in the component tree to get a subset of the state.

As you can see, the concepts involved are actually not that different from Redux. The fact is, even Redux uses the React Context API underneath its public API. However, only recently the Context API reached a level of maturity high enough to be used in the wild.

Creating a React App with Redux

As mentioned, the goal of this article is to show you how the new Context API can replace Redux for small apps. Therefore, you will start by creating a simple React app with Redux and, after that, you will learn how to remove this state management library so you can take advantage of the React Context API.

The sample application you will build is an app that handles a list of some popular foods and their origin. This app will also include a search functionality to enable users to filter the list based on some keyword.

In the end, you will have an app that looks like this:

Project Requirements

As this article uses only React and some NPM libraries, you will need nothing else than Node.js and NPM installed in your development machine. If you don't have Node.js and NPM yet, check out the official installation procedures to install both.

After installing these dependencies, you will need to install the create-react-app tool. This tool helps developers getting started with React. So, to install it, open a terminal and run the following command:

npm i -g create-react-app

Scaffolding the React App

With create-react-app installed, you will have to move to the directory where you want to put your project and execute the following command:

create-react-app redux-vs-context

After a few seconds, create-react-app will have finished creating your app. So, after that, you can move into the new directory created by this tool and install Redux:

# move into your project
cd redux-vs-context # install Redux
npm i --save redux react-redux

Note: redux is the main library and react-redux is a library that facilitates the interaction between React and Redux. In short, the latter acts as a proxy between React and Redux.

Developing React Apps with Redux

Now that you have your React app structured and that you installed Redux, open your project in your preferred IDE. From there, you will create three files into the src directory:


  • foods.json: This file will hold a static array of foods and their origin.

  • reducers.js: This file will manage the state of the Redux version of your app.

  • actions.js: This file that will hold the functions that will trigger changes in the state of the Redux version of your app.

So, to start, you can open the foods.json file and add the following content to it:

[ { "name": "Chinese Rice", "origin": "China", "continent": "Asia" }, { "name": "Amala", "origin": "Nigeria", "continent": "Africa" }, { "name": "Banku", "origin": "Ghana", "continent": "Africa" }, { "name": "Pão de Queijo", "origin": "Brazil", "continent": "South America" }, { "name": "Ewa Agoyin", "origin": "Nigeria", "continent": "Africa" }
]

As you can see, there is nothing special about this file. It is just an array of different food from different countries.

After defining the foods.json file, you can focus on defining your Redux store. To recap, the store is the place where you keep the single source of truth of the state of your app. So, open the reducers.js file and add the following code to it:

import Food from './foods'; const initialState = { food: Food, searchTerm: '',
}; export default function reducer(state = initialState, action) { // switch between the action type switch (action.type) { case 'SEARCHINPUTCHANGED': const {searchTerm} = action.payload; return { ...state, searchTerm: searchTerm, food: searchTerm ? Food.filter( (food) => (food.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1) ) : Food, }; default: return state; }
}

In the code above, you can see that the reducer function receives two parameters: state and action. When you start your React application, this function will get the initialState defined right before it and, when you dispatch instances of an action, this function will get the current state (not the initialState anymore). Then, based on the contents of these actions, the reducer function will generate a new state for your app.

Next, you have to define what these actions are. Actually, to keep things simple, you will define a single action that will be triggered when users input a search term in your app. So, open the actions.js file and insert the following code into it:

function searchTermChanged(searchTerm) { return { type: 'SEARCHINPUTCHANGED', payload: {searchTerm}, };
} export default { searchTermChanged,
};

With this action creator in place, the next thing you need to do is to wrap your App component into the Provider component that is available on react-redux. This provider is responsible for making your single source of truth (i.e., the store) to your React app.

To use this provider, first, you will create your app's store using the initialState defined in the reducers.js file. Then, you will pass this store to your App with the help of Provider. To accomplish these tasks, you will have to open the index.js file and replace its contents with:

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import reducers from './reducers';
import App from './App'; // Creating the store using the reducers info.
// That's because reducers are the building blocks of a Redux Store.
const store = createStore(reducers); ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById('root')
);

That's it! You just finished configuring Redux in your React app. Now, you have to implement the UI (User Interface), so your users can use the features implemented in this section.

Building the React Interface

Now that you have the core of your application ready to go, you can focus on building your user interface. For that, open your App.js file and replace its contents with this:

import React from 'react';
import {connect} from 'react-redux';
import actions from './actions';
import './App.css'; function App({food, searchTerm, searchTermChanged}) { return ( <div> <div className="search"> <input type="text" name="search" placeholder="Search" value={searchTerm} onChange={e => searchTermChanged(e.target.value)} /> </div> <table> <thead> <tr> <th>Name</th> <th>Origin</th> <th>Continent</th> </tr> </thead> <tbody> {food.map(theFood => ( <tr key={theFood.name}> <td>{theFood.name}</td> <td>{theFood.origin}</td> <td>{theFood.continent}</td> </tr> ))} </tbody> </table> </div> );
} export default connect(store => store, actions)(App);

For those not used to Redux, the only thing that they might not be familiar with is the connect function used to encapsulate the App component. This function is actually a High Order Component (HOC) that acts as the glue between your app and Redux.

If you run your app now, you will be able to use it in your browser:

npm run start

However, as you can see, the app right now is ugly. So, to make it look a little bit better, you can open the App.css file and replace its contents with:

table { width: 100%; border-collapse: collapse; margin-top: 15px; line-height: 25px;
} th { background-color: #eee;
} td, th { text-align: center;
} td:first-child { text-align: left;
} input { min-width: 300px; border: 1px solid #999; border-radius: 2px; line-height: 25px;
}

React app implemented with Redux

Done! You now have a basic React and Redux app and can start learning about how to migrate to the Context API.

Implementing React Apps with React Context API

In this section, you will learn how to migrate the Redux version of your app to the React Context API.

Fortunately, as you will see, you won't really need to do a lot of refactoring to switch between Redux and the Context API.

For starter, you will have to remove every trace of Redux from your app. For that, go to your terminal and remove both the redux and react-redux libraries:

npm rm redux react-redux

After that, you can remove the import statements that reference these libraries. So, open the App.js file and remove these lines:

import {connect} from 'react-redux';
import actions from './actions';

Then, still in this file, replace the last line (the one that starts with export default) with this:

export default App;

With these changes in place, you can rewrite your app with the Context API.

Migrating from Redux to React Context API

To convert the previous app from a Redux powered app to using the Context API, you will need a context to store the app's data (this context will replace the Redux Store). Also, you will need a Context.Provider component which will have a state, a props, and a normal React component lifecycle.

Therefore, you will need to create a providers.js file in the src directory and add the following code to it:

import React from 'react';
import Food from './foods'; const DEFAULTSTATE = { allFood: Food, searchTerm: '' }; export const ThemeContext = React.createContext(DEFAULTSTATE); export default class Provider extends React.Component { state = DEFAULT_STATE; searchTermChanged = searchTerm => { this.setState({searchTerm}); }; render() { return ( <ThemeContext.Provider value={{ ...this.state, searchTermChanged: this.searchTermChanged, }}> {this.props.children} </ThemeContext.Provider>); }
}

The Provider class defined in the code above is responsible for encapsulating other components inside the ThemeContext.Provider. By doing that, you enable these components to have access to your app's state and to the searchTermChanged function that provides a way to change this state.

To consume these values later in the component tree, you will need to initiate a ThemeContext.Consumer component. This component will need a render function that will receive the above value props as arguments to use at will.

So, next, you need to create a filed called consumer.js in the src directory and write the following code into it:

import React from 'react';
import {ThemeContext} from './providers'; export default class Consumer extends React.Component { render() { const {children} = this.props; return ( <ThemeContext.Consumer> {({allFood, searchTerm, searchTermChanged}) => { const food = searchTerm ? allFood.filter( food => food.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1 ) : allFood; return React.Children.map(children, child => React.cloneElement(child, { food, searchTerm, searchTermChanged, }) ); }} </ThemeContext.Consumer> ); }
}

Now, to finalize the migration, you will open your index.js file, and inside the render() function, wrap the App component with the Consumer component. Also, you will wrap the Consumer inside the Provider component. Exactly as shown here:

import React from 'react';
import ReactDOM from 'react-dom';
import Provider from './providers';
import Consumer from './consumer';
import App from './App'; ReactDOM.render( <Provider> <Consumer> <App /> </Consumer> </Provider>, document.getElementById('root')
);

Done! You just finished migrating from Redux to the React Context API. If you run your app now, you will see that the whole thing is working just like before. The difference now is that your app is not using Redux more.

"The new React Context API s a great alternative to Redux in small React applications."


Aside: Securing React Apps with Auth0

As you will learn in this section, you can easily secure your React applications with Auth0, a global leader in Identity-as-a-Service (IDaaS) that provides thousands of enterprise customers with modern identity solutions. Alongside with the classic username and password authentication process, Auth0 allows you to add features like Social Login, Multifactor Authentication, Passwordless Login, and much more with just a few clicks.

To follow along the instruction describe here, you will need an Auth0 account. If you don't have one yet, now is a good time to sign up for a free Auth0 account.

Also, if you want to follow this section in a clean environment, you can easily create a new React application with just one command:

npx create-react-app react-auth0

Then, you can move into your new React app (which was created inside a new directory called react-auth0 by the create-react-app tool), and start working as explained in the following subsections.

Setting Up an Auth0 Application

To represent your React application in your Auth0 account, you will need to create an Auth0 Application. So, head to the Applications section on your Auth0 dashboard and proceed as follows:


  1. click on the Create Application button;

  2. then define a Name to your new application (e.g., "React Demo");

  3. then select Single Page Web Applications as its type.

  4. and hit the Create button to end the process.

After creating your application, Auth0 will redirect you to its Quick Start tab. From there, you will have to click on the Settings tab to whitelist some URLs that Auth0 can call after the authentication process. This is a security measure implemented by Auth0 to avoid the leaking of sensitive data (like ID Tokens).

So, when you arrive at the Settings tab, search for the Allowed Callback URLs field and add http://localhost:3000/callback into it. For this tutorial, this single URL will suffice.

That's it! From the Auth0 perspective, you are good to go and can start securing your React application.

Dependencies and Setup

To secure your React application with Auth0, there are only three dependencies that you will need to install:


  • auth0.js: This is the default library to integrate web applications with Auth0.

  • react-router: This is the de-facto library when it comes to routing management in React.

  • react-router-dom: This is the extension to the previous library to web applications.

To install these dependencies, move into your project root and issue the following command:

npm install --save auth0-js react-router react-router-dom

Note: As you want the best security available, you are going to rely on the Auth0 login page. This method consists of redirecting users to a login page hosted by Auth0 that is easily customizable right from your Auth0 dashboard. If you want to learn why this is the best approach, check the Universal vs. Embedded Login article.

After installing all three libraries, you can create a service to handle the authentication process. You can call this service Auth and create it in the src/Auth/ directory with the following code:

// src/Auth/Auth.js
import auth0 from 'auth0-js'; export default class Auth { constructor() { this.auth0 = new auth0.WebAuth({ // the following three lines MUST be updated domain: '<AUTH0DOMAIN>', audience: 'https://<AUTH0DOMAIN>/userinfo', clientID: '<AUTH0CLIENTID>', redirectUri: 'http://localhost:3000/callback', responseType: 'token idtoken', scope: 'openid profile' }); this.getProfile = this.getProfile.bind(this); this.handleAuthentication = this.handleAuthentication.bind(this); this.isAuthenticated = this.isAuthenticated.bind(this); this.login = this.login.bind(this); this.logout = this.logout.bind(this); this.setSession = this.setSession.bind(this); } getProfile() { return this.profile; } handleAuthentication() { return new Promise((resolve, reject) => { this.auth0.parseHash((err, authResult) => { if (err) return reject(err); console.log(authResult); if (!authResult || !authResult.idToken) { return reject(err); } this.setSession(authResult); resolve(); }); }) } isAuthenticated() { return new Date().getTime() < this.expiresAt; } login() { this.auth0.authorize(); } logout() { // clear id token and expiration this.idToken = null; this.expiresAt = null; } setSession(authResult) { this.idToken = authResult.idToken; this.profile = authResult.idTokenPayload; // set the time that the id token will expire at this.expiresAt = authResult.expiresIn * 1000 + new Date().getTime(); }
}

The Auth service that you just created contains functions to deal with different steps of the sign in/sign up process. The following list briefly summarizes these functions and what they do:


  • getProfile: This function returns the profile of the logged-in user.

  • handleAuthentication: This function looks for the result of the authentication process in the URL hash. Then, the function processes the result with the parseHash method from auth0-js.

  • isAuthenticated: This function checks whether the expiry time for the user's ID token has passed.

  • login: This function initiates the login process, redirecting users to the login page.

  • logout: This function removes the user's tokens and expiry time.

  • setSession: This function sets the user's ID token, profile, and expiry time.

Besides these functions, the class contains a field called auth0 that is initialized with values extracted from your Auth0 application. It is important to keep in mind that you have to replace the <AUTH0DOMAIN> and <AUTH0CLIENTID> placeholders that you are passing to the auth0 field.

Note: For the <AUTH0DOMAIN> placeholders, you will have to replace them with something similar to your-subdomain.auth0.com, where your-subdomain is the subdomain you chose while creating your Auth0 account (or your Auth0 tenant). For the <AUTH0CLIENT_ID>, you will have to replace it with the random string copied from the Client ID field of the Auth0 Application you created previously.

Since you are using the Auth0 login page, your users are taken away from the application. However, after they authenticate, users automatically return to the callback URL that you set up previously (i.e., http://localhost:3000/callback). This means that you need to create a component responsible for this route.

So, create a new file called Callback.js inside src/Callback (i.e., you will need to create the Callback directory) and insert the following code into it:

// src/Callback/Callback.js
import React from 'react';
import { withRouter } from 'react-router'; function Callback(props) { props.auth.handleAuthentication().then(() => { props.history.push('/'); }); return ( <div> Loading user profile. </div> );
} export default withRouter(Callback);

This component, as you can see, is responsible for triggering the handleAuthentication process and, when the process ends, for pushing users to your home page. While this component processes the authentication result, it simply shows a message saying that it is loading the user profile.

After creating the Auth service and the Callback component, you can refactor your App component to integrate everything together:

// src/App.js import React from 'react';
import {withRouter} from 'react-router';
import {Route} from 'react-router-dom';
import Callback from './Callback/Callback';
import './App.css'; function HomePage(props) { const {authenticated} = props; const logout = () => { props.auth.logout(); props.history.push('/'); }; if (authenticated) { const {name} = props.auth.getProfile(); return ( <div> <h1>Howdy! Glad to see you back, {name}.</h1> <button onClick={logout}>Log out</button> </div> ); } return ( <div> <h1>I don't know you. Please, log in.</h1> <button onClick={props.auth.login}>Log in</button> </div> );
} function App(props) { const authenticated = props.auth.isAuthenticated(); return ( <div className="App"> <Route exact path='/callback' render={() => ( <Callback auth={props.auth}/> )}/> <Route exact path='/' render={() => ( <HomePage authenticated={authenticated} auth={props.auth} history={props.history} />) }/> </div> );
} export default withRouter(App);

In this case, you are actually defining two components inside the same file (just for the sake of simplicity). You are defining a HomePage component that shows a message with the name of the logged-in user (that is, when the user is logged in, of course), and a message telling unauthenticated users to log in.

Also, this file is making the App component responsible for deciding what component it must render. If the user is requesting the home page (i.e., the / route), the HomePage component is shown. If the user is requesting the callback page (i.e., /callback), then the Callback component is shown.

Note that you are using the Auth service in all your components (App, HomePage, and Callback) and also inside the Auth service. As such, you need to have a global instance for this service, and you have to include it in your App component.

So, to create this global Auth instance and to wrap things up, you will need to update your index.js file as shown here:

// src/index.js import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Auth from './Auth/Auth';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker'; const auth = new Auth(); ReactDOM.render( <BrowserRouter> <App auth={auth} /> </BrowserRouter>, document.getElementById('root')
);
registerServiceWorker();

After that, you are done! You just finished securing your React application with Auth0. If you take your app for a spin now (npm start), you will be able to authenticate yourself with the help of Auth0, and you will be able to see your React app show your name (that is, if your identity provider does provide a name).

If you are interested in learning more, please, refer to the official React Quick Start guide to see, step by step, how to properly secure a React application. Besides the steps shown in this section, the guide also shows:

Conclusion

Redux is an advanced state management library that should be used when building large scale React apps. The Context API, on the other hand, can be used in small-scale React apps where byte-sized changes are made. By using the Context API, you do not have to write a lot of code such as reducers, actions, etc. to work out the logic exhibited by state changes.


Tag cloud