Emotion: ReferenceError: document is not defined

Created on 16 Dec 2018  路  11Comments  路  Source: emotion-js/emotion

Relevant code:

package.json

        ...
    "main": "dist/main.js",
    "files": ["dist/", "package.json", "README.md"],
    "devDependencies": {
        "@babel/core": "^7.2.2",
        "@babel/plugin-proposal-class-properties": "^7.2.1",
        "@babel/preset-env": "^7.2.0",
        "@babel/preset-react": "^7.0.0",
        "babel-core": "7.0.0-bridge.0",
        "babel-jest": "^23.6.0",
        "babel-loader": "^8.0.4",
        "babel-plugin-emotion": "^10.0.5",
        "enzyme": "^3.8.0",
        "enzyme-adapter-react-16": "^1.7.1",
        "jest": "^23.6.0",
        "jest-emotion": "^10.0.5",
        "webpack": "^4.27.1",
        "webpack-cli": "^3.1.2"
    },
    "dependencies": {
        "@emotion/core": "^10.0.5",
        "@emotion/styled": "^10.0.5",
        "debounce": "^1.2.0",
        "prop-types": "^15.6.2",
        "react": "^16.6.3",
        "react-dom": "^16.6.3"
    },
    "scripts": {
        "build": "./node_modules/.bin/webpack --config=webpack.config.js",
        "test": "./node_modules/.bin/jest"
    }
        ...

webpack.config.js

const path = require("path")

module.exports = {
    entry: "./src/index.js",
    mode: "production",
    output: {
        path: path.resolve("./dist"),
        filename: "[name].js",
        libraryTarget: "commonjs2",
        libraryExport: "default"
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: "babel-loader"
            }
        ]
    }
}

src/components/body/elements.js

import styled from "@emotion/styled"
import { css, jsx } from "@emotion/core"

export const Wrapper = styled.div`
  background-color: red;
`

src/components/body/index.js

import { Wrapper } from "./elements"

class Body extends React.Component {
    render() {
        const {
            children,
            containerClassName,
            layerIndex
        } = this.props
        return <Wrapper>{children}</Wrapper>
    }
}

src/index.js

import React from "react"
import ReactDOM from "react-dom"
import PropTypes from "prop-types"
import { debounce } from "debounce"

import Body from "./components/body"

const isBrowser = typeof document !== "undefined" ? true : null

class SimpleModal extends React.Component {
    static defaultProps = {
        mountPointSelector: "body",
        containerClassName: "SimpleModal",
        layerPosition: "above",
        defaultIndex: 100
    }
    _getLayerIndex = () => {
        const { layerPosition, defaultIndex } = this.props
        const allModals = this._getOtherModals()
        const totalModals = allModals && allModals.length ? allModals.length : 0
        var nextIndex = defaultIndex
        if (layerPosition === "above") {
            nextIndex += totalModals
        }
        if (layerPosition === "below") {
            nextIndex -= totalModals
        }
        return nextIndex
    }
    _getOtherModals = (modal) => {
        if (!isBrowser) {
            return
        }
        const { containerClassName } = this.props
        // Get all the elements on the page with the classname modal.
        const otherModals = [].slice.call(
            document.getElementsByClassName(containerClassName)
        )
        // If we got ourselves as an argument, rmeove it from the list of
        // elements we return.
        if (modal) {
            return otherModals.filter((el) => {
                return el !== modal
            })
        }
        // Otherwise, return them all.
        return otherModals
    }
    _getMountPoint = () => {
        if (!isBrowser) {
            return
        }
        const { mountPointSelector } = this.props
        return document.querySelector(mountPointSelector)
    }
    render() {
        const {
            children,
            isVisible,
            containerClassName
        } = this.props
        // If the element is visible...
        if (isVisible) {
                        if(!isBrowser){
                                return null
                        }
            // Then, create the portal element in the DOM, under the BODY.
            const mountPoint = this._getMountPoint()
            const layerIndex = this._getLayerIndex()
            const modal = (
                <Body containerClassName={containerClassName} layerIndex={layerIndex}>
                    {children}
                </Body>
            )
            return ReactDOM.createPortal(modal, mountPoint)
        } else {
            return null
        }
    }
}

export default SimpleModal

What you did:
I am attempting to rebuild a package that I maintain. However, the default component will function after being compiled by webpack. It will render correctly after a jest test though. Any attempt to use this package in another project results in a ReferenceError at runtime. I have pieced the \

What happened:
Rendering \ReferenceError: document is not defined.

Most helpful comment

I ran into the same problem when trying to use v10 with react-on-rails. v9 worked fine. Switching the webpack target unfortunately isn't an option for us. I haven't had a chance to dig into the source too much, but somewhere the test of whether we are in a browser or not changed between v9 and v10.

All 11 comments

Could u prepare a repository with the issue reproduced?

I will if the following isn't sufficient. I realize the module is named .browser, so I'm either doing something wrong (with SSR perhaps) or there's an error with the following module.

The issue seems to be with the function createCache located at https://github.com/emotion-js/emotion/blob/master/packages/cache/src/index.js

Uncompiled source

let createCache = (options?: Options): EmotionCache => {
  if (options === undefined) options = {}
  let key = options.key || 'css'
  let stylisOptions

  if (options.prefix !== undefined) {
    stylisOptions = {
      prefix: options.prefix
    }
  }

  let stylis = new Stylis(stylisOptions)

  if (process.env.NODE_ENV !== 'production') {
    // $FlowFixMe
    if (/[^a-z-]/.test(key)) {
      throw new Error(
        `Emotion key must only contain lower case alphabetical characters and - but "${key}" was passed`
      )
    }
  }
  let inserted = {}
  // $FlowFixMe
  let container: HTMLElement
  if (isBrowser) {
    container = options.container || document.head

    const nodes = document.querySelectorAll(`style[data-emotion-${key}]`)

    Array.prototype.forEach.call(nodes, (node: HTMLStyleElement) => {
      const attrib = node.getAttribute(`data-emotion-${key}`)
      // $FlowFixMe
      attrib.split(' ').forEach(id => {
        inserted[id] = true
      })
      if (node.parentNode !== container) {
        container.appendChild(node)
      }
    })
  }

  let insert: (
    selector: string,
    serialized: SerializedStyles,
    sheet: StyleSheet,
    shouldCache: boolean
  ) => string | void

  if (isBrowser) {
    stylis.use(options.stylisPlugins)(ruleSheet)

    insert = (
      selector: string,
      serialized: SerializedStyles,
      sheet: StyleSheet,
      shouldCache: boolean
    ): void => {
      let name = serialized.name
      Sheet.current = sheet
      if (
        process.env.NODE_ENV !== 'production' &&
        serialized.map !== undefined
      ) {
        let map = serialized.map
        Sheet.current = {
          insert: (rule: string) => {
            sheet.insert(rule + map)
          }
        }
      }
      stylis(selector, serialized.styles)
      if (shouldCache) {
        cache.inserted[name] = true
      }
    }
  } else {
    stylis.use(removeLabel)
    let serverStylisCache = rootServerStylisCache
    if (options.stylisPlugins || options.prefix !== undefined) {
      stylis.use(options.stylisPlugins)
      // $FlowFixMe
      serverStylisCache = getServerStylisCache(
        options.stylisPlugins || rootServerStylisCache
      )(options.prefix)
    }
    let getRules = (selector: string, serialized: SerializedStyles): string => {
      let name = serialized.name
      if (serverStylisCache[name] === undefined) {
        serverStylisCache[name] = stylis(selector, serialized.styles)
      }
      return serverStylisCache[name]
    }
    insert = (
      selector: string,
      serialized: SerializedStyles,
      sheet: StyleSheet,
      shouldCache: boolean
    ): string | void => {
      let name = serialized.name
      let rules = getRules(selector, serialized)
      if (cache.compat === undefined) {
        // in regular mode, we don't set the styles on the inserted cache
        // since we don't need to and that would be wasting memory
        // we return them so that they are rendered in a style tag
        if (shouldCache) {
          cache.inserted[name] = true
        }
        if (
          // using === development instead of !== production
          // because if people do ssr in tests, the source maps showing up would be annoying
          process.env.NODE_ENV === 'development' &&
          serialized.map !== undefined
        ) {
          return rules + serialized.map
        }
        return rules
      } else {
        // in compat mode, we put the styles on the inserted cache so
        // that emotion-server can pull out the styles
        // except when we don't want to cache it(just the Global component right now)

        if (shouldCache) {
          cache.inserted[name] = rules
        } else {
          return rules
        }
      }
    }
  }

  if (process.env.NODE_ENV !== 'production') {
    // https://esbench.com/bench/5bf7371a4cd7e6009ef61d0a
    const commentStart = /\/\*/g
    const commentEnd = /\*\//g

    stylis.use((context, content) => {
      switch (context) {
        case -1: {
          while (commentStart.test(content)) {
            commentEnd.lastIndex = commentStart.lastIndex

            if (commentEnd.test(content)) {
              commentStart.lastIndex = commentEnd.lastIndex
              continue
            }

            throw new Error(
              'Your styles have an unterminated comment ("/*" without corresponding "*/").'
            )
          }

          commentStart.lastIndex = 0
          break
        }
      }
    })

    stylis.use((context, content, selectors) => {
      switch (context) {
        case 2: {
          for (let i = 0, len = selectors.length; len > i; i++) {
            // :last-child isn't included here since it's safe
            // because a style element will never be the last element
            let match = selectors[i].match(/:(first|nth|nth-last)-child/)
            if (match !== null) {
              console.error(
                `The pseudo class "${
                  match[0]
                }" is potentially unsafe when doing server-side rendering. Try changing it to "${
                  match[1]
                }-of-type"`
              )
            }
          }
          break
        }
      }
    })
  }

  const cache: EmotionCache = {
    key,
    sheet: new StyleSheet({
      key,
      container,
      nonce: options.nonce,
      speedy: options.speedy
    }),
    nonce: options.nonce,
    inserted,
    registered: {},
    insert
  }
  return cache
}

Distributed source
_(./node_modules/@emotion/cache/dist/cache.browser.esm.js)_

var createCache = function createCache(options) {
  if (options === undefined) options = {};
  var key = options.key || 'css';
  var stylisOptions;

  if (options.prefix !== undefined) {
    stylisOptions = {
      prefix: options.prefix
    };
  }

  var stylis = new Stylis(stylisOptions);

  if (process.env.NODE_ENV !== 'production') {
    // $FlowFixMe
    if (/[^a-z-]/.test(key)) {
      throw new Error("Emotion key must only contain lower case alphabetical characters and - but \"" + key + "\" was passed");
    }
  }

  var inserted = {}; // $FlowFixMe

  var container;

  {
    container = options.container || document.head;
    var nodes = document.querySelectorAll("style[data-emotion-" + key + "]");
    Array.prototype.forEach.call(nodes, function (node) {
      var attrib = node.getAttribute("data-emotion-" + key); // $FlowFixMe

      attrib.split(' ').forEach(function (id) {
        inserted[id] = true;
      });

      if (node.parentNode !== container) {
        container.appendChild(node);
      }
    });
  }

  var _insert;

  {
    stylis.use(options.stylisPlugins)(ruleSheet);

    _insert = function insert(selector, serialized, sheet, shouldCache) {
      var name = serialized.name;
      Sheet.current = sheet;

      if (process.env.NODE_ENV !== 'production' && serialized.map !== undefined) {
        var map = serialized.map;
        Sheet.current = {
          insert: function insert(rule) {
            sheet.insert(rule + map);
          }
        };
      }

      stylis(selector, serialized.styles);

      if (shouldCache) {
        cache.inserted[name] = true;
      }
    };
  }

  if (process.env.NODE_ENV !== 'production') {
    // https://esbench.com/bench/5bf7371a4cd7e6009ef61d0a
    var commentStart = /\/\*/g;
    var commentEnd = /\*\//g;
    stylis.use(function (context, content) {
      switch (context) {
        case -1:
          {
            while (commentStart.test(content)) {
              commentEnd.lastIndex = commentStart.lastIndex;

              if (commentEnd.test(content)) {
                commentStart.lastIndex = commentEnd.lastIndex;
                continue;
              }

              throw new Error('Your styles have an unterminated comment ("/*" without corresponding "*/").');
            }

            commentStart.lastIndex = 0;
            break;
          }
      }
    });
    stylis.use(function (context, content, selectors) {
      switch (context) {
        case 2:
          {
            for (var i = 0, len = selectors.length; len > i; i++) {
              // :last-child isn't included here since it's safe
              // because a style element will never be the last element
              var match = selectors[i].match(/:(first|nth|nth-last)-child/);

              if (match !== null) {
                console.error("The pseudo class \"" + match[0] + "\" is potentially unsafe when doing server-side rendering. Try changing it to \"" + match[1] + "-of-type\"");
              }
            }

            break;
          }
      }
    });
  }

  var cache = {
    key: key,
    sheet: new StyleSheet({
      key: key,
      container: container,
      nonce: options.nonce,
      speedy: options.speedy
    }),
    nonce: options.nonce,
    inserted: inserted,
    registered: {},
    insert: _insert
  };
  return cache;
};

I'm receiving an error that and leaves the following stack trace:

Message: document is not defined

Stack trace: ReferenceError: document is not defined
    at createCache (webpack:///./node_modules/@emotion/cache/dist/cache.browser.esm.js?:109:38)
    at eval (webpack:///./node_modules/@emotion/core/dist/core.browser.esm.js?:32:149)
    at Module../node_modules/@emotion/core/dist/core.browser.esm.js (/usr/home/main/mnt/da0/code/projects/library/alexseitsinger/sites/main/node_modules/@alexseitsinger/simple-modal/dist/main.js:110:1)
    at __webpack_require__ (/usr/home/main/mnt/da0/code/projects/library/alexseitsinger/sites/main/node_modules/@alexseitsinger/simple-modal/dist/main.js:21:30)
    at eval (webpack:///./node_modules/@emotion/styled-base/dist/styled-base.browser.esm.js?:7:71)
    at Module../node_modules/@emotion/styled-base/dist/styled-base.browser.esm.js (/usr/home/main/mnt/da0/code/projects/library/alexseitsinger/sites/main/node_modules/@alexseitsinger/simple-modal/dist/main.js:194:1)
    at __webpack_require__ (/usr/home/main/mnt/da0/code/projects/library/alexseitsinger/sites/main/node_modules/@alexseitsinger/simple-modal/dist/main.js:21:30)
    at eval (webpack:///./src/components/body/elements.js?:6:78)
    at Module../src/components/body/elements.js (/usr/home/main/mnt/da0/code/projects/library/alexseitsinger/sites/main/node_modules/@alexseitsinger/simple-modal/dist/main.js:431:1)
    at __webpack_require__ (/usr/home/main/mnt/da0/code/projects/library/alexseitsinger/sites/main/node_modules/@alexseitsinger/simple-modal/dist/main.js:21:30)

You have mentioned SSR - are those .browser files imported when trying to run this in node?

The project this is used in uses SSR. An example project layout can be found here. In the frontend/src folder is a render.js file. This is where the app is passed in and rendered for the server-side bundle. The client-side bundle is rendered from client.js. Both of these entry points import a module \elements.js modules that use @emotion/styled and @emotion/core to create styled components. Nothing unusual was done when upgrading to emotion 10 other changing the import statements used (following the documentation), but somehow these errors occur now.

Could u specify target: 'node' in your webpack config? https://webpack.js.org/concepts/targets/#usage

That did it. Thanks!

What about react-rails? I tried setting

environment.config.merge({
    target: 'node'
});

in rails webpacker, but then I was getting require is not a function

i don't think changing a target is good option.

i'm trying to get emotion working with this SSR solution https://github.com/Limenius/ReactBundle, and as soon as I import emotion (without even using it) i get this ReferenceError: document is not defined error. like @dachinat said, changing the target in webpack is not an option because like him i then get require is not a function. I've tried using the older emotion SSR api's, doesn't seem to help... Any help or even clues are appreciated! thanks

I ran into the same problem when trying to use v10 with react-on-rails. v9 worked fine. Switching the webpack target unfortunately isn't an option for us. I haven't had a chance to dig into the source too much, but somewhere the test of whether we are in a browser or not changed between v9 and v10.

I ran into the same problem when trying to use v10 with react-on-rails. v9 worked fine. Switching the webpack target unfortunately isn't an option for us. I haven't had a chance to dig into the source too much, but somewhere the test of whether we are in a browser or not changed between v9 and v10.

Did you ever manage to solve this?

Was this page helpful?
0 / 5 - 0 ratings