2

I need to access the context API in my _app.js file in order to set global state triggered by the router events. The reason for this is to set a loading state which can be accessed by individual components throughout the app. The problem is is the context is provided from the _app.js file, so I don't have the context's context as it were.

context.js

import React, { createContext, useState } from "react";

export const Context = createContext();

const ContextProvider = (props) => {
    const [isLoading, setIsLoading] = useState(false);

    return (
        <Context.Provider
            value={{
                isLoading,
                setIsLoading,
            }}
        >
            {props.children}
        </Context.Provider>
    );
};

export default ContextProvider;

_app.js

import React, { useContext } from "react";
import App from "next/app";
import Head from "next/head";
import Aux from "../hoc/Aux";
import ContextProvider, { Context } from "../context/context";
import { withRouter } from "next/router";

class MyApp extends App {
    static contextType = Context;

    componentDidMount() {
        this.props.router.events.on("routeChangeStart", () => {
            this.context.isLoading(true);
        });
        this.props.router.events.on("routeChangeComplete", () => {
            this.context.isLoading(false);
        });
        this.props.router.events.on("routeChangeError", () => {
            this.context.isLoading(false);
        });
    }

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

        return (
            <Aux>
                <Head>
                    <title>My App</title>
                </Head>
                <ContextProvider>
                    <Component {...pageProps} />
                </ContextProvider>
            </Aux>
        );
    }
}

export default withRouter(MyApp);

Clearly this wouldn't work since _app.js is not wrapped in the context provider. I've tried moving the router event listeners further down the component tree, but then I don't get the loading state from my home page to my dynamically created pages that way.

Is there any workaround that lets me consume context in _app.js? I can't think of any other way I can access loading state globally to conditionally load specific components.

2 Answers 2

3

It's not clear to me why you need the context provider to be a parent of _app.js (or a separate component at all). Wouldn't the following work?

class MyApp extends App {
  state = {
    isLoading: false,
  };

  componentDidMount() {
    this.props.router.events.on("routeChangeStart", () => {
      this.setIsLoading(true);
    });

    this.props.router.events.on("routeChangeComplete", () => {
      this.setIsLoading(false);
    });

    this.props.router.events.on("routeChangeError", () => {
      this.setIsLoading(false);
    });
  }

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

    return (
      <Aux>
        <Head>
          <title>My App</title>
        </Head>
        <Context.Provider
          value={{
            isLoading: this.state.isLoading,
            setIsLoading: this.setIsLoading,
          }}>
          <Component {...pageProps} />
        </Context.Provider>
      </Aux>
    );
  }

  setIsLoading = (isLoading) => {
    this.setState({ isLoading });
  }
}

export default withRouter(MyApp);

Alternatively (if there's something I'm really not understanding about your use case), you could create a HoC:

function withContext(Component) {
  return (props) => (
    <ContextProvider>
      <Component {...props} />
    </ContextProvider>
  );
}

class MyApp extends App {
  ...
}

export default withContext(withRouter(MyApp));
Sign up to request clarification or add additional context in comments.

2 Comments

You're right, I was able to make it work with your first example. Thanks!
The higher order component solution is a much more general applicable solution, than solution one. In my case I wanted to check my UserContext before displaying any other routes/pages.
1

You can show a loading indicator using nprogress. For example:

import NProgress from "nprogress";
import Router from "next/router";

Router.onRouteChangeStart = () => NProgress.start();
Router.onRouteChangeComplete = () => NProgress.done();
Router.onRouteChangeError = () => NProgress.done();

Source

2 Comments

I know, I'm trying to do skeleton loading though so I need global state to programmatically load certain components. I have a map component that I don't want to rerender every route change.
Then use the above example and swap out nprogress for your own custom skeleton :) The core idea is the same.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.