Over the last year and a half I have been developing a components library using Storybook, React and Webpack 5 for my team. Recently we have started to look at Next.JS and have a major project well underway using its framework. This however has created some challenges, as next.js renders Server-side and Client-side, meaning any imports that use client-side exclusive objects/functions etc. cause an error. Now this can be solved using dynamic imports, but that then creates loading times or missing content if not handled correctly.
Our whole components library causes this SSR error. It doesn't matter if you are importing a button or a popover that actually utilises window, you have to use dynamic imports. This then creates loading times and missing content on the rendered page. We can't even use the loading component in the library, as that needs loading. We also have the problem, that even if we took out all references to window or document in our code, some of our dependencies reference them somewhere and we just can't avoid it.
What we would like to be able to do with the library is import it in several ways to isolate window and document calls to their individual components, so we can avoid dynamic loading wherever possible.
import { Component } from 'Library'
import { Component } from 'Library/ComponentCategory'
import { Component } from 'Library/Component'
The reason behind the three imports is simple:
This way of importing has been implemented, but no matter which route you take, it still fires the Next.JS 'self is not defined' error. This seems to mean, that even on an individual component import, the entire code base of the library is still referenced.
We removed any unneeded references to client exclusive code and added conditional statements around any statements we could not remove.
if (typeof window !== 'undefined') {
// Do the thing with window i.e window.location.href = '/href'
}
This didn't have any effect, largely due to the nature of the npm ecosystem. Somewhere in the code, document, screen or window is called and there isn't a lot I can do about it. We could wrap every import in this conditional, but lets be honest, that is pretty gross and likely wouldn't solve the problem without other steps being taken.
Using webpack 5 entry
, output
and splitChunks
magic has also not solved the problem.
The first step was to configure entry and output. So I set my entry to something like this:
entry: {
// Entry Points //
//// Main Entry Point ////
main: './src/index.ts',
//// Category Entry Points ////
Buttons: './src/components/Buttons', // < - This leads to an index.ts
...,
//// Individual Component Entry Points ////
Button: './src/components/Buttons/Button.tsx',
OtherComponent: '...',
...,
},
And my output to:
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js',
library: pkg.name,
libraryTarget: 'umd',
umdNamedDefine: true,
},
This has allowed us to now import the library as a whole, via categories or as individual components. We can see this, as in the dist folder of the library, there are now Component.js(.map) files. Unfortunately this has still not allowed us to creep past the SSR error. We can import Button from Library/dist/Button
but Next.JS still screams about code its not even using.
The next step on this adventure, and currently the last, was to use Webpacks splitChunks functionality, alongside the entry/output changes.
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
This has also not worked, though I am not 100% sure its even firing correctly, as I see no npm.packageName
in my dist folder. There are now a bunch of 4531.js
(3-4 numbers followed by js), but opening these, contained within the webpack generated code, are some classNames I have written, or the string that has been generated for my scss-modules.
ALL RESULTS WILL BE POSTED ON THREAD
Making a library of three simple components (Red, Blue, Green) and trying to split them out. One will contain window, and using npm pack, we will keep making changes till something sticks in Next.JS. I don't necessarily think this will help, but may improve understanding of how everything is working.
Funnily enough I looked at this when I first started on the library, realised it was a dragon I didn't need to tackle and ran away. The solution here, would be to separate out the categories into their own self contained npm packages. These would then be contained in a lerna enviroment. This could also be done without a tool like lerna, but we do not want to install part of the components library but all of it. I still feel like this route is over complicated, unnecessary and will cause more things to maintain in the long run. It is also going to require a rethink of structure and a rewrite of some sections i.e storybook, the docker image that deploys the storybook
Again, this solution has a funny anecdote to go along with it. Lots of JS developers do not understand some of the fundamental tools they use. That isn't to say they are bad developers, but CLI tools like create-react-app generate a lot of the required project boilerplate, meaning the developer can focus on the functionality of their application. This was the case for my colleague and I, so we decided that it made sense to start from scratch. Webpack was the bundler I chose (and thank god for all those webpack 5 upgrades) but maybe this was the wrong decision and I should have used rollup?
It is possible that this is an issue of Next.JS and that in reality Next.JS is the problem. I think that is a bad way to look at things however. Next.JS is a very cool framework and other than the problem being described here, has been wonderful to use. Our existing deployed application stacks are; Webpack, pug and express. Maybe deciding to use a framework is a bad move and we need to rewrite the application currently being developed in next. I do recall seeing that SSR errors could arise from react component life cycle methods/useEffect, so perhaps that has been the real culprit this whole time.
The library uses pnpm as its package manager.
"dependencies": {
"@fortawesome/fontawesome-pro": "^5.15.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/pro-regular-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"classname": "^0.0.0",
"classnames": "^2.3.1",
"crypto-js": "^4.1.1",
"date-fns": "^2.28.0",
"formik": "^2.2.9",
"html-react-parser": "^1.4.5",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"nanoid": "^3.2.0",
"react-currency-input-field": "^3.6.4",
"react-datepicker": "^4.6.0",
"react-day-picker": "^7.4.10",
"react-modal": "^3.14.4",
"react-onclickoutside": "^6.12.1",
"react-router-dom": "^6.2.1",
"react-select-search": "^3.0.9",
"react-slider": "^1.3.1",
"react-tiny-popover": "^7.0.1",
"react-toastify": "^8.1.0",
"react-trix": "^0.9.0",
"trix": "1.3.1",
"yup": "^0.32.11"
},
"devDependencies": {
"postcss-preset-env": "^7.4.2",
"@babel/core": "^7.16.12",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@dr.pogodin/babel-plugin-css-modules-transform": "^1.10.0",
"@storybook/addon-actions": "^6.4.14",
"@storybook/addon-docs": "^6.4.14",
"@storybook/addon-essentials": "^6.4.14",
"@storybook/addon-jest": "^6.4.14",
"@storybook/addon-links": "^6.4.14",
"@storybook/addons": "^6.4.14",
"@storybook/builder-webpack5": "^6.4.14",
"@storybook/manager-webpack5": "^6.4.14",
"@storybook/react": "^6.4.14",
"@storybook/theming": "^6.4.14",
"@svgr/webpack": "^6.2.0",
"@testing-library/react": "^12.1.2",
"@types/enzyme": "^3.10.11",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/jest": "^27.4.0",
"@types/react": "^17.0.38",
"@types/react-datepicker": "^4.3.4",
"@types/react-dom": "^17.0.11",
"@types/react-slider": "^1.3.1",
"@types/yup": "^0.29.13",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"@vgrid/sass-inline-svg": "^1.0.1",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.6",
"audit-ci": "^5.1.2",
"babel-loader": "^8.2.3",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-plugin-react-docgen": "^4.2.1",
"babel-plugin-react-remove-properties": "^0.3.0",
"clean-css-cli": "^5.5.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.1",
"css-loader": "^6.5.1",
"css-modules-typescript-loader": "^4.0.1",
"dependency-cruiser": "^11.3.0",
"enzyme": "^3.11.0",
"eslint": "^8.7.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-css-modules": "^2.11.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.0.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-sonarjs": "^0.11.0",
"eslint-webpack-plugin": "^3.1.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.4.7",
"jest-environment-enzyme": "^7.1.2",
"jest-environment-jsdom": "^27.4.6",
"jest-enzyme": "^7.1.2",
"jest-fetch-mock": "^3.0.3",
"jest-sonar-reporter": "^2.0.0",
"jest-svg-transformer": "^1.0.0",
"lint-staged": "^12.3.1",
"mini-css-extract-plugin": "^2.5.3",
"narn": "^2.1.0",
"node-notifier": "^10.0.0",
"np": "^7.6.0",
"postcss": "^8.4.5",
"postcss-loader": "^6.2.1",
"precss": "^4.0.0",
"prettier": "^2.5.1",
"prettier-eslint": "^13.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-is": "^17.0.2",
"sass": "^1.49.0",
"sass-loader": "^12.4.0",
"sass-true": "^6.0.1",
"sonarqube-scanner": "^2.8.1",
"storybook-formik": "^2.2.0",
"style-loader": "^3.3.1",
"ts-jest": "^27.1.3",
"ts-loader": "^9.2.6",
"ts-prune": "^0.10.3",
"typescript": "^4.5.5",
"typescript-plugin-css-modules": "^3.4.0",
"url-loader": "^4.1.1",
"webpack": "^5.67.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.3",
"webpack-node-externals": "^3.0.0"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
Thanks for reading and any suggestions would be great.
First of all here is the webpack config I forgot to include, minus all the entry points.
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const inliner = require('@vgrid/sass-inline-svg');
const ESLintPlugin = require('eslint-webpack-plugin');
const pkg = require('./package.json');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// Note: Please add comments to new entry point category additions
entry: {
// Entry Points //
//// Main Entry Point ////
main: './src/index.ts',
//// Category Entry Points ////
Buttons: './src/components/Buttons/index.ts',
...
},
// context: path.resolve(__dirname),
resolve: {
modules: [__dirname, 'node_modules'],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.scss', '.css'],
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js',
library: pkg.name,
libraryTarget: 'umd',
umdNamedDefine: true,
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
minChunks: 1,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
devtool: 'source-map',
module: {
rules: [
// ! This rule generates the ability to use S/CSS Modules but kills global css
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{ loader: 'css-modules-typescript-loader' },
{
loader: 'css-loader', //2
options: {
modules: {
localIdentName: '[local]_[hash:base64:5]',
},
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
extract: true,
modules: true,
use: ['sass'],
},
},
},
'sass-loader',
],
include: /\.module\.css$/,
},
// ! This allows for global css alongside the module rule. Also generates the d.ts files for s/css modules (Haven't figured out why).
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{ loader: 'css-modules-typescript-loader' },
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
extract: true,
use: ['sass'],
},
},
},
'sass-loader',
],
exclude: /\.module\.css$/,
},
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
// {
// test: /\.(js|jsx|ts|tsx)$/,
// exclude: /node_modules/,
// use: {
// loader: 'eslint-webpack-plugin',
// },
// },
{
test: /\.(png|jpg|jpeg|woff|woff2|eot|ttf)$/,
type: 'asset/resource',
},
{
test: /\.svg$/,
use: ['@svgr/webpack', 'url-loader'],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{ from: './src/scss/**/*.scss', to: './scss/' }],
}),
new MiniCssExtractPlugin(),
new ESLintPlugin(),
],
externals: [nodeExternals()],
};
An answer suggested it was the CSS modules being injected into the HTML that was the issue and I needed to extract. I updated the PostCSS rules in my webpack, to have extract: true
and modules: true
before recognising the problem. I am extracting all the css with webpack using the MiniCSSExtractPlugin
. Due to Content-Security-Policy style rules on the webapps my company develops, the injection of styles into the HTML via tools like Style-Loader
breaks everything. There are also very good argument against using tools like style-loader beyond a development environment.
I did more research into webpack extraction and saw people recommending different tools that played better with SSR. I have seen recommendations for MiniTextExtractPlugin (which was deprecated in favour of MiniCSSExtractPlugin), NullLoader (which I believe solves a completely different problem to the one I am facing), CSSLoader/Locales (which I can't find documentation for in the css-loader docs) and a few others; ObjectLoader, as well as style-loader, iso-style-loader etc. During my research into this, I recognised I was in a dead end. Perhaps MiniCSSExtractPlugin works poorly in the webpack of an application utilising SSR, but to quote an old video, "this is a library". Its built, packaged and published long before we install and utilise it in our application.
I updated the Next.JS config of my application based on this and a few other posts. https://github.com/vercel/next.js/issues/10975#issuecomment-605528116
This is now my next.js config
const withTM = require('next-transpile-modules')(['@company/package']); // pass the modules you would like to see transpiled
module.exports = withTM({
webpack: (config, { isServer }) => {
// Fixes npm packages that depend on `fs` module
if (!isServer) {
config.resolve.fallback = {
fs: false,
};
}
return config;
},
});
This also did not solve the problem.
The library uses CopyWebpackPlugin to copy all the scss into a directory within the build. This allows us to expose mixins, variables, common global classnames etc. In an attempt to debug the webpack, I turned this off. This had no effect but I will document it anyway.
I am currently replacing the bundler with rollup just to test whether or not it has any effect.
So rollup was a failure, didn't solve any problems but did bring some issues to light.
Due to the nature of the problem, I decided to just dynamically load anything from the library that was needed, and to extract the loader from the library so I could use it for the dynamic loading.
If I manage to solve this problem in the way I intended, I will make another update. However I believe that this is just another issue with Next to add to the list.
TLDR; Use next 13 and the "use client" string to solve self is not defined errors when using window or React hooks.
So I thought I would post an answer now we fully understand the issue.
No matter what we did, in next 12 this was always going to happen. The moment we used anything like event listners or react (non-server components), next would return the self is not defined error, because they rely on browser functionality. I had suspected react could be causing the issues, but my focus had been on window as that was what was always mentioned in the error. No code splitting would have ever solved this and I'd actually split the codebase as far as I could. Of course in hindsight I should have always realised the issue was from my usage of hooks within a server environment, but I had kind of expected for THE react stack framework to handle this a bit better, or at least be verbose in what the problem was.
Now there is a solution. Ignore all previous versions of next, and upgrade to 13. In next 13 this is solved completely with the "use client" comment you can put at the top of your js/ts file. Next reads this and doesn't try and render the component server side. I hope this will help anyone else who has this issue further down the line.