4

I am creating a React component library for a Credit Card Form and the CreditCardForm.tsx looks like this:

CreditCardForm.tsx:

import React, {useState} from "react";

interface CreditCardFormProps {
  testState: any;
  name: string;
  onChangeName: () => void;
  cardNumber: string;
  onChangeCardNumber: () => void;
  expirationDate: string;
  onChangeExpirationDate: () => void;
  cvv: string;
  onChangeCvv: () => void;
  onSubmit: () => void;
}


const CreditCardForm = (props: CreditCardFormProps) => {
  const [testState, setTestState] =useState<any>("hello")
  return (
    <form onSubmit={props.onSubmit}>
      <label htmlFor="name">Name on Card</label>
      <input
        type="text"
        id="name"
        name="name"
        value={props.name}
        onChange={props.onChangeName}
        required
        pattern="^[A-Za-z\s]*$"
        title="Please input your name correctly"
      />
      <p>test: {testState}</p>

      <label htmlFor="cardNumber">Card Number</label>
      <input
        type="text"
        id="cardNumber"
        name="cardNumber"
        value={props.cardNumber}
        onChange={props.onChangeCardNumber}
        required
        minLength={16}
        maxLength={16}
        pattern="^[0-9]*$"
        title="Your Card Number should contain only numbers."
      />
        <label htmlFor="expirationDate">Expiration Date</label>
        <input
          type="number"
          id="expirationDate"
          name="expirationDate"
          value={props.expirationDate}
          onChange={props.onChangeExpirationDate}
          required
          title="Please input your Expiration Date correctly"

        />

      <label htmlFor="cvv">CVV</label>
      <input
        type="text"
        id="cvv"
        name="cvv"
        value={props.cvv}
        onChange={props.onChangeCvv}
        required
        minLength={3}
        maxLength={3}
        title="Your CVV should contain only numbers."

      />

      <button type="submit">Submit</button>
    </form>
  );
};

export default CreditCardForm;

package.json

{
  "name": "@packagename",
  "version": "0.1.24",
  "description": "A react component library",
  "scripts": {
    "rollup": "rollup -c"
  },
  "author": " Name ",
  "license": "ISC",
  "peerDependencies": {
    "react-dom": "^18.2.0",
    "react": "^18.0.2"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^24.1.0",
    "@rollup/plugin-node-resolve": "^15.0.2",
    "@rollup/plugin-typescript": "^11.1.0",
    "@types/react": "^18.2.0",
    "rollup": "^3.21.0",
    "rollup-plugin-dts": "^5.3.0",
    "source-map-explorer": "^2.5.3",
    "tslib": "^2.5.0",
    "typescript": "^5.0.4"
  },
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "files": [
    "dist"
  ],
  "types": "dist/index.d.ts",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com/@username"
  },
  "dependencies": {
    "react": "^18.0.2",
    "react-dom": "^18.2.0"

  }
}

I used rollup to build my package and my rollup.config.mjs file looks like this:

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import packageJson from "./package.json" assert { type: "json" }; //we import package.json so when we use commonjs modules

export default [
  //first configuration object
  {
    input: "src/index.ts", //entry point for this part of our library (it exports all of our components so that the user can import the library)
    output: [
      {
        file: packageJson.main,
        format: "cjs",
        sourcemap: true,
      }, //commonjs modules, which will use the main field of packageJson module
      {
        file: packageJson.module,
        format: "esm",
        sourcemap: true,
      }, //for the es6 modules
    ],
    plugins: [
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json" }),
    ], //node resolve plugin, and the other plugins (typescript plugin needs the specific directory)
  },

  //second configuration object
  {
    input: "dist/esm/types/index.d.ts",
    output: [{ file: "dist/index.d.ts", format: "esm" }],
    plugins: [dts()],
  },
];

So when I publish this react library (npm publish), and I install it on a react app project(to test it) and I run the project(npm start) it returns me this error:

Compiled with problems:
×
ERROR
Cannot read properties of null (reading 'useState')
TypeError: Cannot read properties of null (reading 'useState')
    at Object.useState (http://localhost:3000/static/js/bundle.js:2135:29)
    at CreditCardForm (http://localhost:3000/static/js/bundle.js:3112:50)
    at renderWithHooks (http://localhost:3000/static/js/bundle.js:24102:22)
    at mountIndeterminateComponent (http://localhost:3000/static/js/bundle.js:27388:17)
    at beginWork (http://localhost:3000/static/js/bundle.js:28684:20)
    at HTMLUnknownElement.callCallback (http://localhost:3000/static/js/bundle.js:13694:18)
    at Object.invokeGuardedCallbackDev (http://localhost:3000/static/js/bundle.js:13738:20)
    at invokeGuardedCallback (http://localhost:3000/static/js/bundle.js:13795:35)
    at beginWork$1 (http://localhost:3000/static/js/bundle.js:33669:11)
    at performUnitOfWork (http://localhost:3000/static/js/bundle.js:32916:16)

Why is this shown and how can I fix it?

Martina
  • 123
  • 9

1 Answers1

6

As we discussed in the comments, when you build a library that should use host's runtime, in your case react and react-dom, you have to externalize them.

Basically you should do 2 things:

  1. Let the package manager know that you don't want clients of your lib to download react again. In your lib's package.json, you got to remove react and react-dom from dependencies and put them into devDependencies and peerDependencies instead. devDependencies signal that you use react for development locally, and peerDependencies signal that for production, react will be provided by the host app.
  2. That's not enough however, you've got to tell Rollup explicitly as well that it should externalize react deps. The reason is simple - when Rollup goes through your source files, it resolves the import statements it finds, and it does so primarily by looking into node_modules. And in your case it doesn't matter whether you defined react in dependencies or devDependecies, it just finds react deps in node_modules and bundles them in your lib anyway.

You violated the 2nd condition, and you ended up with 2 sets of react dependencies in your app, which throws your error.

Now your error was confusing, because it sounded like the react import is wrongly resolved, but actually the original error was You might have more than one copy of React in the same app (I tried to run your lib myself).

So much for the root cause, and now how to fix point 2? Just configure Rollup to externalize react!

rollup.config.mjs

import external from 'rollup-plugin-peer-deps-external';

export default [
  {
    plugins: [
      external(),
      // your other plugins
    ],
    external: ["react", "react-dom"],
    // rest of your config
  }
];

This way you tell Rollup that it shouldn't bundle react or react-dom when it encounter their import, but rather that it prepares for host app to have it instead. This removes react deps from your lib bundle and eliminates your error!

Dan Macak
  • 16,109
  • 3
  • 26
  • 43