Skip to main content

Introducing Contensis React Base 3

Wednesday, 30 March 2022
NF
Contensis
Neil FlatleySolutions architect
News10 min

Say hello to React 17, dynamic component splitting and a new approach to Redux state management. Upgrade your project today to realise the benefits of writing simpler code and creating smaller bundles – with full TypeScript support.

A breaking change you say? No thank you, that’s not for us… If that sounds like you, then suspend your disbelief, read on, and let’s see if we can’t change your mind.

A number of key dependencies have been updated in Contensis React Base, which will require some dependency management and a few tweaks to your Webpack build to get the best experience.

We have removed deprecated dependencies such as react-loadable and moved over to the supported and recommended @loadable/components, with @loadable/server already implemented for your server-side rendered components.

We also now natively offer state management with immer. Existing functionality has been rebuilt with Immer at the core and this is the new standard, in keeping with React community recommendations. We have deprecated the existing approach to state immutability with immutable library for our new projects, although use of Immutable syntax is still supported for existing projects. This is to allow the time to plan and test refactoring any Immutable code that may be currently running in your project.

Finally, we have upgraded key dependencies such as react, react-dom, @hot-loader/react-dom and react-helmet to their latest major revisions.

Why is it a breaking change?

This version introduces React 17 as a dependency. React is very sensitive to having multiple versions installed and running them at the same time. Depending on what else you use in your own project, you may not need to do anything to start using Contensis React Base v3.0. For example, if your project dependencies are already up-to-date and you are not using Storybook in your project at all, you should be fine. However, in order to get the best out of the changes introduced by the upgrade you should still read on.

What do I need to do?

Dependency housekeeping

React

Check your project’s package.json file under dependencies. Do you have “react”, “react-dom” or “@hot-loader/react-dom” installed from here? If so, uninstall them – we want the correct version of these dependencies to be installed and managed by contensis-react-base.

When your app is built with multiple versions of React installed you will see an "Invalid Hook Call Warning" in the browser console and your app will not render when testing after installing this update.

To remove all existing React dependencies, run:

Bash
npm uninstall react npm uninstall react-dom npm uninstall @hot-loader/react-dom

Storybook

Many existing projects upgrading to v3.0 are likely to be using an older version of Storybook, which forces an installation of another version of React. When running along with React 17, this results in a critical console error which prevents the app from rendering.

You will need to upgrade your version of Storybook to be >= 6.1, which has support for React 17. Further reading

Storybook has provided a simple way to upgrade. Run npx sb upgrade when upgrading to v3.0 of contensis-react-base. You should also ensure you are running the latest version of webpack or webpack@4 to prevent further errors when loading Storybook.

If you are still getting React errors after this, check your project for other dependencies to remove/update that rely on legacy versions of React. Instances of the string “react” in package-lock.json file will tell you exactly what versions are being asked for by which package and therefore being installed.

React Helmet

You will likely see errors when running your project after upgrading. The latest major update for React Helmet means there is no default export for this library anymore. The fix is very simple.

Search your entire project for:

Bash
import Helmet from 'react-helmet';

Replace it with:

Bash
import { Helmet } from 'react-helmet';

Webpack

We are beginning to upgrade our application builds to Webpack 5, and due to the dependency management and all the other upgrades we have done, this should be an easy transition.

You should be running on at least the latest version of Webpack@4 to minimise the risk of build errors creeping in when other dependencies such as build plugins are updated.

Bash
npm i webpack@4 --save-dev

Zengenti Forms Package

If your project references zengenti-forms-package you will need to update this to version 2.

This update is needed to fully support the parts of the Redux store that we are treating as Immer-based when we hydrate the application after SSR. The rewritten parts of version 2 allow the core of the application to work with a single state type, and not run in a hybrid state with immutable and Immer both in play. As the core of the application is now written for Immer, we needed a way for consumers of Zengenti Forms Package to be able to drop immutable from their project build completely.

We will explore the new stateType option and immutable changes later in the post. For now, run this to install the latest version of the forms package:

Bash
npm i zengenti-forms-package@prerelease

Note: The @prerelease tag is required to install version 2 at time of writing. Check the versions page in npm to see the current @latest version.

Finally, upgrade Contensis React Base

After checking all of the required steps we can now make the upgrade.

Bash
npm i @zengenti/contensis-react-base@prerelease

Note: The @prerelease tag is required to install version 3 at time of writing. Check the versions page in npm to see the current @latest version.

Check your package.json file to see @zengenti/contensis-react-base under dependencies installed with version 3.

What else should I do?

Convert deprecated react-loadable code

React-loadable is deprecated and has been superceded by React.lazy. However, as this does not support SSR, we are now fully supporting @loadable/components.

You will likely have code in your project that looks like the below (you can do a global search for react-loadable to find all references):

Bash
import Loadable from 'react-loadable'; import { Loading } from './Loading'; const staticRoutes: StaticRoute[] = [ { path: '/', exact: true, fetchNode: true, component: Loadable({ loader: () => import(/* webpackChunkName: "home.page" */ '~/pages/Home/home.page'), loading: Loading, }), }, ]; export default staticRoutes;

You will need to change the code to look like this:

Bash
import loadable from '@loadable/component'; const staticRoutes: StaticRoute[] = [ { path: '/', exact: true, fetchNode: true, component: loadable( () => import(/* webpackChunkName: "home.page" */ '~/pages/Home/home.page') ), } ]; export default staticRoutes;

In this case the loadable syntax is very important. Do not change casing or aliases and do not abstract the loadable function into any helpers or library code.

You will also need to add a Babel plugin and configure the Webpack plugin. We can take some old ones out when we reconfigure these.

All instances of HtmlWebPackPlugin must have their inject property set to false - all bundle injection is handled server side for us with @loadable/server.

A side-by-side comparison of the differences in the webpack plugin in Contensis React Base 3 versus Contensis React Base 2..
./webpack/webpack.config.prod.js
JavaScript
const plugins = { base: [ [ 'module-resolver' ], 'react-hot-loader/babel', // Replace... // 'react-loadable/babel', // with... '@loadable/babel-plugin', // ... '@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-optional-chaining', ], };

We should remember to install these new build plugins in our project:

Bash
npm i @loadable/babel-plugin @loadable/webpack-plugin --save-dev

We can clean up our project a little by uninstalling the packages for any import or require that we have removed – ensuring they are no longer required anywhere else in the project - with npm un <package-name>, e.g.

Bash
npm un webpack-module-nomodule-plugin

Convert existing `immutable` code

We have removed all references to ‘immutable’ from our application code and are now using immer for our state management. We are continuing to support existing immutable stores by requiring a stateType option to activate the new lightweight plain JS store.

Projects will continue to get an immutable store by default, but with the v3.0 upgrade we will be lazy-loading the additional immutable dependencies rather than bundling them by default.

To activate the Immer (plain JS) store you will need to add stateType: 'js' to both the client and server entrypoints in your application.

You will need to ensure your code does not reference ‘immutable’ anywhere. Do a global search for the following syntax: .toJS(), fromJS(, .get(, .getIn(, .set(, .setIn(. This syntax is now deprecated and will log console errors when accessed from an app with a plain JS store.

If you have Redux features that are bespoke to your application (these will be registered in ~/core/redux/reducers and ~/core/redux/sagas), it is important that all the code in here is no longer referencing any kind of immutable syntax.

Reducers

This is the biggest change and will involve refactoring every custom reducer in your application to use the Immer approach. In our experience this is a much simpler task than it may appear, as we are actually replacing old, complex, and sometimes convoluted immutable syntax, into plain JS assignments.

Instead of having to update state using syntax like this: state.set(‘someKey’, fromJS(action.someValue));

The Immer syntax will be more like state.someKey = action.someValue; – much simpler to write and it provides the same effect.

At the core of the Immer functionality is the produce function and we will integrate this into each reducer. There is plenty of information out there on this subject – some simple examples can be found here.

Selectors

Selectors will most likely look something like this: state.getIn([‘myFeature’, ‘someKey’]).

As state is not an immutable object any more, .getIn will no longer exist on our non-Immutable state object. We will refactor all selectors to be expressed in plain JavaScript and remove all Immutable syntax.

For example state.myFeature.someKey. Because our app state is now just plain JS, we will never need to call .toJS() or wrap our components with ToJS(MyComponent) to convert our props into non-Immutable objects, and as such all references to any of this syntax should also be removed from your app.

Sagas

Saga code is likely to be calling some of the deprecated Immutable syntax examples above, e.g. if a selector is used that returns an Immutable Map instead of a plain object.

If you have already done the global searches mentioned above and the necessary refactoring to remove the immutable syntax this should be already converted.

After converting to immer state management I get “Object is not extensible error”

Cannot add property results, object is not extensible

This means somewhere, we are mutating the state in our app. This was previously masked by us always needing to constantly convert immutable state into plain JS via calling object.toJS() or wrapping our component with another higher-order component that converts our injected props for us, e.g. export default toJS(MyComponent). We would end up with a new object which can be mutated, often unintentionally.

This cannot happen going forward and the offending code needs to be refactored so we are not mutating the original state object. This is just poorly written code and we have been grateful to the Immer library for bringing this to our attention each time we have seen this error.

Keep your project dependencies up-to-date

We cannot update any dependency mentioned in your own package.json file. You need to maintain this yourself.

Run npm outdated command in your project directory to see what it returns.

Sometimes we intentionally run on older package versions. This is usually to maintain compatibility with something we absolutely require, or to be compatible with another dependency we are yet to update. Be aware of this when upgrading your project’s dependencies.

Breaking changes should not be avoided. Instead, check the package’s documentation / Google search for what these breaking changes are. Most of the time they either don’t affect you, or you just need to update a small bit of code, config or change an import to follow modern conventions. It might be a short painful spell creating errors, but it is always best to endure the short term pain to reap the long term benefits.

Have your tools work for you by keeping your linting tools and plugins updated to ensure the latest conventions are loaded by default.

To upgrade all packages in your project you can run npm upgrade, and to upgrade a package to the latest major revision you can run:

Bash
npm i <package>@latest --save/--save-dev

What else could I do?

Check out our Leif demo site and GitHub repository for code examples. The Leif site is running on Contensis React Base 3 and will contain examples of everything we have reviewed in this article.

Speak to someone at Zengenti to discuss approaches for rebasing your existing project inside our latest TypeScript starter that is already running with the most up-to-date configuration.

Why should I bother?

Reduced bundle sizes and lazy loading

With the introduction of Immer and plain JS for application state management, we instantly lose 64KB of minified data from our vendor bundle, just by not loading the core immutable library. That along with a simpler syntax means less application code gets written and built into the app bundle as well.

We shaved at least a third of our vendor bundle size off our starter project, and if you are not ready to convert your custom redux feature code into immer/plain JS yet, we will lazy load all the necessary immutable libraries and create an immutable application state so your application should continue to work as before.

A screenshot of the Webpack bundle analyzer built into react-starter with `npm run analyze`.
Webpack bundle analyzer built into react-starter with `npm run analyze`.

Best server-side rendering

Prior to Contensis React Base 3 we have used react-loadable for our code splitting and lazy loading. It has served us well, but has been out of support for some years. Warnings are fired about it using deprecated React features in development and we have recently seen issues with their server-side module capturing, meaning not all of our split bundles are getting loaded up front.

We have reimplemented this using the React-recommended approach of the @loadable/components library. We expect to see all of your application’s split chunks to be rendered at the server side, avoiding the ‘flicker’ you may or may not see during client-side hydration.

Improved performance scores

As we keep an eager eye on our bundle sizes and contents for any opportunity to make this smaller, whenever this drops we always see improvements in performance scores.

A screenshot of a Google Lighthouse test on a local project running Contensis React Base 3.

Better SEO (so I’m told)

I’m passionate about writing good quality code. Someone who knows more about this area could elaborate, at this point I would expect to hear some informed comment about Google’s algorithm? However, I believe the theory is better SSR === better SEO.

Best Contensis Cloud experience

As nearly all of our customers are hosted on Contensis Cloud, it makes sense to offer the best experience out of the box. We handle all the hosting intricacies for you, most of which are not on the radar of your average web developer or marketing team – until something has already gone awry.

Like all good software, the cloud infrastructure constantly evolves and receives upgrades too. We will always make sure the latest contensis-react-base version provides the necessary knobs, collects and emits the right cache invalidation headers and talks to the right bit of the infrastructure to get the job done.

Maintained upgrade path

When you ask for help, your help speaks the same language. This is true if you’re Googling around for answers or approaches, or contacting Zengenti support to report an issue.

We don’t like to deal with too many breaking changes in our industry, so it's fair to say v3.0 will be the maintained version for the foreseeable future. And best of all, new features will become available to you for free.

Begin upgrading your project today to gain the benefits.

I don’t want to deal with all this stress

Get in touch if you'd like to work with our customer experience squad (CXS) or put together a professional services squad to get the required upgrades for your specific implementation.