Creating a React component using Symfony UX
Tags: react, symfony, ux, php
Published: 2024-07-05 15:26:26

## Introduction
I have been using Angular to build my front-ends for much time and I wanted to try another framework. I usually read a lot of posts and articles about [React](https://es.react.dev/) and I decided to start learning it. As I am a [Symfony](https://symfony.com/) lover, I decided to start my learning trying to integrate a React component by using the [symfony/ux-react](https://symfony.com/bundles/ux-react/current/index.html) component.
In this post, I will explain the steps I've followed to achieve it. 

## Install Webpack Encore
[Webpack](https://webpack.js.org/) is a bundler for JavaScript applications. It takes in multiple entry points and bundles them into optimized output files, along with their dependencies, for efficient delivery to the browser. 
Symfony provides a component to easily integrate webpack within your application. You can install it using composer and then npm to install the javascript required libraries:

```shell
composer require symfony/webpack-encore-bundle
npm install
```
> This post assumes you are using [symfony-flex](https://symfony.com/doc/current/quick_tour/flex_recipes.html)

After [installing webpack-encore-bundle](https://symfony.com/doc/current/frontend/encore/installation.html) you will see an **assets** directory under your project-root folder containing the following files:

- **app.js**: It is the file which will manage all your frontend dependencies. In this post case, react components. It will also import css files.
- **styles/app.css**: A file where you can put your CSS. You can use another css files. To be able to use them in your project, you should import them in the **app.js** file.

The **app.js** file contains the following content after created:

```javascript
import './styles/app.css';
```

Encore flex recipe also creates a file on your project-root folder named **webpack.config.js**. This file contains all the webpack configuration to bundle your assets.
At this point, the most important configurations are the following:

- **The Output Path**: Specifies the directory where compiled assets will be stored.
- **The Public Path**: Specifies the public path used by the web server to access the output path.
- **Entry**: Specifies the main entry file (app.js).

When the flex recipe creates the **webpack.config.js** file, it sets the previous values as follows:

```javascript
Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('app', './assets/app.js')
;
```
Unless we need some special configuration, we can leave these values ​​as they are.

## Installing The Stimulus Bundle
The [stimulus-bundle](https://symfony.com/bundles/StimulusBundle/current/index.html) is the component in charge of activating the other symfony ux-components you want to use in your application (in our case, symfony/ux-react). 

The stimulus-bundle must be installed using composer:

```shell
composer require symfony/stimulus-bundle
npm install
```

As the installation uses Symfony Flex, after it, we will see two new files under the **assets** directory:

- **bootstrap.js**: This file starts the stimulus app so that other symfony ux-components can be installed.

```javascript
import { startStimulusApp } from '@symfony/stimulus-bridge';
const app = startStimulusApp();
```
The above code snippet shows the **bootstrap.js** file contents. It simply starts the stimulus app. We must import this file in the **app.js** file:

```javascript
import './bootstrap.js';
import './styles/app.css';
```

- **controllers.json**: Contains the ux-components which must be activated within the application.

```json
{
    "controllers": [],
    "entrypoints": []
}
```
The above **controllers.json** is empty because we have not installed the ux-react component yet. After installing it, we will come back to this file to analyze its content.

This recipe also adds the following line in the **webpack.config.js** file:

```javascript
Encore
   // ........
   .enableStimulusBridge('./assets/controllers.json')
   // ........
;
```
This line enables the stimulus bridge specifying that the **controllers.json** file will contain all the ux-components to activate.

## Enabling Typescript
To enable typescript, we must follow the next steps:

### Enable typescript in webpack.config.js
You will find the following commented line in the **webpack.config.js** file:

```javascript
Encore
  // ..........
  //.enableTypeScriptLoader()
  // ..........
;
```
We have to uncomment the above line.

### Rename app.js to app.ts
As we are going to use typescript, we must rename the app.js to use the typescript extension.
Then, we have to return to the **webpack.config.js** file and change this line:

```javascript
.addEntry('app', './assets/app.js')
```

By this:

```javascript
.addEntry('app', './assets/app.ts')
```

### Create the tsconfig.json file
The **tsconfig.json** file is a configuration file used by the TypeScript compiler to determine how to compile TypeScript code into JavaScript. It contains various settings and options that control the behavior of the TypeScript compiler, such as the target JavaScript version, module resolution, and source maps. Let's see how this file looks like:

```json
{
    "compileOnSave": true,
    "compilerOptions": {
      "sourceMap": true,
      "moduleResolution": "node",
      "lib": ["dom", "es2015", "es2016"],
      "jsx": "react-jsx",
      "target": "es6",
    },
    "include": ["assets/**/*"]
}
```
> If you want to know more about the tsconfig configuration parameters, you can read the [docs](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html).

The two important parameters we have to pay attention to are the following:

- **jsx**: Specifies the JSX factory function to use
- **include**: Specifies that all the ts files under the assets folder will be compiled.

## Installing the UX-react component
Having encore and stimulus-bundle installed and typescript enabled, we are ready to install the symfony ux-react component. As always, we must use composer to install it:

```shell
composer require symfony/ux-react
npm install -D @babel/preset-react --force
```
As this component also uses Symfony Flex, after being installed it will add the following line in the **webpack.config.js**.

```javascript
Encore
   // .......
  .enableReactPreset()
   // .......
;
```
The above line enables react in the webpack config.
This recipe also adds the following code to your app.ts file:

```typescript
import './bootstrap.js';
import { registerReactControllerComponents } from '@symfony/ux-react';
registerReactControllerComponents(require.context('./react/controllers', true, /\.(j|t)sx?$/));

import './styles/app.css';
```

The two lines after the **bootstrap.js** import enable the automatic registration for all react components located into the **assets/react/controllers** folder. It supports both jsx and tsx (typescript) extensions.

If we look now in the controllers.json file, we will see the following content:

```json
{
    "controllers": {
        "@symfony/ux-react": {
            "react": {
                "enabled": true,
                "fetch": "eager"
            }
        }
    },
    "entrypoints": []
}
```

As you can see, the controllers key has a new entry. This entry enables react components and specifies an eager fetch. An eager fetch means that all the React components will be fetched upfront. If you would set the "lazy" value, the react components would be loaded only when they are required. For this article, an eager fetch can fit.

## Creating the React Component
Now it's time to create the React component. We are going to create a react component which will contains a form with an input named "amount" and a button to call a function which makes an api call.

### Creating the Api Service
Before showing the component's code, let's create a class which will contain the method to send the request call. This file must be located under the **assets/react/services** folder and must have the **.ts** extension.

```typescript
export class ApiService {

    sendDeposit(amount: number): Promise<any> {
        return fetch('/<your_call_url>', {
            method: "POST",
            mode: "same-origin",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                'amount' : amount
            })
        });
    }

}
```

The **ApiService** class uses the global function **fetch** from the [Javascript API Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to send the request to the server.

### Creating the component
This component must be located under the **assets/react/controllers** folder and have a **tsx** extension.

```typescript
import { useState, useEffect } from 'react';
import { ApiService } from '../services/api';

interface FormData {
    amount: number;
}


export default function DepositForm () {

    const [formData, setFormData] = useState<FormData>({ amount: 0 });
    const [depositSuccess, setDepositSuccess] = useState<boolean>(false);
    const apiService: ApiService = new ApiService();

    useEffect(() => {
        if (depositSuccess) {
          console.log('Form submitted successfully!');
        }
      }, [depositSuccess]);


    const handleChange = (event: any) => {
        const { name, value } = event.target;
        setFormData ( (previousFormData) => ({ ...previousFormData, [name]: value}) )
    }

    const handleForm = (event: any) => {
        apiService.sendDeposit(formData.amount).then(
            (r: any) => {
                if (!r.ok) {
                  throw new Error(`HTTP error! Status: ${r.status}`);
                }
                setDepositSuccess(true);
            } 
        ) 
    }


    return (
        <div>
            <div className="row mb-3">
                <div className="col-md-12">
                    <div className="form-floating mb-3 mb-md-0">
                        <input type="text" name="amount" id="amount" className="form-control" value={formData.amount} onChange={handleChange} />
                        <label htmlFor="amount" className="form-label">Amount</label>
                    </div>
                </div>
            </div>
            <div className="row mb-3">
                <div className="col-md-12">
                    <button type="button" className="btn btn-primary" onClick={handleForm}>Send deposit</button>
                </div>
            </div>
        </div>
    );
}

```

Let's analyze the component step-by-step:

```typescript
import { useState, useEffect } from 'react';
import { ApiService } from '../services/api';

interface FormData {
    amount: number;
}
```
- It imports the React [useState](https://react.dev/reference/react/useState) and [useEffect](https://react.dev/reference/react/useEffect) hooks
- It imports the previously created **ApiService** class
- It creates an interface to represent the form fields.

```typescript
const [formData, setFormData] = useState<FormData>({ amount: 0 });
const [depositSuccess, setDepositSuccess] = useState<boolean>(false);
const apiService: ApiService = new ApiService();

useEffect(() => {
   if (depositSuccess) {
      console.log('Form submitted successfully!');
   }
}, [depositSuccess]);
```
- It uses the useState hook to initialize the formData amount value and the depositSuccess value.
- It creates an ApiService instance.
- It uses the useEffect hook to show a console message when depositSuccess becomes true.

```typescript
const handleChange = (event: any) => {
    const { name, value } = event.target;
    setFormData ( (previousFormData) => ({ ...previousFormData, [name]: value}) )
}

const handleForm = (event: any) => {
    apiService.sendDeposit(formData.amount).then(
        (r: any) => {
           if (!r.ok) {
              throw new Error(`HTTP error! Status: ${r.status}`);
           }
           setDepositSuccess(true);
        } 
    ) 
}
```
- The **handleChange** function is used to update formData when the form amount value changes.
- The **handleForm** function sends the request using the **ApiService** sendDeposit function.

```typescript
return (
   <div>
      <div className="row mb-3">
          <div className="col-md-12">
              <div className="form-floating mb-3 mb-md-0">
                  <input type="text" name="amount" id="amount" className="form-control" value={formData.amount} onChange={handleChange} />
                  <label htmlFor="amount" className="form-label">Amount</label>
               </div>
          </div>
      </div>
      <div className="row mb-3">
          <div className="col-md-12">
               <button type="button" className="btn btn-primary" onClick={handleForm}>Send deposit</button>
          </div>
      </div>
  </div>
);
```
The component html contains an input and a button. The input value property holds the **formData.amount** value and the **onChange** event executes the **handleChange** function. As the **handleChange** function updates the form data with the new values, the amount field will be updated after every change.
The button executes the **handleForm** function after being clicked. 

## Calling the React component into Twig
Calling the React component into Twig is as easy as use the twig **react_component** function.

```twig
{% extends 'base.html.twig' %}

{% block title %}Make a deposit{% endblock %}

{% block body %}

<div class="container-fluid px-4">
  <h1 class="mt-4">Deposits</h1>
    <ol class="breadcrumb mb-4">
        <li class="breadcrumb-item active">Send deposit and start generating interests</li>
    </ol>
  
  <div class="row" >
    <div class="col-xl-6">
      <div class="card mb-4">
        <div class="card-header">
          <i class="fas fa-chart-area me-1"></i>
            Send deposit form
        </div>
        <div class="card-body">
          <div {{ react_component('DepositForm')}} </div>
        </div>
      </div>
    </div>
  </div>
</div>

{% endblock %}
```

**Important**: You have to include your webpack bundled assets in your base.html.twig file (or the corresponding file in your project) so that the stimulus application is initialized and the react components are loaded. This can be done within the html head tag.

```twig
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <title>{% block title %}Welcome!{% endblock %}</title>
    {% block stylesheets %}
        <!-- Your other stylesheets (if there are) -->
        {{ encore_entry_link_tags('app') }}
    {% endblock %}
    {% block javascripts %}
        <!-- Your other javascripts (if there are) -->
        {{ encore_entry_script_tags('app') }}
    {% endblock %}
</head>
```
The **encore_entry_link_tags** and the **encore_entry_script_tags** functions include both the bundled css and scripts.

## Conclusion
This article shows how to prepare your symfony project to support react and how to create a react component and use it within your project using the **react_component** TWIG function. 
Although backend and frontend applications are usually separated and communicate via APIs, this approach can be useful in situations when frontend and backend can co-live in the same project.

> If you enjoy my content and like the Symfony framework, consider reading my book: [Building an Operation-Oriented Api using PHP and the Symfony Framework: A step-by-step guide](https://amzn.eu/d/3eO1DDi)