A set of tools facilitating the migration of Ember components to React components:
ember install react-migration-toolkit
// 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' }],
],
},
},
},
];
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.
// 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;
}
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>
);
}
✅ 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>
<ReactBridge
@reactComponent={{this.reactExample}}
@props={{hash text="this.props.text"}}
>
<SomeEmberComponent />
</ReactBridge>
<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"}}
/>
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.
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!');
});
});
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 {}
}
- Ember.js v4.12 or above
- Embroider or ember-auto-import v2
See the Contributing guide for details.
This project is licensed under the MIT License.