2

I'm writing a sample typescript react component library with folder structure as shown below, exporting just 1 button component. On running npm run build, just one js bundle is generated. The bundle has the react library code (did not use externals property yet in webpack config) but not the exported button component with the button tag (on searching keyword button in bundle file, was getting no results). Have added the code of bundle/build file also.

Not sure where I'm going wrong.

This issue does not happen with webpack bundles while exporting simple javascript function i.e. the exported javascript functions are found in generated bundle but not exported react component.


Update :

After adding output.library.name = "template-react-component-library" and output.library.type = "umd" in my webpack config , I was able to see the button tag (of exported component) in webpack bundle file but on importing the same button component from the template-react-component-library, I'm getting an empty object. Moreover, with exposing normal javascript functions from a library with same webpack configuration, I'm getting the javascript function and not empty object on importing it from library and that also without adding any output.library configs.

So now the question is how to solve the empty object on importing component from the library and why output.library behaves differently in exposing javascript functions and react components.


Repository link : https://github.com/dhiren-eng/template-react-component-library

Folder structure :

Folder structure

webpack.config.js :

const path = require("path")
module.exports = {
entry: "./src/index.ts",
output: {
 filename: "main.js",
 path: path.resolve(__dirname, 'build'),
 clean: true
},
module: {
 rules: [
 { test: /\.(jsx|js|tsx|ts)$/, exclude: /node_modules/, use: {loader: "babel-loader", options: {presets: ["@babel/preset-env","@babel/preset-react"]}} },
 { test: /\.(tsx|ts)$/, exclude: /node_modules/, use: ["ts-loader"] }
 ]
},
resolve: {
 extensions: ['.ts', '.tsx', '.js', '.jsx']
}
}

tsconfig.json :

{
compilerOptions: {
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "build",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true
}}

Installed below packages. package.json :

{
 "name": "template-react-component-library",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
 "test": "echo \"Error: no test specified\" && exit 1",
 "build": "webpack --config webpack.config.js"
 },
 "author": "",
 "license": "ISC",
 "devDependencies": {
 "@babel/cli": "^7.21.5",
 "@babel/core": "^7.21.8",
 "@babel/preset-env": "^7.21.5",
 "@babel/preset-react": "^7.18.6",
 "@types/react": "^18.0.27",
 "babel-loader": "^9.1.2",
 "react": "^18.2.0",
 "ts-loader": "^9.4.2",
 "typescript": "^4.9.5",
 "webpack": "^5.76.2",
 "webpack-cli": "^5.0.1"
}

src/components/Button/Button.tsx :

import React from "react";
export interface ButtonProps { 
 label: string;
}
const Button = (props: ButtonProps) => {
 return <button>{props.label}</button>;
};
export default Button;

src/components/Button/index.ts :

export {default} from './Button'

src/components/index.ts :

export {default as Button} from './Button'

src/index.ts :

export {Button} from './components'

Build file generated, main.js :

 /*! For license information please see main.js.LICENSE.txt */
(()=>{"use strict";var t={408:(t,e)=>{Symbol.for("react.element"),Symbol.for("react.portal"),Symbol.for("react.fragment"),Symbol.for("react.strict_mode"),Symbol.for("react.profiler"),Symbol.for("react.provider"),Symbol.for("react.context"),Symbol.for("react.forward_ref"),Symbol.for("react.suspense"),Symbol.for("react.memo"),Symbol.for("react.lazy"),Symbol.iterator;var o={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},r=Object.assign,a={};function n(t,e,r){this.props=t,this.context=e,this.refs=a,this.updater=r||o}function c(){}function p(t,e,r){this.props=t,this.context=e,this.refs=a,this.updater=r||o}n.prototype.isReactComponent={},n.prototype.setState=function(t,e){if("object"!=typeof t&&"function"!=typeof t&&null!=t)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,t,e,"setState")},n.prototype.forceUpdate=function(t){this.updater.enqueueForceUpdate(this,t,"forceUpdate")},c.prototype=n.prototype;var s=p.prototype=new c;s.constructor=p,r(s,n.prototype),s.isPureReactComponent=!0;Array.isArray,Object.prototype.hasOwnProperty},294:(t,e,o)=>{o(408)}},e={};!function o(r){var a=e[r];if(void 0!==a)return a.exports;var n=e[r]={exports:{}};return t[r](n,n.exports,o),n.exports}(294)})();

After running the webpack build, 2 folders are generated :

  1. Types folder as mentioned in outDir property of tsconfig.json
  2. Build folder generated by webpack

enter image description here

asked May 16, 2023 at 13:31
7
  • Why would you expect that to be part of the output? Commented May 16, 2023 at 16:31
  • If I'm bundling a file which exports a react component, so I would naturally expect react component in the bundle and since the react component renders button element, so I'll be expecting the button element also in the bundle Commented May 16, 2023 at 16:41
  • But the button element is part of the runtime environment. It's provided by the browser. There's no way for it to do what you're asking and it wouldn't make sense for it to do so either. Commented May 16, 2023 at 16:43
  • I mean to say that I'm expecting a button tag in the bundle. We will be needing the button tag as a parameter of the function of React.createElement(), in the bundle. Otherwise how will the runtime environment know what element to render ? Commented May 16, 2023 at 16:47
  • 1
    Here is the link to the repository : github.com/dhiren-eng/template-react-component-library Commented May 18, 2023 at 5:57

1 Answer 1

1

OK where should I start? First let me say that I am heavily in favour of distributing libraries in ESM (Ecma Script Module or simply module) format, since it's the most promising, modern module format out there.

That doesn't mean you can't distribute your lib in other formats (eg. UMD) as well, but I will focus on ESM in my answer because it's the preferred way to consume component libraries in React apps.

Configuring Webpack

Now that we know we want Webpack to produce ESM output, let me tell you Webpack's support for ESM is still experimental so there might be issues along the way.

Let's change webpack.config.js to produce ESM:

 output: {
 ...,
 library: {
 type: "module",
 },
 },
 externals: {
 react: "react",
 },
 experiments: {
 outputModule: true,
 },
 ...

With this config, the output will contain something like import * as React from 'react', which is what we want.

Now there is a problem, upon TS transpilation Webpack for some reason doesn't respect "allowSyntheticDefaultImports": true from TS config, which should make import React from 'react' work even though 'react' doesn't provide any default export. But for some reason, the output still tries to retrieve default from react, which is not present. This leads to issues

Cannot read properties of undefined (reading 'createElement')

which means react wasn't resolved correctly.

I didn't find any ideal solution here, but what worked for me is this:

  1. Add externalsType: "import", to webpack.config.js. This will import react dynamically, but is not desirable since it adds some 2kB of runtime to your output.
  2. Use import * as React from 'react' in your source files.

Both of those work and make it possible to use your component with import { Button } from 'your-lib';. But they both also kinda suck.

Alternative

Which begs the question, if there isn't more appropriate bundler for libraries. I'd suggest Rollup, either directly or through Vite. Unlike Webpack, it has excellent support for ESM format out of the box.

Add rollup.config.js

import typescript from "@rollup/plugin-typescript";
import babel from "@rollup/plugin-babel";
import external from "rollup-plugin-peer-deps-external";
import terser from "@rollup/plugin-terser";
export default {
 input: "src/Button.tsx",
 output: {
 file: "build/main.js",
 format: "es",
 sourcemap: true,
 },
 plugins: [
 typescript(),
 babel({
 presets: ["@babel/preset-react"],
 }),
 external(),
 terser(),
 ],
 external: ["react"],
};

This config is a precise mirror of your Webpack config, and is imo easier to configure and read. Of course you have to install the needed dependencies

yarn add -D rollup rollup-plugin-peer-deps-external @rollup/plugin-babel @rollup/plugin-terser @rollup/plugin-typescript tslib

Another side effect is that Rollup's output, at least in your case, is much smaller, and basically amounts to:

import t from "react";
const e = (e) => t.createElement("button", null, e.label);
export { e as Button };

whereas Webpack's is much bigger.

PS: If you encounter problems because the Rollup config uses import and export, just add "type": "module", to your package.json, which will force your project files to use ESM instead of CommonJS (and its module.exports and require()).

answered May 18, 2023 at 11:20
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks. But 'react' package does provide default export which can be understood from the basic import statement in every react component i.e. import React from 'react'
No it doesn't. You can verify this by setting "allowSyntheticDefaultImports": false in your tsconfig.json. It will throw an error which mentions the missing default export.
Thanks, I changed the webpack config to the one mentioned by you i.e. output.library.type="module", externals.react="react" and experiments.outputModule=true without adding externalsType: "import" , but I never got the error "Cannot read properties of undefined (reading 'createElement')" while running webpack build or importing from library . I'm still getting an empty object on importing from the library and it's the same even after adding externalsType: "import" in webpack config
I tried again with the changes for webpack config from my answer and it works still fine. How are you importing your library in your project? Also, can you share how your lib's main.js looks like?

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.