DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on

Creating a React component using Symfony UX

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 and I decided to start learning it. As I am a Symfony lover, I decided to start my learning trying to integrate a React component by using the symfony/ux-react component.
In this post, I will explain the steps I've followed to achieve it.

Install Webpack Encore

Webpack 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:

composer require symfony/webpack-encore-bundle
npm install
Enter fullscreen mode Exit fullscreen mode

This post assumes you are using symfony-flex

After installing webpack-encore-bundle 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:

import './styles/app.css';
Enter fullscreen mode Exit fullscreen mode

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:

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('app', './assets/app.js')
;
Enter fullscreen mode Exit fullscreen mode

Unless we need some special configuration, we can leave these values ​​as they are.

Installing The Stimulus Bundle

The stimulus-bundle 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:

composer require symfony/stimulus-bundle
npm install
Enter fullscreen mode Exit fullscreen mode

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.
import { startStimulusApp } from '@symfony/stimulus-bridge';
const app = startStimulusApp();
Enter fullscreen mode Exit fullscreen mode

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:

import './bootstrap.js';
import './styles/app.css';
Enter fullscreen mode Exit fullscreen mode
  • controllers.json: Contains the ux-components which must be activated within the application.
{
    "controllers": [],
    "entrypoints": []
}
Enter fullscreen mode Exit fullscreen mode

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:

Encore
   // ........
   .enableStimulusBridge('./assets/controllers.json')
   // ........
;
Enter fullscreen mode Exit fullscreen mode

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:

Encore
  // ..........
  //.enableTypeScriptLoader()
  // ..........
;
Enter fullscreen mode Exit fullscreen mode

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:

.addEntry('app', './assets/app.js')
Enter fullscreen mode Exit fullscreen mode

By this:

.addEntry('app', './assets/app.ts')
Enter fullscreen mode Exit fullscreen mode

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:

{
    "compileOnSave": true,
    "compilerOptions": {
      "sourceMap": true,
      "moduleResolution": "node",
      "lib": ["dom", "es2015", "es2016"],
      "jsx": "react-jsx",
      "target": "es6",
    },
    "include": ["assets/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

If you want to know more about the tsconfig configuration parameters, you can read the docs.

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:

composer require symfony/ux-react
npm install -D @babel/preset-react --force
Enter fullscreen mode Exit fullscreen mode

As this component also uses Symfony Flex, after being installed it will add the following line in the webpack.config.js.

Encore
   // .......
  .enableReactPreset()
   // .......
;
Enter fullscreen mode Exit fullscreen mode

The above line enables react in the webpack config.
This recipe also adds the following code to your app.ts file:

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

import './styles/app.css';
Enter fullscreen mode Exit fullscreen mode

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:

{
    "controllers": {
        "@symfony/ux-react": {
            "react": {
                "enabled": true,
                "fetch": "eager"
            }
        }
    },
    "entrypoints": []
}
Enter fullscreen mode Exit fullscreen mode

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.

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
            })
        });
    }

}
Enter fullscreen mode Exit fullscreen mode

The ApiService class uses the global function fetch from the Javascript API Fetch 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.

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>
    );
}

Enter fullscreen mode Exit fullscreen mode

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

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

interface FormData {
    amount: number;
}
Enter fullscreen mode Exit fullscreen mode
  • It imports the React useState and useEffect hooks
  • It imports the previously created ApiService class
  • It creates an interface to represent the form fields.
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]);
Enter fullscreen mode Exit fullscreen mode
  • 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.
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);
        } 
    ) 
}
Enter fullscreen mode Exit fullscreen mode
  • 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.
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>
);
Enter fullscreen mode Exit fullscreen mode

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.

{% 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 %}
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
phpcontrols profile image
PHPControls • Edited

Error during build

npm run build

[webpack-cli] ReferenceError: enableTypeScriptLoader is not defined

// webpack.config.js should be
.enableTypeScriptLoader(function (typeScriptConfigOptions) {
typeScriptConfigOptions.transpileOnly = true;
typeScriptConfigOptions.configFile = 'tsconfig.json';
});

Collapse
 
icolomina profile image
Nacho Colomina Torregrosa

Hey, Never got that error. Can you paste your entire webpack.config.json file ?

Collapse
 
phpcontrols profile image
PHPControls

It would be nice to have the source on Github

Thread Thread
 
icolomina profile image
Nacho Colomina Torregrosa

Hey, It seems all it's ok in your webpack.config.js.
You can check this repo: github.com/icolomina/soroban-crowd..., it is a full symfony application that uses Symfony UX and the React component.
Maybe by comparing your code with the repo code you can figure out what your error comes from.

Collapse
 
phpcontrols profile image
PHPControls

`const Encore = require('@symfony/webpack-encore');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
// only needed for CDN's or subdirectory deploy
//.setManifestKeyPrefix('build/')

/*
 * ENTRY CONFIG
 *
 * Each entry will result in one JavaScript file (e.g. app.js)
 * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
 */
.addEntry('app', './assets/app.ts')

// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()

.enableReactPreset()

// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
.enableStimulusBridge('./assets/controllers.json')

// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
.enableSingleRuntimeChunk()

/*
 * FEATURE CONFIG
 *
 * Enable & configure other features below. For a full
 * list of features, see:
 * https://symfony.com/doc/current/frontend.html#adding-more-features
 */
.cleanupOutputBeforeBuild()
.enableBuildNotifications()
.enableSourceMaps(!Encore.isProduction())
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning(Encore.isProduction())

// configure Babel
// .configureBabel((config) => {
//     config.plugins.push('@babel/a-babel-plugin');
// })

// enables and configure @babel/preset-env polyfills
.configureBabelPresetEnv((config) => {
    config.useBuiltIns = 'usage';
    config.corejs = '3.23';
})

// enables Sass/SCSS support
//.enableSassLoader()

// uncomment if you use TypeScript
.enableTypeScriptLoader(function (typeScriptConfigOptions) {
    typeScriptConfigOptions.transpileOnly = true;
    typeScriptConfigOptions.configFile = 'tsconfig.json';
});

// uncomment if you use React
//.enableReactPreset()

// uncomment to get integrity="..." attributes on your script & link tags
// requires WebpackEncoreBundle 1.4 or higher
//.enableIntegrityHashes(Encore.isProduction())

// uncomment if you're having problems with a jQuery plugin
//.autoProvidejQuery()
Enter fullscreen mode Exit fullscreen mode

;

module.exports = Encore.getWebpackConfig();
`