ReactJS in a Plugin

The default frontend of a Festi project is server-side: plugin templates, HUI components, the DGS, and jimbo.js. Use it for lists, forms, and dashboards.

Use ReactJS only for a page with much state in the browser, for example an editor, a board with drag and drop, or a live monitor.

This page tells you:

  • what the framework supplies for ReactJS
  • how to add a React application to a plugin
  • how the application gets data
  • what the framework has for microfrontends

What the framework supplies

The Festi CLI has the command festi-react. Its create mode makes a starter React application. The application is a Vite project. Its build writes main.js and main.css directly into the Festi project.

Optionally, the create mode adds a URL, a method, and a template to a plugin.

The framework has no other React support. There is no React component library, and the HUI components are not React components.

Note: An earlier version of the CLI made the application with Create React App, and the mode deploy copied the build. The mode deploy is only for such an application. See the README of the Festi CLI.

Directories

The source of the application is outside the directories that the web server serves. Only the build is in the static directory of the plugin that owns the application.

<project root>/
|-- react/
|   `-- cart/                         the React source (package.json, vite.config.js, src/)
`-- plugins/
    `-- Shop/
        |-- ShopPlugin.php
        |-- templates/
        |   `-- cart.phtml            the template with the mount element
        `-- static/
            `-- cart-static/          the build: main.js, main.css
Directory Content In git On the production server
react/cart/ The source of the application. Yes, without node_modules. Not necessary.
plugins/Shop/static/cart-static/ The files that the browser loads. A decision of the project: commit them, or build them in the pipeline. Necessary.

Caution: The web server serves the static directory of a plugin. Do not put the source or node_modules there. The create mode shows a warning when the path of the application is in a served directory.

Add a React application to a plugin

1. Create the application

Run the command in the root directory of the project.

./vendor/bin/festi-react --mode create --name cart --plugin Shop --config ./config.php

The command does these things:

  1. It makes the application in ./react/cart/.
  2. It asks for the method name, the URL pattern, the area, and the type. It adds the method to the plugin Shop, and it copies the template to templates/cart.phtml.
  3. It sets the build directory of the application to plugins/Shop/static/cart-static/.
Option Use
--name The name of the application.
--path The directory of the application. Default: ./react/<name>.
--plugin The plugin that gets the page.
--target The directory for the build. Default with --plugin: <plugin>/static/<name>-static.
--config The config.php of the project. The command needs it to find the plugin. Default: ./config.php.

Without --plugin and --target, the command asks: "Do you want append project to plugin?" To make only the application, give the build directory with --target. You then add the method and the template (see step 3).

Caution: Each build removes all files in the build directory. Thus the command refuses a build directory that contains files. Give each application its own build directory.

The application contains:

File Content
package.json React, Redux, Vite, and the scripts dev, build, watch, preview.
vite.config.js The build directory, as a path relative to this file, and the file names main.js and main.css.
src/main.jsx Renders the component App into the element with the id root.
src/components/App.jsx The first component.
src/store.js, src/reducers/ The Redux store.
src/rpc/request.js A JSON-RPC client. See Data for the application.

No file of the application contains a path of your computer. Thus you can commit the application, and a different developer or a pipeline can build it.

2. Build

cd react/cart
npm install
npm run build

npm run build writes main.js and main.css to the build directory. No copy step is necessary.

During development, npm run watch builds again after each change in src.

3. Mount the application in a plugin page

The plugin method gives the URL of the build to a template. The create mode with --plugin adds this method and this template for you.

/**
 * @urlRule ~^/shop/cart/$~
 * @area backend
 * @userType user
 */
public function onDisplayCart(Response &$response): bool
{
    $this->assign('path', $this->getStaticUrl('cart-static'));

    $response->content = $this->ui->widget()
        ->setTitle(__('Cart'))
        ->setContent($this->fetch('cart.phtml'))
        ->fetch();

    return true;
}

templates/cart.phtml:

<?php
/** @var string $path */
?>

<link href="<?php echo $path; ?>/main.css" rel="stylesheet">

<div id="root"></div>

<script type="module" src="<?php echo $path; ?>/main.js"></script>

The build is an ES module. Thus the <script> tag needs type="module".

The page is a normal page of the project. The theme renders the layout, the menu, and the permission checks work. React controls only the content of the mount element.

4. Browser cache

The file names of the build have no hash (main.js). Thus the browser can use an old file after a release. Add a version parameter to the two URLs in the template, and change it with each release. Use the same value as the theme: the setting js_version.

Data for the application

Initial data from the server

Give the first data to the application in the template. The application then needs no request at the start.

<div id="root"
     data-id-customer="<?php echo (int) $idCustomer; ?>"
     data-url="<?php echo $this->getUrl('/shop/cart/items/'); ?>"></div>
const root = document.getElementById('root');
const itemsUrl = root.dataset.url;

Requests to the server

There are two methods.

onJson methods. The application calls a plugin method with fetch(). The method returns JSON through the Response object. See Response.

public function onJsonCartItems(Response &$response): bool
{
    $response->items = $this->object->getCartItems($this->core->user->getID());

    return true;
}
const response = await fetch(itemsUrl, {credentials: 'same-origin'});
const data = await response.json();

setItems(data.items);

The reply has the key type only when the response has an action. If the method throws an exception, the reply is an alert: {"messages": [...], "type": "alert"}. Examine data.type in the application, or give the reply to Jimbo.response(data) to show the message in the usual manner. Jimbo is available because the theme loads jimbo.js on the page.

JSON-RPC. The starter application contains src/rpc/request.js, a client for the RPC plugin. The client reads the address of the API and the token of the user from the attributes data-endpoint and data-access-token of the mount element. The method that the create mode adds to the plugin assigns the two values to the template as empty strings. Set them in that method.

Rules for a React application in a Festi project

  • One plugin owns one React application. Do not share one application between plugins.
  • Keep the server as the owner of the business logic and of the permissions. The application only shows data and sends actions.
  • Do not load jQuery, Bootstrap, or a second CSS framework in the application. The theme supplies the look.
  • Use the CSS classes of the theme for standard elements, so that the page looks the same as the other pages.
  • Use __() on the server for the texts, and give the translated texts to the application as data.
  • Do not replace DGS lists and forms with React. Use the DGS.

Microfrontends

The framework has no microfrontend runtime. There is no shell application, no module loader, and no shared React instance between plugins.

What you can do with the current framework:

Need Method
Each team develops and builds its frontend independently. Each plugin has its own React application in its static directory, with its own package.json.
A page contains more than one application. Give each application its own mount element with a different id, and load each bundle. Each bundle contains its own copy of React.
Applications send messages to each other. Use the event bus of jimbo.js: Jimbo.fireEvent() and Jimbo.addListener(). See Frontend and backend events.
Old pages and new pages work together. A plugin page with a React application is a normal page. The theme, the menu, and the permissions are the same.

Limits:

  • Two applications cannot share components or state at run time.
  • Each bundle makes the page larger, because each contains React.
  • The starter application uses the id root. Change it when a page has two applications.

What was tested

The procedure on this page was not run after the Festi CLI changed the starter application to Vite. The statements about the create mode and about the starter application come from the source of the Festi CLI (src/react/mode/CreateMode.php and src/react/resources/create/) and from its README. The examples in Data for the application were not run.