Multiple React Roots in AngularJS

derekmt12

Derek N. Davis

Posted on December 24, 2020

Multiple React Roots in AngularJS

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 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']
  );
}
Enter fullscreen mode Exit fullscreen mode

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,
    };
  });
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>`,
});
Enter fullscreen mode Exit fullscreen mode

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']))
  );

Enter fullscreen mode Exit fullscreen mode

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>
  `,
});
Enter fullscreen mode Exit fullscreen mode

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,
    };
  });
Enter fullscreen mode Exit fullscreen mode

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']
  );
}
Enter fullscreen mode Exit fullscreen mode

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.
💖 💪 🙅 🚩
derekmt12
Derek N. Davis

Posted on December 24, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Migration to React - Introduction
react Migration to React - Introduction

December 24, 2020

Multiple React Roots in AngularJS
react Multiple React Roots in AngularJS

December 24, 2020