Integrate React JS into Sencha Ext JS
Create a simple React library usable within a Sencha Ext JS application
Adding React to an existing Ext JS application can be challenging. As of today (Aug 14, 2020) there are only a few solutions present in the wild for how best to incorporate a React application into an existing Ext application. That said, however, there are no great methods for incorporating components at a more module level or in a way that allows for the common use of JSX.
Recently, I approached this exact problem. While we would all love to throw away a project and start something new, that is unfortunately not the reality under most circumstances. Often the best solution, when possibly, is to allow for the integration of new content into an existing system. The goal, in this particular case, was to allow front-end engineering teams to develop new content in React while integrating into the existing application and a paced conversion of legacy content.
In this guide, I am going to create a React library that loads a basic header
with a couple sub-components. Then I will take that code and turn it into a re-usable npm package that can be used in the browser and node with webpack
, babel
, and typescript
. From that point we can easily integrate the React library into Ext containers via the React vanilla JS library.
Building a Basic React Library
Step 1: Create & init the app
$ mkdir react-library && cd react-library && npm init
Step 2: Create Some Components & Header Module
$ npm install react react-dom$ mkdir src src/components src/modules src/components/Title src/components/Nav src/modules/Header
With the structure in place we can create our components and styling.
Nav
.global-nav ul {
display: flex;
justify-content: flex-end;
list-style: none;
padding: 0;
}.global-nav ul li {
padding: 0 5px;
}.global-nav ul li:not(:first-child) {
margin-left: 5px;
}
import React from 'react';const Nav = (props) => (
<nav {...props}>
<ul>
<li>
<a href="#">Home</a>
</li>
<li>
<a href="#">About</a>
</li>
<li>
<a href="#">Contact</a>
</li>
</ul>
</nav>
);export default Nav;
Title
import React from 'react';
const Title = ({ children, ...rest }) => (
<h1 {...rest}>{children || 'React Library'}</h1>
);
export default Title;
Header
.global-header {
align-content: center;
align-items: center;
display: flex;
justify-content: space-between;
height: 64px;
}
import React from 'react';
import Nav from '../../index.js';
import Title from '../../components/Title/Title';
import "./header.css";const Header = (props) => (
<header className="global-header" {...props}>
<Title className="global-title" />
<Nav className="global-nav" />
</header>
);export default Header;
Step 3: Setup Exports for Library
Use ES6 re-exporting to create a common library. Read more about this on 2ality.
// Components
import Nav from './components/Nav/Nav';
import Title from './components/Title/Title';// Modules
import Header from './modules/Header/Header';// Library
export default {
components: {
Nav,
Title,
},
modules: {
Header,
},
};
Step 4: Testing
We’re going to utilize react-testing-library
and jest
for unit testing in this library. Follow the setup guidelines for react-testing-library
.
Once installed and set up you’ll need to set up some basic testing for the existing library assets. See the sample library for how I set it up for the demo.
Now run jest
From here we’ll see our first error:
FAIL src/modules/Header/Header.test.js
● Test suite failed to run/Users/hollyos/dev/react-library/rtl.setup.js:2
import '@testing-library/jest-dom/extend-expect';
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^SyntaxError: Unexpected stringat Runtime._execModule (node_modules/jest-runtime/build/index.js:1179:56)
And I thought this would have been the easiest part. Apparently, jest
doesn't like the use of the import
statement.
At the moment, with just an index.js file and jest, its going to be running inside a node.js environment where import
is not yet supported.
Transpile
Step 1: Babel
This is where babel
comes into play. babel
will transpile ("translate/compile") your js files from ES6 (where you can use import
) and ES5 which node.js supports. Jest has a section on enabling babel support here. We'll have to install a few more packages, and a little configuration.
Install & Configure Babel
$ npm install --save-dev babel-jest @babel/core @babel/preset-env @babel/polyfill @babel/preset-react @babel/plugin-transform-modules-umd @babel/core @babel/cli
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
"@babel/preset-react",
],
plugins: [
['@babel/plugin-transform-modules-umd', {
exactGlobals: true,
globals: {
index: 'ReactLibrary'
}
}],
],
};
We’ll also want to add a "babel"
script to our package.json
...
"scripts": {
"babel": "npx babel src/index.js -d lib",
...
Then when you run jest
, you get to the next error:
FAIL src/modules/Header/Header.test.js
● Test suite failed to runCannot find module 'react' from 'node_modules/@testing-library/react/dist/pure.js'Require stack:
node_modules/@testing-library/react/dist/pure.js
node_modules/@testing-library/react/dist/index.js
rtl.setup.jsat Resolver.resolveModule (node_modules/jest-resolve/build/index.js:307:11)
at Object.<anonymous> (node_modules/@testing-library/react/dist/pure.js:31:37)
Aha, progress. Now it knows how to read the import
statement, and the test runs. Now i'm missing a dependency - react
. npm install --save react
. Now re-run jest and all green!
Then we can run our babel
script which will create a lib/index.js file ready to be consumed as a script tag in your browser.
$ yarn babel
Using the library in a browser using a script tag
Step 1: Create a Demo
From the root directory let’s create a base HTML file.
<!DOCTYPE html>
<html lang="en">
...
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
...
<script src="../lib/index.js"></script>
<script>
const ReactElement = React.createElement;
const { Header } = ReactLibrary.default.modules; ReactDOM.render(
ReactElement(Header, null, 'React Component'),
document.getElementById('root')
);
</script>
Step 2: Serve the Demo
Add a script to serve your demo with http-server
...
"scripts": {
...
"serve": "npx http-server --cors -o",
...
And serve the demo:
$ npm run serve> react-library@1.0.0 serve /Users/hollyos/dev/react-library
> npx http-server demoStarting up http-server, serving demo
Available on:
http://127.0.0.1:8080
http://192.168.1.222:8080
Hit CTRL-C to stop the server
open: http://127.0.0.1:8080
Once the page loads we’ll see that we get another error:
Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
if we check in the console the value of ReactLibrary.default.modules.Header
we’ll see that we’re getting undefined
> ReactLibrary.default.modules.Header
> undefined
All we did was transpile the index.js
file. The import code isn’t there, and the browser doesn’t know that its supposed to search your node_modules directory for it (nor can it).
What we need to do is create one js file, that has all the dependencies of that library built into it, so that you only need to add the 1 script tag to your site. Babel can’t handle your dependencies, it can only translate.
Step 3: Webpack
We’ll use webpack to take a look at our file, and everything that is imported/required will then get “packed” into one file.
Install & Setup Webpack
$ npm install --save-dev webpack webpack-cli babel-loader style-loader css-loader
const path = require('path');module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'lib'),
filename: 'react-library.bundle.js',
library: 'ReactLibrary',
libraryTarget: 'var',
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
],
};
...
"scripts": {
...
"webpack": "npx webpack",
...
...
- <script src="./lib/index.js"></script>
+ <script src="./lib/react-library.js"></script>
...
Now that everything is setup and the config is in place we can run the webpack
script.
npm run webpack
Refresh the page, and success!
Integrate
Now that we have our library built and working in browser we can incorporate our library into an existing Ext application, much in the same way. We’ll want to update our app.json
and then create a container for the React components.
Step 1: Include React, ReactDOM, and Our New Library
Here we’ll need to ensure that we include React into our project from the index.html
and then bundle our react library bundle into the ext application.
index.html
...
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
...
app.json
...
"js": [
{ "path": "path/to/library/react-library.bundle.js" },
...
Step 2: Update Webpack & React Inclusion
With the addition of React into the Ext app we’ll run into an error when we try to utilize hooks within our react-library
where there’s multiple versions of React. Unfortunately, this error is not very clear and can lead to some confusion.
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.
We’ll need to update our webpack.config.js
to exclude React from the bundle. But first, let’s create a dummy file that will add React to the global scope.
dummyReact.js
module.exports = window.React;
webpack.config.js
module.exports = {
...
resolve: {
alias: {
react: 'dummyReact.js',
},
},
externals: {
react: 'React',
},
};
Step 3: Access react-library
Within Ext
Create component inside Ext container with the proper component or module. We’ll do this much the same way we did in our demo.
ReactContainer.js
Ext.define('view.ReactContainer', {
extend: 'Ext.panel.Panel',
alias: 'widget.reactContainer', listeners: {
afterrender: function () {
const ReactElement = React.createElement;
const { Header } = ReactLibrary.default.modules; ReactDOM.render(
ReactElement(Header, null, 'React Component'),
document.getElementById('root')
);
}
},
});
And there you have it! You’ve now built a custom React component library and incorporated it into an existing Sencha Ext JS application.