diff --git a/src/React/DependencyInjection/ReactExtension.php b/src/React/DependencyInjection/ReactExtension.php index efcdd5db3b2..99ee33f53a8 100644 --- a/src/React/DependencyInjection/ReactExtension.php +++ b/src/React/DependencyInjection/ReactExtension.php @@ -18,7 +18,7 @@ use Symfony\UX\React\Twig\ReactComponentExtension; /** - * @author Titouan Galopin + * @author Titouan Galopin * * @internal */ diff --git a/src/React/README.md b/src/React/README.md index 9cb24d3fc29..8e7fb7ba204 100644 --- a/src/React/README.md +++ b/src/React/README.md @@ -1,58 +1,11 @@ # Symfony UX React -Symfony UX React integrates [React](https://reactjs.org/) into Symfony applications. It provides tools -to render React components from Twig. +Symfony UX React integrates [React](https://reactjs.org/) into Symfony applications. +It provides tools to render React components from Twig. **This repository is a READ-ONLY sub-tree split**. See https://github.com/symfony/ux to create issues or submit pull requests. -## Usage - -```bash -composer require symfony/ux-react - -# Don't forget to install the JavaScript dependencies as well and compile -$ yarn install --force -$ yarn encore dev -``` - -Then in your `assets/app.js` file, add the following lines: - -``` -// assets/app.js -import { registerReactControllerComponents } from '@symfony/ux-react'; - -// Registers React controller components to allow loading them from Twig -// -// React controller components are components that are meant to be rendered -// from Twig. These component then rely on other components that won't be called -// directly from Twig. -// -// By putting only controller components in `react/controllers`, you ensure that -// internal components won't be automatically included in your JS built file if -// they are not necessary. -registerReactControllerComponents(require.context('./react/controllers', true, /\.(j|t)sx?$/)); -``` - -You can now create your first React controller component in `assets/react/controllers`: - -```js -// assets/react/controllers/MyComponent.jsx -import React from 'react'; - -export default function (props) { - return
Hello {props.fullName}
; -} -``` - -And use it in your Twig files: - -```twig -{# templates/home.html.twig #} - -
-``` - ## Resources - [Documentation](https://symfony.com/bundles/ux-react/current/index.html) diff --git a/src/React/Resources/assets/dist/render_controller.js b/src/React/Resources/assets/dist/render_controller.js index 555380ef462..d6e5ba59e60 100644 --- a/src/React/Resources/assets/dist/render_controller.js +++ b/src/React/Resources/assets/dist/render_controller.js @@ -4,14 +4,13 @@ import { Controller } from '@hotwired/stimulus'; class default_1 extends Controller { connect() { this._dispatchEvent('react:connect', { component: this.componentValue, props: this.propsValue }); - if (this.element.timeout) { - clearTimeout(this.element.timeout); - } - this.element.timeout = setTimeout(() => { - const component = window.resolveReactComponent(this.componentValue); - this._renderReactElement(React.createElement(component, this.propsValue, null)); - this._dispatchEvent('react:mount', { component: this.componentValue, props: this.propsValue }); - }, 50); + const component = window.resolveReactComponent(this.componentValue); + this._renderReactElement(React.createElement(component, this.propsValue, null)); + this._dispatchEvent('react:mount', { + componentName: this.componentValue, + component: component, + props: this.propsValue, + }); } disconnect() { this.element.unmount(); @@ -33,7 +32,7 @@ class default_1 extends Controller { }; } _dispatchEvent(name, payload) { - this.element.dispatchEvent(new CustomEvent(name, { detail: payload })); + this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true })); } } default_1.values = { diff --git a/src/React/Resources/assets/package.json b/src/React/Resources/assets/package.json index 191dec6e2a7..4b0b2ba510f 100644 --- a/src/React/Resources/assets/package.json +++ b/src/React/Resources/assets/package.json @@ -16,16 +16,15 @@ }, "peerDependencies": { "@hotwired/stimulus": "^3.0.0", - "react": "^16.0 || ^17.0 || ^18.0", - "react-dom": "^16.0 || ^17.0 || ^18.0" - }, - "dependencies": { - "@types/webpack-env": "^1.16" + "react": "^18.0", + "react-dom": "^18.0" }, "devDependencies": { "@hotwired/stimulus": "^3.0.0", - "@types/react": "^16.0 || ^17.0 || ^18.0", - "react": "^16.0 || ^17.0 || ^18.0", - "react-dom": "^16.0 || ^17.0 || ^18.0" + "@types/react": "^18.0", + "@types/react-dom": "^18.0", + "@types/webpack-env": "^1.16", + "react": "^18.0", + "react-dom": "^18.0" } } diff --git a/src/React/Resources/assets/src/register_controller.ts b/src/React/Resources/assets/src/register_controller.ts index e04330042ac..6d278e74dc0 100644 --- a/src/React/Resources/assets/src/register_controller.ts +++ b/src/React/Resources/assets/src/register_controller.ts @@ -20,7 +20,7 @@ export function registerReactControllerComponents(context: __WebpackModuleApi.Re // Expose a global React loader to allow rendering from the Stimulus controller (window as any).resolveReactComponent = (name: string): object => { - const component = reactControllers['./' + name + '.jsx'] || reactControllers['./' + name + '.tsx']; + const component = reactControllers[`./${name}.jsx`] || reactControllers[`./${name}.tsx`]; if (typeof component === 'undefined') { throw new Error('React controller "' + name + '" does not exist'); } diff --git a/src/React/Resources/assets/src/render_controller.ts b/src/React/Resources/assets/src/render_controller.ts index 6443a93ad76..d714d461394 100644 --- a/src/React/Resources/assets/src/render_controller.ts +++ b/src/React/Resources/assets/src/render_controller.ts @@ -10,6 +10,7 @@ 'use strict'; import React, { ReactElement } from 'react'; +import { createRoot } from 'react-dom/client'; import { Controller } from '@hotwired/stimulus'; export default class extends Controller { @@ -24,52 +25,26 @@ export default class extends Controller { connect() { this._dispatchEvent('react:connect', { component: this.componentValue, props: this.propsValue }); - // Use a timeout to avoid mounting and demounting right after - if ((this.element as any).timeout) { - clearTimeout((this.element as any).timeout); - } + const component = window.resolveReactComponent(this.componentValue); + this._renderReactElement(React.createElement(component, this.propsValue, null)); - (this.element as any).timeout = setTimeout(() => { - const component = window.resolveReactComponent(this.componentValue); - this._renderReactElement(React.createElement(component, this.propsValue, null)); - - this._dispatchEvent('react:mount', { - componentName: this.componentValue, - component: component, - props: this.propsValue, - }); - }, 50); + this._dispatchEvent('react:mount', { + componentName: this.componentValue, + component: component, + props: this.propsValue, + }); } disconnect() { - (this.element as any).unmount(); + (this.element as any).root.unmount(); this._dispatchEvent('react:unmount', { component: this.componentValue, props: this.propsValue }); } _renderReactElement(reactElement: ReactElement) { - if (parseInt(React.version) >= 18) { - // React 18+ rendering - // eslint-disable-next-line @typescript-eslint/no-var-requires - const root = require('react-dom/client').createRoot(this.element); - root.render(reactElement); - - // Register a way to unmount this element - (this.element as any).unmount = () => { - root.unmount(); - }; - - return; - } - - // Up to React 17 rendering - // eslint-disable-next-line @typescript-eslint/no-var-requires - const reactDom = require('react-dom'); - reactDom.render(reactElement, this.element); + const root = createRoot(this.element); + root.render(reactElement); - // Register a way to unmount this element - (this.element as any).unmount = () => { - reactDom.unmountComponentAtNode(this.element); - }; + (this.element as any).root = root; } _dispatchEvent(name: string, payload: any) { diff --git a/src/React/Resources/doc/index.rst b/src/React/Resources/doc/index.rst index 22599f3155f..9771a34ec00 100644 --- a/src/React/Resources/doc/index.rst +++ b/src/React/Resources/doc/index.rst @@ -1,4 +1,90 @@ Symfony UX React ================ -TODO +Symfony UX React is a Symfony bundle integrating `React`_ in +Symfony applications. It is part of `the Symfony UX initiative`_. + +React is a JavaScript library for building user interfaces. +Symfony UX React provides tools to render React components from Twig, +handling rendering and data transfers. + +Symfony UX React supports React 18+. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then install the bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-react + + # Don't forget to install the JavaScript dependencies as well and compile + $ yarn install --force + $ yarn encore dev + +You also need to add the following lines at the end to your ``assets/app.js`` file: + +.. code-block:: javascript + + // assets/app.js + import { registerReactControllerComponents } from '@symfony/ux-react'; + + // Registers React controller components to allow loading them from Twig + // + // React controller components are components that are meant to be rendered + // from Twig. These component then rely on other components that won't be called + // directly from Twig. + // + // By putting only controller components in `react/controllers`, you ensure that + // internal components won't be automatically included in your JS built file if + // they are not necessary. + registerReactControllerComponents(require.context('./react/controllers', true, /\.(j|t)sx?$/)); + + +Usage +----- + +UX React works by using a system of **React controller components**: React components that +are registered using ``registerReactControllerComponents`` and that are meant to be rendered +from Twig. + +When using the ``registerReactControllerComponents`` configuration shown previously, all +React components located in the directory ``assets/react/controllers`` are registered as +React controller components. + +You can then render any React controller component in Twig using the ``react_component``. +For example: + +.. code-block:: javascript + + // assets/react/controllers/MyComponent.jsx + import React from 'react'; + + export default function (props) { + return
Hello {props.fullName}
; + } + + +.. code-block:: twig + + {# templates/home.html.twig #} + +
+ +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: +https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, +meaning it is not bound to Symfony's BC policy for the moment. + +.. _`React`: https://reactjs.org/ +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html