Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 0 additions & 51 deletions .try.mjs
Original file line number Diff line number Diff line change
@@ -1,56 +1,5 @@
// When building your addon for older Ember versions you need to have the required files
const compatFiles = {
'ember-cli-build.js': `const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const { compatBuild } = require('@embroider/compat');
module.exports = async function (defaults) {
const { buildOnce } = await import('@embroider/vite');
let app = new EmberApp(defaults);
return compatBuild(app, buildOnce);
};`,
'config/optional-features.json': JSON.stringify({
'application-template-wrapper': false,
'default-async-observers': true,
'jquery-integration': false,
'template-only-glimmer-components': true,
'no-implicit-route-model': true,
}),
};

const compatDeps = {
'@embroider/compat': '^4.0.3',
'ember-cli': '^5.12.0',
'ember-auto-import': '^2.10.0',
'@ember/optional-features': '^2.2.0',
};

export default {
scenarios: [
{
name: 'ember-lts-5.8',
npm: {
devDependencies: {
'ember-source': '~5.8.0',
...compatDeps,
},
},
env: {
ENABLE_COMPAT_BUILD: true,
},
files: compatFiles,
},
{
name: 'ember-lts-5.12',
npm: {
devDependencies: {
'ember-source': '~5.12.0',
...compatDeps,
},
},
env: {
ENABLE_COMPAT_BUILD: true,
},
files: compatFiles,
},
{
name: `ember-lts-6.4`,
npm: {
Expand Down
69 changes: 54 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ React integration for Ember with reactive updating.

## Compatibility

- Ember.js v5.8 or above
- Ember.js v6.3 or above, not that earlier won't work. But this repo isn't testing prior to 6.3

## Installation

Expand All @@ -14,7 +14,59 @@ pnpm add @universal-ember/react

## Usage

Because Ember's tests are the same syntax and style as app code, usage examples in real app code match the tests. Some samples:
Import the react component into an ember component and render it.

```gjs
import { makeRenderable } from '@universal-ember/react';
import { HelloWorld } from './hello-world.tsx';

const Hello = makeRenderable(HelloWorld);

<template>
<Hello />

and with props!
<Hello @greeting="Hola" />

or with a props-bag
<Hello @props={{Object greeting="Hola"}} />
</template>
```

Note that react components should be defined using jsx or tsx. Using jsx/tsx syntax in js/ts is confusing[^and-incorrect] and should be avoided.

[^and-incorrect]: and incorrect -- the impact of JSX and TSX being supported in JS and TS files without the `x` extension has wreaked tons of havoc on the broader JavaScript ecosystem.

### Accessing the owner

```jsx
import { getOwner } from '@ember/owner';

function MyReactComponent(props) {
let owner = getOwner(props);
let store = owner.lookup('service:store');

return <>
... do something with the store ...
</>;
}

```

## Testing

testing with React components is a bit harder than with native ember components, because react testing doesn't have any sort of test-waiter system. The test-waiter system is something library-devs use to make testing easier for app developers, so that app develpers never need to worry about `waitUntil`-style timing.

That said, all `@ember/test-helpers` should still work with React subtrees.
Just the same, `testing-library` works well across both frameworks.

But, in React, it's very important to minimize the number of effects use, preferring data derivation, and only using effects as a last resort.


### Examples


These examples come from this library's own test suite.

```gjs
import { Greet, HelloWorld } from './hello-world.tsx';
Expand All @@ -40,19 +92,6 @@ module('makeRenderable', function (hooks) {
});
```

Note that react components should be defined using jsx or tsx. Using jsx/tsx syntax in js/ts is confusing[^and-incorrect] and should be avoided.

[^and-incorrect]: and incorrect -- the impact of JSX and TSX being supported in JS and TS files without the `x` extension has wreaked tons of havoc on the broader JavaScript ecosystem.

## Testing

testing with React components is a bit harder than with native ember components, because react testing doesn't have any sort of test-waiter system. The test-waiter system is something library-devs use to make testing easier for app developers, so that app develpers never need to worry about `waitUntil`-style timing.

That said, all `@ember/test-helpers` should still work with React subtrees.
Just the same, `testing-library` works well across both frameworks.

But, in React, it's very important to minimize the number of effects use, preferring data derivation, and only using effects as a last resort.


## Contributing

Expand Down
34 changes: 31 additions & 3 deletions src/render-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { waitForPromise } from '@ember/test-waiters';
import { setComponentTemplate } from '@ember/component';
import Modifier from 'ember-modifier';
import type { ComponentLike } from '@glint/template';
import React from 'react';
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import Component from '@glimmer/component';
import { macroCondition, isTesting } from '@embroider/macros';
import { setOwner, getOwner } from '@ember/owner';

interface Signature<Props> {
Args: {
Expand All @@ -22,7 +24,14 @@ export function makeRenderable<
>(ReactComponent: ReactComponentType): ComponentLike<Signature<Props>> {
class ReactRoot extends Component<Signature<Props>> {
get props() {
return this.args.props ? { ...this.args.props } : { ...this.args };
const owner = getOwner(this);
const props = this.args.props ? { ...this.args.props } : { ...this.args };

if (owner) {
setOwner(props, owner);
}

return props;
}
}
return setComponentTemplate(
Expand Down Expand Up @@ -53,8 +62,27 @@ class mount extends Modifier<{
) {
this.#root ||= createRoot(element);

this.#root.render(React.createElement(component as any, props));
const toRender = React.createElement(component as any, props);
/**
* Subsequent re-renders will diff and replace contents as needed.
*/
if (macroCondition(isTesting())) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(window as any).IS_REACT_ACT_ENVIRONMENT = true;
act(() => {
this.#root?.render(toRender);
});
} else {
this.#root.render(toRender);
}

/**
* For ember's test waiter system.
* We don't know how long a react component will take to render,
* but it often doesn't finish synchronously.
*
* Waiting until the next animation frame before test executions continues.
*/
waitForPromise(
(async () => {
await new Promise((resolve) => {
Expand Down
10 changes: 4 additions & 6 deletions vite.config.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { defineConfig } from 'vite';
import { extensions, ember, classicEmberSupport } from '@embroider/vite';
import { defineConfig, splitVendorChunkPlugin } from 'vite';
import { extensions, ember } from '@embroider/vite';
import { babel } from '@rollup/plugin-babel';

import react from '@vitejs/plugin-react';

// For scenario testing
const isCompat = Boolean(process.env.ENABLE_COMPAT_BUILD);

export default defineConfig({
plugins: [
...(isCompat ? [classicEmberSupport()] : []),
splitVendorChunkPlugin(),
ember(),
react(),
babel({
Expand All @@ -18,6 +15,7 @@ export default defineConfig({
}),
],
build: {
minify: false,
rollupOptions: {
input: {
tests: 'tests/index.html',
Expand Down