Next.js: document is not defined

Viewed 107061

I am trying to create a payment form where people can pay but I keep getting this error.

document is not defined

I'm using Next.js. Please see my code below:

import React from "react";
import {Elements, StripeProvider} from 'react-stripe-elements';
import CheckoutForm from '../../components/Payment/CheckoutForm';
import { useRouter } from 'next/router';


var stripe_load = () => {
    var aScript = document.createElement('script');
    aScript.type = 'text/javascript';
    aScript.src = " https://js.stripe.com/v3/";

    document.head.appendChild(aScript);
    aScript.onload = () => {

    };
};

function Payment({host}) {
    const key = host.includes('localhost') ? 'test' : 't';

    stripe_load();

    const router = useRouter();

    return (
        <div className="Payment Main">
            <StripeProvider apiKey={key}>
                <Elements>
                    <CheckoutForm planid={router.query.id}/>
                </Elements>
            </StripeProvider>
            <br/>
            <br/>
            <p>Powered by Stripe</p>
        </div>
    );
};


Payment.getInitialProps = async ctx => {
    return { host: ctx.req.headers.host }
};

export default Payment
10 Answers

I think, in server rendering mode, the document is undefined. You should be able to use it inside class lifecycle methods or useEffect

import React, {useEffect} from "react";
import {Elements, StripeProvider} from 'react-stripe-elements';
import CheckoutForm from '../../components/Payment/CheckoutForm';
import { useRouter } from 'next/router';


var stripe_load = () => {
    var aScript = document.createElement('script');
    aScript.type = 'text/javascript';
    aScript.src = " https://js.stripe.com/v3/";

    document.head.appendChild(aScript);
    aScript.onload = () => {

    };
};

function Payment({host}) {
    const key = host.includes('localhost') ? 'test' : 't';

    useEffect(() => {
      var aScript = document.createElement('script');
       aScript.type = 'text/javascript';
       aScript.src = " https://js.stripe.com/v3/";

       document.head.appendChild(aScript);
       aScript.onload = () => {

       };
    }, [])
    //stripe_load();

    const router = useRouter();

    return (
        <div className="Payment Main">
            <StripeProvider apiKey={key}>
                <Elements>
                    <CheckoutForm planid={router.query.id}/>
                </Elements>
            </StripeProvider>
            <br/>
            <br/>
            <p>Powered by Stripe</p>
        </div>
    );
};


Payment.getInitialProps = async ctx => {
    return { host: ctx.req.headers.host }
};

export default Payment

You need to wrap your document using validator process.browser, because this document is belong to client side, and the error occured when nextjs render in server side.

var stripe_load = () => {
    if (process.browser) {
        var aScript = document.createElement('script');
        aScript.type = 'text/javascript';
        aScript.src = " https://js.stripe.com/v3/";

        document.head.appendChild(aScript);
        aScript.onload = () => {

        };
    }
};

for me, this error occurs from lots of mistakes: first, you have to use

if (typeof window !== "undefined") {

for every window. and document. and especially for localStorage. functions (my self forgot to use this if clause for localStorage.getItem and the error didn't fix)

another problem was the import of the line below which was totally wrong!:

import {router} from "next/client";

For example, when the module includes a library that only works in the browser. Some times dynamic import can solve this issue.

Take a look at the following example:

import dynamic from 'next/dynamic'

const DynamicComponentWithNoSSR = dynamic(
  () => import('../components/hello3'),
  { ssr: false }
)

function Home() {
  return (
    <div>
      <Header />
      <DynamicComponentWithNoSSR />
      <p>HOME PAGE is here!</p>
    </div>
  )
}

export default Home

To access the document in Next.js you need await the page render first then get it in a variable, like so:

const [_document, set_document] = React.useState(null)

React.useEffect(() => {
    set_document(document)
}, [])

You should read the difference between JS in node.js and JS in Browser. In node.js, you don't have DOM APIs (window, document, document.getElementById,...), the thing can be only have when your HTML is rendered in a thing called windows of Browsers. So next.js use node.js to run JS code and take result to render HTML file. But in node.js, nothing is windows of Browser.

My English is quite bad. But I hope this answer will be helpful for you.

You have to make sure you have two things setup.

  1. DOM render is completed.
  2. Then load the function.

Step 1: create hook isMounted this will make sure that your DOM is rendered.

import React, {useEffect, useState} from 'react';

function RenderCompleted() {

    const [mounted, setMounted] = useState(false);

    useEffect(() => {
        setMounted(true)

        return () => {
            setMounted(false)
        }
    });

    return mounted;
}

export default RenderCompleted;

Inside function Payment load the hook :

import React, {useEffect} from "react";
import {Elements, StripeProvider} from 'react-stripe-elements';
import CheckoutForm from '../../components/Payment/CheckoutForm';
import { useRouter } from 'next/router';
import RenderCompleted from '../hooks/isMounted';

var stripe_load = () => {
    var aScript = document.createElement('script');
    aScript.type = 'text/javascript';
    aScript.src = " https://js.stripe.com/v3/";

    document.head.appendChild(aScript);
    aScript.onload = () => {

    };
};

function Payment({host}) {
    const[key,setKey] = useEffect('');

    //will help you on re-render or host changes

    useEffect(()=>{
        const key = host.includes('localhost') ? 'test' : 't';
        setKey(key);
    },[host])


    useEffect(() => {
      var aScript = document.createElement('script');
       aScript.type = 'text/javascript';
       aScript.src = " https://js.stripe.com/v3/";

       document.head.appendChild(aScript);
       aScript.onload = () => {

       };
    }, [isMounted])


    const router = useRouter();
    const isMounted = RenderCompleted();

    return (
        <div className="Payment Main">
            <StripeProvider apiKey={key}>
                <Elements>
                    <CheckoutForm planid={router.query.id}/>
                </Elements>
            </StripeProvider>
            <br/>
            <br/>
            <p>Powered by Stripe</p>
        </div>
    );
};


Payment.getInitialProps = async ctx => {
    return { host: ctx.req.headers.host }
};

export default Payment

Another way to do this is, to use head component of next.js: https://nextjs.org/docs/api-reference/next/head

<Head>
        <title>My page title</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
      </Head>

you can use a new feature called dynamic imports with ssr off. this is a workaround for rerendering the component.

The best practice solution in 2022 is using dynamic

import dynamic from "next/dynamic";
const ... = dynamic(
() => import("../components/..."),
{
    ssr: false,
}

This might be helpful to some else, I got this error when Material UI anchorEl is true

const [anchoeEl, setAnchorEl] = useState(null)

<Menu 
    anchorEl={anchorEl}  
    anchorOrigin={{
        vertical: "top"
        horizontal: "right"
    }}
/>
Related