Multiple React Roots in AngularJS
Derek N. Davis
Posted on December 24, 2020
One of the biggest challenges with incrementally migrating an app to React is how to manage multiple React “root” components. In a brand new React app, this isn’t a concern, since there’s a single top-level component. If there are any global providers, they're always at the top.
ReactDOM.render(<App />, document.getElementById('root'));
function App() {
return (
<GlobalCacheProvider>
<NotifierProvider>
{/* components */}
</NotifierProvider>
</GlobalCacheProvider>
);
}
The basic setup for a React app
However, in a migration scenario, there are multiple top-level components. Maybe only the user editing screen and product viewing screen have been converted to React. These components will need all the global providers typically found in an App component and may need to access shared state, like app configuration, internationalization, a router or cache.
An AngularJS app with multiple React root components
An Easy Way to Create React Root Components
When managing multiple React roots, it is important to have an easy way to "Angularize" React components and wrap them in all the global providers. Creating a helper function hides away the complexity of shared state and services when defining a new React root.
The buildReactRoot
function is what we'll use to define our new React root components.
// buildReactRoot.js
import React from 'react';
import GlobalCacheProvider from '...';
import NotifierProvider from '...';
export default function buildReactRoot(
WrappedComponent,
propNames = []
) {
return react2Angular(
({ reactRoot, ...props }) => (
<GlobalCacheProvider cacheService={reactRoot.cacheService}>
<NotifierProvider notifier={reactRoot.notifier}>
<WrappedComponent {...props} />
</NotifierProvider>
</GlobalCacheProvider>
),
propNames,
['reactRoot']
);
}
Let's break this function down.
We're passing ['reactRoot']
as an argument to react2Angular
. This parameter specifies that the reactRoot
Angular Service should be passed as a prop to the component.
Since we're destructuring the the props, we pass everything else (other than reactRoot
) onto the WrappedComponent
. This allows us to still pass props from Angular code directly to the component.
Let's define the reactRoot
service that includes the shared global services.
// appModule.js
import userModule from './userModule';
import { createCache } from '...';
const cacheService = createCache();
angular
.module('app', [userModule.name])
.factory('reactRoot', (notifier) => {
return {
cacheService,
notifier,
};
});
We used userModule
above, but we haven't defined that yet. We'll build that out next to define a React component with the new React root setup.
// userModule.js
import React from 'react';
import { react2angular } from 'react2angular';
import buildReactRoot from './buildReactRoot';
// a component that uses the notifier and cache providers
function UserScreen() {
const notifier = useNotifier();
const cache = useCache();
return (
// ...
);
}
// defining the component is just a single line of code!
const userModule = angular.module('userModule', [])
.component('reactUserScreen', buildReactRoot(UserScreen)));
export default userModule;
Now when we use that component in an Angular UI Router state
definition, we treat it as a normal Angular component, and we don't have to pass any global services to it. reactRoot
does all of that for us behind the scenes.
$stateProvider.state({
name: 'user',
url: '/user',
template: `<react-user-screen></react-user-screen>`,
});
Passing Props from Angular
We can also pass props from Angular by listing them in the component definition.
// userModule.js
// ...
const userModule = angular.module('userModule', [])
.component(
'reactUserScreen',
buildReactRoot(UserScreen, ['currentUser']))
);
Then we can just pass them in like Angular component bindings.
$stateProvider.state({
name: 'user',
url: '/user',
controller: function (currentUser) {
this.currentUser = currentUser;
},
template: `
<react-user-screen
current-user="currentUser"
>
</react-user-screen>
`,
});
Shared State Between Root Components
An important thing to note about having multiple root components is that global state cannot be held inside React. It could be in the Angular code or just in a plain function. This is so it can be part of the reactRoot
service, which is passed to each React root component.
In the above example, the cache service was created in the appModule.js
file and added to reactRoot
.
// appModule.js
import userModule from './userModule';
import { createCache } from '...';
const cacheService = createCache();
angular
.module('app', [userModule.name])
.factory('reactRoot', (notifier) => {
return {
cacheService,
notifier,
};
});
Then in buildReactRoot
, we passed the cacheService
to the
GlobalCacheProvider
, which gives each React root access to the shared service.
export default function buildReactRoot(
WrappedComponent,
propNames = []
) {
return react2Angular(
({ reactRoot, ...props }) => (
<GlobalCacheProvider cacheService={reactRoot.cacheService}>
<NotifierProvider notifier={reactRoot.notifier}>
<WrappedComponent {...props} />
</NotifierProvider>
</GlobalCacheProvider>
),
propNames,
['reactRoot']
);
}
In Summary
- An incremental migration to React requires an easy way to wrap new React root components in global providers.
- A single AngularJS service of all global state and services helps facilitate defining new React root components.
- All global state needs to be held outside of React.
Posted on December 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.