Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 userAuthenticatedto 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?

like image 732
Ruby Avatar asked Jan 09 '20 19:01

Ruby


People also ask

How do I pass Props to all pages in next JS?

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

How do I pass props in next TypeScript?

To pass an object as props to a component in React TypeScript: Define an interface for the type of the object. Pass an object of the specified type to the child component, e.g. <Employee {... obj} /> .

How do I use getInitialProps in next JS?

Make sure the returned object from getInitialProps is a plain Object and not using Date , Map or Set . For the initial page load, getInitialProps will run on the server only. getInitialProps will then run on the client when navigating to a different route via the next/link component or by using next/router .

Can we pass props in next JS?

Yes, just like you are using useRouter in Home you can use it in some other component as well. next/router shares context with whole react tree. You don't need to pass it using prop.


1 Answers

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

like image 187
Ruby Avatar answered Oct 13 '22 23:10

Ruby