Skip to content

qonto/react-migration-toolkit

Repository files navigation

ember-autofocus-modifier-illustration

react-migration-toolkit

A set of tools facilitating the migration of Ember components to React components:

Installation

ember install react-migration-toolkit

Configuration

⚠️ When using Webpack, your Ember apps need to compile jsx:

// webpack.config.js
rules: [
  // ...
  {
    test: /\.jsx/, // replace or add tsx if you use typescript
    use: {
      loader: 'babel-loader',
      options: {
        presets: [
          // Add other presets here if you need Typescript support for example
          ['@babel/preset-react', { runtime: 'automatic' }],
        ],
      },
    },
  },
];

Usage

The main component brought by this addon is the ReactBridge Ember component.

It renders React components within Ember templates, permitting progressive UI migration and preserving existing logics and tests.

The native Bridge can be used as is for simple components. Multiple bridges can be injected within a same template.

Basic Example

To inject a React component in Ember:

// app/react/components/example.tsx

export function Example({ userName }: ExampleProps) {
  return <h1>Hello {userName}!</h1>;
}
// app/components/my-ember-component.js

import Component from '@glimmer/component';
import { Example } from 'app/react/components/example.tsx';

export default class MyComponent extends Component {
  reactExample = Example;
}
{{! app/components/my-ember-component.hbs }}

<ReactBridge
  @reactComponent={{this.reactExample}}
  @props={{hash userName='John'}}
/>

Content projection -

The React Bridge accepts yielded values, which can be accessed via the children prop on the React side.

<ReactBridge
  @reactComponent="{{this.reactExample}}"
  @props={{hash text="this.props.text"}}
>
  <p>Hello World!</p>
</ReactBridge>
function ReactExample({ children, text }: ReactExampleProps) {
  return (
    <div>
      <h1>{text}</h1>
      {children} // <p>Hello World!</p>
    </div>
  );
}

⚠️ Using yielded values does come with some risks. What is supported so far:

✅ Passing Ember helpers works

<ReactBridge
  @reactComponent="{{this.reactExample}}"
  @props={{hash text="this.props.text"}}
>
  {{format/iban @iban}}
</ReactBridge>

❌ Directly nesting conditions cause an error:

Failed to execute 'removeChild' on 'Node'
<ReactBridge
  @reactComponent="{{this.reactExample}}"
  @props={{hash text="this.props.text"}}
>
  {{#if this.someCondition}}
    {{t "some-text"}} 
  {{else}} 
    {{t "some-other-text"}}
  {{/if}}
</ReactBridge>

✅ Wrap conditions in an html element

<ReactBridge
  @reactComponent={{this.reactExample}}
  @props={{hash text="this.props.text"}}
>
  <div>
    {{#if this.someCondition}}
      {{t "some-text"}} 
    {{else}} 
      {{t "some-other-text"}}
    {{/if}}
  </div>
</ReactBridge>

⚠️ Be careful with nesting Ember components in a ReactBridge. It's hard to debug when you have issues.

<ReactBridge
  @reactComponent={{this.reactExample}}
  @props={{hash text="this.props.text"}}
>
  <SomeEmberComponent />
</ReactBridge>

⚠️ Similarly, we recommend not nesting bridges.

<ReactBridge
  @reactComponent={{this.reactExample}}
  @props={{hash text="this.props.text"}}
>
  <ReactBridge
    @reactComponent={{this.someReactComponent}}
  />
</ReactBridge>

✅ Consider migrating both components at the same time, returning just a React component.

<ReactBridge
  @reactComponent={{this.reactExampleWithEmberComponent}}
  @props={{hash text="this.props.text"}}
/>

Sharing context between Ember and React

It is also flexible enough to allow custom providers when shared context is needed between Ember and React. In that situation, creating an adapter around the native Bridge is required.

It is as simple as passing providerOptions as an argument to the ReactBridge

An example is provided in the test app:

💡 In practice, you probably want to create a wrapper that hooks up your most common providers. For example, intl, common services, and routing.

How to test React components in Ember

Like any other Ember component, the ReactBridge can be rendered in QUnit and asserted on. This method is ideal to test React components in isolation.

import { WelcomeMessage } from 'qonto/react/component/welcome-message';

module('my component test', function () {
  test('It renders properly', async function (assert) {
    this.setProperties({ userName: 'Jane Doe' });
    this.reactWelcomeMessage = WelcomeMessage;

    await render(
      hbs`<ReactBridge
        @reactComponent={{this.reactWelcomeMessage}}
        @props={{hash
          cardLevel=this.userName
        }}
      />`,
    );

    assert.dom('h1').hasText('Welcome Jane Doe!');
  });
});

If you're using custom providers, they require the same setup as your Ember components:

import { WelcomeMessage } from 'qonto/react/component/welcome-message';
import { setupIntl } from 'ember-intl';
import { intlProvider } from 'qonto/react/providers/intl';

module('my component test', function (hooks) {
  setupIntl(hooks);
  test('It renders properly', async function (assert) {
    this.setProperties({ userName: 'Jane Doe' });
    this.reactWelcomeMessage = WelcomeMessage;

    await render(
      hbs`<ReactBridge
        @reactComponent={{this.reactWelcomeMessage}}
        @props={{hash
          cardLevel=this.userName
        }}
        customProviders={{this.intlProvider}}
      />`,
    );

    assert.dom('h1').hasText('Welcome Jane Doe!');
  });
});

Typescript Usage

The react-bridge has proper Glint types, which allow you when using TypeScript to get strict type checking in your templates.

Unless you are using strict mode templates (via first class component templates), you need to import the addon's Glint template registry and extend your app's registry declaration as described in the Using Addons documentation:

import '@glint/environment-ember-loose';

import type ReactMigrationToolkitRegistry from '@qonto/react-migration-toolkit/template-registry';

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry 
    extends ReactMigrationToolkitRegistry {}
}

Compatibility

  • Ember.js v4.12 or above
  • Embroider or ember-auto-import v2

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.