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.”
Tweet This
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:
- The state of the entire app is stored in a single object (known as the source of truth).
- To change the state, you need to dispatch
that describes what needs to happen.actions
- 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:
- Passing the initial state to
. This function then returns an object with aReact.createContext
and aProvider
.Consumer
- Using the
component at the top of the tree and making it accept a prop calledProvider
. This value can be anything!value
- Using the
component anywhere below the Provider in the component tree to get a subset of the state.Consumer
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:
is the main library andredux
is a library that facilitates the interaction between React and Redux. In short, the latter acts as a proxy between React and Redux.react-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:
: This file will hold a static array of foods and their origin.foods.json
: This file will manage the state of the Redux version of your app.reducers.js
: This file that will hold the functions that will trigger changes in the state of the Redux version of your app.actions.js
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 'SEARCH_INPUT_CHANGED': 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: 'SEARCH_INPUT_CHANGED', 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; }
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 DEFAULT_STATE = { allFood: Food, searchTerm: '' }; export const ThemeContext = React.createContext(DEFAULT_STATE); 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.”
Tweet This
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:
- click on the Create Application button;
- then define a Name to your new application (e.g., "React Demo");
- then select Single Page Web Applications as its type.
- 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:
: This is the default library to integrate web applications with Auth0.auth0.js
: This is the de-facto library when it comes to routing management in React.react-router
: This is the extension to the previous library to web applications.react-router-dom
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: '<AUTH0_DOMAIN>', audience: 'https://<AUTH0_DOMAIN>/userinfo', clientID: '<AUTH0_CLIENT_ID>', redirectUri: 'http://localhost:3000/callback', responseType: 'token id_token', 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:
: This function returns the profile of the logged-in user.getProfile
: This function looks for the result of the authentication process in the URL hash. Then, the function processes the result with thehandleAuthentication
method fromparseHash
.auth0-js
: This function checks whether the expiry time for the user's ID token has passed.isAuthenticated
: This function initiates the login process, redirecting users to the login page.login
: This function removes the user's tokens and expiry time.logout
: This function sets the user's ID token, profile, and expiry time.setSession
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 <AUTH0_DOMAIN>
and <AUTH0_CLIENT_ID>
placeholders that you are passing to the auth0
field.Note: For the
placeholders, you will have to replace them with something similar to<AUTH0_DOMAIN>
, whereyour-subdomain.auth0.com
is the subdomain you chose while creating your Auth0 account (or your Auth0 tenant). For theyour-subdomain
, you will have to replace it with the random string copied from the Client ID field of the Auth0 Application you created previously.<AUTH0_CLIENT_ID>
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.About the author
Abdulazeez Adeshina
Software Developer and Student