简体   繁体   中英

Pass prop to every single page in Next.js with getInitialProps using Typescript

I have a case when I need to know if the user is logged in or not before the page gets rendered server-side and sent back to me using Next.js to avoid having flicker changes in the UI.

I was able to figure out how to prevent the user from accessing some pages if he is already logged in using this HOC component ...

export const noAuthenticatedAllowed = (WrappedComponent: NextPage) => {
    const Wrapper = (props: any) => {
        return <WrappedComponent {...props} />;
    };

    Wrapper.getInitialProps = async (ctx: NextPageContext) => {
        let context = {};
        const { AppToken } = nextCookie(ctx);
        if (AppToken) {
            const decodedToken: MetadataObj = jwt_decode(AppToken);
            const isExpired = () => {
                if (decodedToken.exp < Date.now() / 1000) {
                    return true;
                } else {
                    return false;
                }
            };

            if (ctx.req) {
                if (!isExpired()) {
                    ctx.res && ctx.res.writeHead(302, { Location: "/" });
                    ctx.res && ctx.res.end();
                }
            }

            if (!isExpired()) {
                context = { ...ctx };
                Router.push("/");
            }
        }

        const componentProps =
            WrappedComponent.getInitialProps &&
            (await WrappedComponent.getInitialProps(ctx));

        return { ...componentProps, context };
    };

    return Wrapper;
};

And this works great.

Now, how can I build a similar HOC component to wrap it around let's say "_app.tsx" so that I can pass "userAuthenticated" prop to every single page by getting the token and figure out if it is expired or not and based on that prop I can show the proper UI to the user without that annoying flickering effect?

I hope you can help me with that, I tried to do it the same way I built the above HOC, but I couldn't do it, especially that Typescript doesn't make this any easier with its weird errors :(


Edit ============================================

I was able to create such HOC component and pass down the pro userAuthenticated to each page like this ...

export const isAuthenticated = (WrappedComponent: NextPage) => {
    const Wrapper = (props: any) => {
        return <WrappedComponent {...props} />;
    };

    Wrapper.getInitialProps = async (ctx: NextPageContext) => {
        let userAuthenticated = false;

        const { AppToken} = nextCookie(ctx);
        if (AppToken) {
            const decodedToken: MetadataObj = jwt_decode(AppToken);
            const isExpired = () => {
                if (decodedToken.exp < Date.now() / 1000) {
                    return true;
                } else {
                    return false;
                }
            };

            if (ctx.req) {
                if (!isExpired()) {
                    // ctx.res && ctx.res.writeHead(302, { Location: "/" });
                    // ctx.res && ctx.res.end();
                    userAuthenticated = true;
                }
            }

            if (!isExpired()) {
                userAuthenticated = true;
            }
        }

        const componentProps =
            WrappedComponent.getInitialProps &&
            (await WrappedComponent.getInitialProps(ctx));

        return { ...componentProps, userAuthenticated };
    };

    return Wrapper;
};

However I had to wrap every single page with this HOC in order to pass down the prop userAuthenticated to the global layout that I have, because I couldn't wrap the "_app.tsx" class component with it, it always gives me an Error ...

This works ...

export default isAuthenticated(Home);
export default isAuthenticated(about);

But this doesn't ...

export default withRedux(configureStore)(isAuthenticated(MyApp));

So it is a bit annoying having to do this to each and every page, and then pass down the prop to the global layout in each and every page instead of just do it once in the "_app.tsx".

I'm guessing the reason may be because "_app.tsx" is a class component and not a function component like the rest of the pages? I don't know, I'm just guessing.

Any help with that?

For those of you who might come across the same issue, I was able to solve this as following ...

import React from "react";
import App from "next/app";
import { Store } from "redux";
import { Provider } from "react-redux";
import withRedux from "next-redux-wrapper";
import { ThemeProvider } from "styled-components";
import GlobalLayout from "../components/layout/GlobalLayout";
import { configureStore } from "../store/configureStore";
import { GlobalStyle } from "../styles/global";
import { ToastifyStyle } from "../styles/toastify";
import nextCookie from "next-cookies";
import jwt_decode from "jwt-decode";

 export interface MetadataObj {
   [key: string]: any;
 }

const theme = {
    color1: "#00CC99",
    color2: "#CC0000"
};

export type ThemeType = typeof theme;

interface Iprops {
    store: Store;
    userAuthenticated: boolean;
}

class MyApp extends App<Iprops> {
    // Only uncomment this method if you have blocking data requirements for
    // every single page in your application. This disables the ability to
    // perform automatic static optimization, causing every page in your app to
    // be server-side rendered.

    static async getInitialProps({ Component, ctx }: any) {
        let userAuthenticated = false;

        const { AppToken } = nextCookie(ctx);
        if (AppToken) {
            const decodedToken: MetadataObj = jwt_decode(AppToken);
            const isExpired = () => {
                if (decodedToken.exp < Date.now() / 1000) {
                    return true;
                } else {
                    return false;
                }
            };

            if (ctx.isServer) {
                if (!isExpired()) {
                    userAuthenticated = true;
                }
            }

            if (!isExpired()) {
                userAuthenticated = true;
            }
        }

        return {
            pageProps: Component.getInitialProps
                ? await Component.getInitialProps(ctx)
                : {},
            userAuthenticated: userAuthenticated
        };
    }

    render() {
        const { Component, pageProps, store, userAuthenticated } = this.props;
        return (
            <Provider store={store}>
                <ThemeProvider theme={theme}>
                    <>
                        <GlobalStyle />
                        <ToastifyStyle />
                        <GlobalLayout userAuthenticated={userAuthenticated}>
                            <Component {...pageProps} />
                        </GlobalLayout>
                    </>
                </ThemeProvider>
            </Provider>
        );
    }
}

export default withRedux(configureStore)(MyApp);

As you see I posted the whole _app.tsx component so that you can see the packages that I'm using.

I'm using next-redux-wrapper and styled-components with Typescript.

I had to make the appContext in gitInitialProps as of type any , otherwise it won't work. So if you have a better type suggestion please let me know. I tried to use the type NextPageContext , but that didn't work in this case for some reason.

And with that solution, I was able to get to know if the user authenticated or not and pass a prop to the global layout so that I can use it in every page and without having to do it page per page and that also benefits in case you don't want the header and the footer to get rendered every time if they have to depend on on the userAuthenticated prop, because now you can just put the header and the footer in the GlobalLayout component and still have the userAuthenticated prop at your disposal :D

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM