Tina Development & NPM Links

Hi all!

I know we’ve all had issues with local development copies of Tina, and React/React-Dom making a huge mess.

I recently spent a whole week debugging this, and here is what I’ve learned.

The Error

If you get the following error from your application:

Unminified error output:

**Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See **https://fb.me/react-invalid-hook-call** for tips about how to debug and fix this problem.**

Minified error output:

Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant=321 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

This indicates one of two things:

  • You used hooks wrong, look at this doc and verify whether or not this is not true. If it isn’t, then;
  • You have mismatched versions of React or React DOM when your app is compiling.

Solutions

If it’s not hooks, then it’s mismatched versions of React or React DOM.

This causes an error because hooks require all hooks of an application to be using the same version of React or React DOM, and the node module resolution algorithm will resolve the closest parent node modules folder.

For example, if you use:

cd path/to/tina/local/copy 
npm link 
cd path/to/local/application/folder 
npm link tinacms

You might end up with a node module structure like this in your application:

|- src/
|--- index.ts        => any imports of react/react-dom will resolve node_modules in root
|- node_modules/
|--- react
|--- react-dom
|--- tinacms           => any imports of react/react-dom will resolve node_modules in node_modules/tinacms
|----- node_modules 
|------- react
|------- react-dom

There are three ways of resolving this:

  1. Delete the duplicate dependencies in the development project’s (tina or a tina plugin) node_modules folder (this is the most annoying )
  2. NPM link dependencies in both directions
  3. Use bundler aliases to enforce resolution of one path for all imports

As well as all approaches requiring that react and react dom are not dependencies of the project, but instead devDependencies and peerDependencies .

This ensures that during automated tests for that project, react/react dom are available as devDependencies, but not when the project gets built on a CI, and that users are warned if they are missing those dependencies when installing them in their projects.

Delete the duplicate dependencies

If node wants to resolve imports from the closest node modules folder, then make it the right one by deleting the dependencies in folders down the chain.

Pros:

  • takes five seconds

Cons:

  • this needs to be repeated every time the dev project has its dependencies restored

NPM link dependencies in both directions

You can use the “link” feature of NPM or Yarn to emulate the node module installation process with symlinks.

To do so, the following is expected to already have been done:

  • NPM dependencies are installed in the application project
  • NPM dependencies are installed in the tina or tina plugin project

Now, let’s assume that the package we’re developing is called react-tinacms-coolpackage.

First, go to the tina or tina plugin project and run:

npm link # this creates a symlink to this folder as the "name" field of the package.json, as if you ran npm install react-tinacms-coolpackage -g 
npm link ../path/to/application/folder/node_modules/react # this ensures the dev project uses the react from the application 
npm link ../path/to/application/folder/node_modules/react-dom # this ensures the dev project uses the react-dom from the application

Then go to the application and run:

npm link react-tinacms-coolpackage # this ensures that the application will install a symlink to the local dev project as if we ran npm i react-tinacms-coolpackage -S

Now everything should work properly, and reversing this is as simple as running the following in the application:

npm unlink react-tinacms-coolpackage

And the following in the dev project:

npm unlink ../path/to/application/folder/node_modules/react 
npm unlink ../path/to/application/folder/node_modules/react-dom 
npm unlink

Use bundler aliases

If your application uses a bundler like webpack (which next.js does, for example) and you can edit or run your own config, then aliases can do the same job as npm link.

For example, to create an alias in next.js, you can do the following in next.config.js:

module.exports = (phase, { defaultConfig }) => {
  // aliases node modules by name to a specific node_modules folder
  const mapToFolder = (dependencies, folder) => {
    return dependencies.reduce((acc, dependency) => {
      return {
        [dependency]: path.resolve(`${folder}/${dependency}`),
        ...acc
      }
    }, {});
  }

  return {
    webpack: (config, { dev }) => {
      // Prevent development overlaps of react dependencies
      config.resolve.alias = {
        ...config.resolve.alias, // use current config
        ...mapToFolder(['react', 'react-dom'], './node_modules') // add aliases for react and react-dom to local node_module folder
      }

      return config;
  }
}
1 Like