A blog about React and related technologies

Sharing global data in Next.js with custom App and useContext Hook

How to use some global data in Next.js pages is a very common question new developers have. In this post, we'll do it with a custom App component and useContext Hook that's included in new React 16.8.

Using the React Context API with Next.js has been possible for some time, there's even a with-context-api example in the Next.js GitHub repository. It's a good example, but now with new React 16.8 we can use Context in a much simpler way.

We'll build a simple login form and store user data in React Context with the new useContext Hook, to be able to get that data in any component.

Custom App Component

Next.js allows us to create a special _app.js file inside the pages folder to be able to override its App component with a custom component. It allows us to persist the layout between pages and other cool features. One of those features is being able to share global data to all pages.

Here's an example override of the App component:

import React from 'react';
import App, { Container } from 'next/app';

class MyApp extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  render() {
    const { Component, pageProps } = this.props;

    return (
      <Container>
        <Component {...pageProps} />
      </Container>
    );
  }
}

export default MyApp;

The <Component {...pageProps} /> part represents individual pages. When you navigate between different pages of your website, only the children data of <Component {...pageProps} /> changes.

React Context

A common functionality that many websites need is the ability to share info about the signed in user throughout the site. In this example, we'll use React Context to do achieve this. Context makes it possible to pass data through the component tree without having to pass props down manually at every level.

First we need to create a components/UserContext.js component file where we'll create a new context:

import { createContext } from 'react';

const UserContext = createContext();

export default UserContext;

This file doesn't need to contain any other code, although you could include some initial data. We'll import this UserContext component inside the _app.js file and set the value via the context provider:

<Container>
  <UserContext.Provider value={{ user: this.state.user, signIn: this.signIn, signOut: this.signOut }}>
    <Component {...pageProps} />
  </UserContext.Provider>
</Container>

Here we've set 3 values to the UserContext Provider: name of the user (from the state) and methods to signIn and signOut. I won't go into details on how to implement the authentication system, that's beyond the scope of this tutorial.

In this simplified example, I'm only storing the username to both state and localStorage, here's the full code inside the _app.js file:

import React from 'react';
import App, { Container } from 'next/app';
import Router from 'next/router';
import UserContext from '../components/UserContext';

export default class MyApp extends App {
  state = {
    user: null
  };

  static async getInitialProps({ Component, router, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  componentDidMount = () => {
    const user = localStorage.getItem('coolapp-user');
    if (user) {
      this.setState({
        user
      });
    } else {
      Router.push('/signin');
    }
  };

  signIn = (username, password) => {
    localStorage.setItem('coolapp-user', username);

    this.setState(
      {
        user: username
      },
      () => {
        Router.push('/');
      }
    );
  };

  signOut = () => {
    localStorage.removeItem('coolapp-user');
    this.setState({
      user: null
    });
    Router.push('/signin');
  };

  render() {
    const { Component, pageProps } = this.props;

    return (
      <Container>
        <UserContext.Provider value={{ user: this.state.user, signIn: this.signIn, signOut: this.signOut }}>
          <Component {...pageProps} />
        </UserContext.Provider>
      </Container>
    );
  }
}

Inside the componentDidMount we are checking whether the user info exists in localStorage, and if not, redirect the visitor to the Sign In page. Once a user signs in, we're saving their info to localStorage and redirecting to homepage.

Getting data with useContext hook

Now we can import the same UserContext component inside any other component that needs user data and retrieve the data via a single line of code:

const { user } = useContext(UserContext);

Here's a complete code of a /component/UserInfo.js that displays the username of the signed in user, and a Sign Out button:

import { useContext } from 'react';
import UserContext from '../components/UserContext';

const UserInfo = () => {
  const { user, signOut } = useContext(UserContext);

  return (
    <div className="user-info">
      <p>
        Hello, <strong>{user}</strong>
      </p>
      <p>Welcome to our app</p>
      <button className="btn" onClick={signOut}>
        Sign Out
      </button>
    </div>
  );
};

export default UserInfo;

Using React Hooks in the Sign In form

Before React 16.8. was released, we'd have to create a class based Form component, but now we can have a simple function based component where we set form data into state with the useState Hook. Here is a simple example:

import { useState, useContext } from 'react';
import UserContext from '../components/UserContext';

const Form = () => {
  const { signIn } = useContext(UserContext);
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [message, setMessage] = useState('');

  const authenticate = e => {
    e.preventDefault();
    if (username != '' || password != '') {
      signIn(username, password);
    } else {
      setMessage('Please enter your username and password');
    }
  };

  return (
    <form className="sign-in">
      <input type="text" name="username" placeholder="username" onChange={e => setUsername(e.target.value)} />
      <input type="password" name="password" placeholder="password" onChange={e => setPassword(e.target.value)} />
      {message != '' && <div className="message">{message}</div>}
      <button className="btn" onClick={e => authenticate(e)}>
        Sign In
      </button>
    </form>
  );
};

export default Form;

Once the user clicks the Sign In button, we are checking whether username and password aren't empty and then call the signIn function that we got from the UserContext. The signIn funtion is inside the _app.js file.

Here's a live example:

You can download the complete source code on GitHub.