React Context is missing! ESModules / CommonJS might be the cause.

Christian Senk
5 min readJan 9, 2024

--

The problem

We recently updated the UI Kit that is widely used within our products. It was only a minor version update and contained a few fixes we reported and needed to continue our work. As it was a rather small update we didn’t expect many problems along the way.

Naturally, we were quite surprised to find the following error:

Uncaught Error: It appears the <Provider /> is missing. Make sure to wrap your app in it.

This error is kind of familiar and I guess most of the React devs out there know this one. Most of the time, some sub-tree in the application is missing the correct provider further up the tree. A provider that is also used by the UI Kit. A provider that is of course used by us, as it was also needed before the minor update. So what was the cause?

The cause

As a passionate dev, I often search and scan articles to solve the issue at hand to return later and read the article for a profound understanding.

I want this article to help other devs who are in the same situation as me having the same issue, so here is the cause right away.

The UI Kit we use fixed their package.json to correctly declare the ESModules and CommonJS bundles. The UI Kit was already delivered in both flavors, but only the fix caused classic bundlers like Webpack to behave differently. Both, ESM and CJS variants of the same UI Kit were subsequently bundled. The <Context.Provider /> was backed by the ESM variant, while the <Context.Consumer /> was backed by the CJS variant. Although it’s originally in the same package with the same version, both variants don’t know each other. The <Context.Consumer /> has a hard time to find it’s <Context.Provider /> because the CJS version of the provider was never used in this scenario.

If this is enough knowledge to give you an idea of how to fix it, go ahead. The rest of this article will be the story of how we approached the problem. And it will still be here later on.

The approach

To have an appropriate solution, we needed to better understand what was causing the problem.

The core of the problem

The team I worked with didn’t own an application but rather contributed to multiple ones by internally publishing react-based libraries.

A simple dependency graph with three components. “Application”, an ES module. “Library”, a CJS module. “UI Kit” with both ES and CJS modules. “Application” has a dependency on “Library” and “UI Kit”. “Library” has a dependency on “UI Kit”.
Simplified dependency graph with module annotations.

As you see in the above-simplified dependency graph. The application itself uses the UI Kit, while the library we are publishing is using it as well.

Using Webpack as an example bundler, it traverses the dependency graph and bundles all dependencies together (simplified, but serves the purpose of explanation).

Starting at the root (probably the application itself), it does this by resolving import and require statements.

As soon as Webpack encounters an import/require statement it needs to gather information about the package by looking at the package.json. Which fields are important to Webpack depends on the file contents. The most up-to-date declaration of this kind of information is package exports. The fixed version of the UI Kit was now using this kind of declaration.

"exports": {
"./package.json": "./package.json",
".": {
"types": "./build/esm/index.d.ts",
"import": "./build/esm/index.js",
"default": "./build/cjs/index.js"
}
}

The libraries we published up to this point contained only the main field because we published a CJS-only package. It also contained the types field for type declarations, but that is merely a side note for completeness.

"main": "./index.js",
"types": "./index.d.ts",

Considering the kind of import/require statement and the information taken from the package.json, Webpack is now able to decide what to do. Let’s go over a simplified step-by-step description

Application > UI Kit

  1. Start with index.js from the root application.
  2. Encounter an import statement importing the UI Kit directly.
  3. Look at the package.json of the UI Kit, finding the exports entry.
  4. Take the ESM variant because
    - an import statement is used
    - exports declared a variant to be used with import statements.

The dependency of the root application on the UI Kit was resolved by using the ESM variant. Let’s have a look at how Webpack resolved the dependency on our library.

Application > Library

  1. Start with index.js from the root application.
  2. Encounter an import statement importing library.
  3. Look at the package.json of the library, finding the main entry.
  4. Take the CJS variant because
    - an import statement is used
    - exports is not available
    - main declares the presence of a CJS variant with no other options available, not even module (see resolve.mainFields)

Up until now, everything seems fine. The dependency of the root application to our library was resolved by using the CJS variant. The problems start if we look at what happens if the dependency from our library to the UI Kit is resolved.

Application > UI Kit

  1. Start with index.js from the library.
  2. Encounter a require statement (not an import, because it’s a CJS package) requiring the UI Kit.
  3. Look at the package.json of the UI Kit, finding the exports entry.
  4. Take the CJS variant because
    - a require statement is used
    - exports declare a variant to be used with require statements.

This time, the dependency on the UI Kit was resolved using the CJS variant. Although everything looks very similar we now have two variants of the UI Kit in the bundle. Which leads to the problem this whole article is about.

The solution we implemented

The source of the problem was that our library was published as a CJS package. Once Webpack traversed the dependency graph and encountered our library it continued to bundle dependencies of our library with CJS variants as well, because it encountered require statements.

A simple dependency graph with three components. “Application”, an ES module. “Library”, a CJS module. “UI Kit” with both ES and CJS modules. “Application” has a dependency on “Library” and “UI Kit”. “Library” has a dependency on “UI Kit”. The new ES module variant for “Library” is highlighted.
The same graph as above with the ES variant that was added.

Since our library was used in multiple applications we couldn’t make any assumptions like “this library will only be used in a ESM context” especially because this could also be different for the same application if it was for client-side or server-side rendering. We simply had to deliver our library with both ESM and CJS variants. This enables the bundler or runtime to decide on their own which variant fits best.

"module": "build/esm/index.js",
"main": "build/cjs/index.js",
"types": "build/esm/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./build/esm/index.d.ts",
"import": "./build/esm/index.js",
"default": "./build/cjs/index.js"
}
},
"sideEffects": false

What’s next?

You hopefully got a good idea of the problem, the cause, and the solution we’ve implemented. React Context might have a hard time working if your bundle contains the package in ESM and CJS variants by accident. It can be solved by delivering a package in both variants if it would force your bundler otherwise into a specific variant.

I don’t want to go beyond the scope of this article, this is not the end of the story, and there are some open points:

  • How is such a package built if the source is written in typescript?
  • Many run-/buildtime tools are behaving differently. Your library is probably bundled using something like Webpack to be used on the client-side by a browser. But it also needs to work with server-side rendering executed by node.

But these are points I want to discuss in other articles.

--

--