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
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';
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')
;
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
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();
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';
- controllers.json: Contains the ux-components which must be activated within the application.
{
"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:
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:
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:
.addEntry('app', './assets/app.js')
By this:
.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:
{
"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.
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
As this component also uses Symfony Flex, after being installed it will add the following line in the webpack.config.js.
Encore
// .......
.enableReactPreset()
// .......
;
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';
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": []
}
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
})
});
}
}
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>
);
}
Let's analyze the component step-by-step:
import { useState, useEffect } from 'react';
import { ApiService } from '../services/api';
interface FormData {
amount: number;
}
- 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]);
- 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);
}
)
}
- 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>
);
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 %}
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>
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)
Error during build
[webpack-cli] ReferenceError: enableTypeScriptLoader is not defined
// webpack.config.js should be
.enableTypeScriptLoader(function (typeScriptConfigOptions) {
typeScriptConfigOptions.transpileOnly = true;
typeScriptConfigOptions.configFile = 'tsconfig.json';
});
Hey, Never got that error. Can you paste your entire webpack.config.json file ?
It would be nice to have the source on Github
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.
`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/')
;
module.exports = Encore.getWebpackConfig();
`