I am trying to create a local shared folder / workspace / package to share code between projects in a monorepo using npm workspaces, and it works in vite dev mode, but not when I build and preview using vite / rollup.
TypeError: Cannot read properties of null (reading 'useState')
at react_production_min.useState (index.js:85289:286)
at useDebounceValue (index.js:86924:53)
at AppContainer (index.js:86942:36)
This is my folder structure:
my-app/
├─ node_modules/
├─ packages/
│ ├─ shared/
│ ├─ app/
├─ package.json
This is my root package.json:
{
"name": "frontends",
"version": "1.0.0",
"description": "Our frontends",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"workspaces": [
"packages/*"
],
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"autoprefixer": "10.4.17",
"postcss": "8.4.35",
"tailwindcss": "3.4.1"
}
}
This my shared package.json
{
"name": "@myscope/shared",
"version": "1.0.0",
"description": "Shared components, libraries and utils for frontend projects",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"type": "module",
"author": "",
"license": "ISC",
"peerDependencies": {
"i18next": "23.11.3",
"oidc-client-ts": "2.4.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "14.2.1",
"i18next": "23.11.3",
"oidc-client-ts": "2.4.0",
"react": "18.2.0",
"react-dom": "18.2.0"
}
}
And I have a single React hook in shared:
import React from "react";
/**
* Debounced value update, e.g. to trigger search after a user stops typing
* @param value
* @param delay
*/
export default function useDebounceValue(value: string, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value]);
return debouncedValue;
}
This is the app vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import { htmlInjectionPlugin } from "vite-plugin-html-injection";
// https://vitejs.dev/config/
export default defineConfig({
preview: {
port: 5174,
},
server: {
port: 5174,
},
plugins: [
react(),
htmlInjectionPlugin({
injections: [
{
name: "Global Config",
path: "./public/config.js",
type: "js",
injectTo: "head",
},
],
}),
],
build: {
assetsDir: "public/assets",
outDir: "./build",
minify: false,
target: "esnext",
assetsInlineLimit: 8192,
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`,
},
},
},
base: "./",
});
And inside app I have an appContainer.tsx where I try to use useDebounceValue
(as a test only)
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { useDebounceValue } from "@myscope/shared/react/hooks";
import { MenuItem } from "../core/models/menu";
import { RootState } from "../core/state/dependencies";
export const AppContainer = () => {
const activeMenu: MenuItem | null | undefined = useSelector(
(state: RootState) => state.menu.activeMenu,
);
const debouncedTest = useDebounceValue(activeMenu?.title, 500);
useEffect(() => {
console.info(debouncedTest);
}, [debouncedTest]);
return (
<div>Test</div>
);
};
In vite dev this runs fine, but if I do vite build && vite preview, it fails with the null error from the top.
If I inspect the index.js that rollup builds, I see that all usage of react from the “app” uses the following react ref:
var reactExports$1 = react$1.exports;
const React$1 = /*@__PURE__*/getDefaultExportFromCjs(reactExports$1);
But the one from useDebounceValue seem to have it’s own react reference which ends up being null.
var reactExports = react.exports;
const React = /*@__PURE__*/getDefaultExportFromCjs(reactExports);
/**
* @license React
...
function useDebounceValue(value, delay) {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value]);
return debouncedValue;
}
What am I doing wrong here?