Hi! Here we gonna talk about the way we are doing project here at Novactive.
Meaning React + Typescript + Symfony Webpack Encore
React:
- Component Oriented Javascript Framework, it can be used for SPA but also for classical website mounting any components in any page
- We already use it at Novactive (Check Melijoe or Lambert or Kpeyes), you can check the latest video of Sébastien
Typescript:
- Strongly Typed Language that we transpile onto Javascript
- Modern JS Libraries used it (you may have found files with .d.ts)
- Fully compatible with Javascript
This project will be used as REX + as Tutorial to reproduce the same kind of approach as we've done with Lambert.
First of all you need to install symfony
On a new folder (symfony-ts by example)
composer create-project symfony/skeleton application
We're in 2020 everyone is using docker now, so we are creating are own PHP images.
You can view it inside ./infrastructure/Dockerfile
.
It contains xdebug to debug and blackfire for performance analysis.
make php-image-dev
Now everything should be done using docker, please see the makefile.
Start the project!
make start
Open it here, it should show you the symfony hello page ;).
We're gonna install webpack encore to orchestrate all our Typescript files
make composer F="require symfony/webpack-encore-bundle"
Then
make yarn F="install"
Nowadays, everybody is using Sass to manage their CSS, it is possible directly on Webpack-encore
make yarn F="add sass-loader@^8.0.0 node-sass --dev"
Then uncomment .enableSassLoader()
into the webpack.config.js
file
In order to use React along with webpack-encore we need to have the react presets from babel:
make yarn F="add @babel/preset-react@^7.0.0 --dev"
make yarn F="add react react-dom"
Then uncomment .enableReactPreset()
into the webpack.config.js
file
Before trying to use Typescript we're gonna do a simple React component.
You may already know twig so let's install it along with Annotation routing
make composer F="require symfony/twig-pack annotation"
Our Controller can look like that now
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class Index extends AbstractController
{
/**
* @Route("/", name="index", methods={"GET"})
*/
public function __invoke(): Response
{
return $this->render('base.html.twig');
}
}
Please reload the page, you should have an empty one!
Everything is almost already done!
You have two twig functions to invoke in your base.html.twig file to make it looks like:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome{% endblock %}</title>
{% block stylesheets %}
{% if pages is defined %}
{% for page in pages %}
{{ encore_entry_link_tags(page) }}
{% endfor %}
{% endif %}
{% endblock %}
</head>
<body>
{% block javascripts %}
{% if pages is defined %}
{% for page in pages %}
{{ encore_entry_script_tags(page) }}
{% endfor %}
{% endif %}
{% endblock %}
</body>
</html>
You can see that we added the calls to {{ encore_entry_link_tags(page) }}
and {{ encore_entry_script_tags(page) }}
.
Page is dynamic because we will setup multiple pages with only one template!
The name of page
should be an entry present in an addEntry
call made in webpack.config.js
In the app.js file we can now do a React component
First of all, rename it into app.jsx
the react extension and onto the webpack.config.js
rename the file also.
You will need to restart the front on each time you edit the webpack config.
make front-restart
Then on the base.html.twig
add this block
<div id="root"></div>
And of course on your controller, your actions looks like:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class Index extends AbstractController
{
/**
* @Route("/", name="index", methods={"GET"})
*/
public function __invoke(): Response
{
return $this->render('base.html.twig', ['pages' => ['app']]);
}
}
And your React component can looks like:
import '../css/app.css';
import React from 'react';
import ReactDom from 'react-dom';
const App = () => {
return <h1>Hi</h1>
}
ReactDom.render(<App/>, document.getElementById('root'));
Let's now transform the javascript onto typescript
make yarn F="add typescript ts-loader@^5.3.0 @babel/preset-typescript@^7.0.0 --dev"
You can now add this block onto our webpack.config.js
Encore
// OTHER THINGS...
.enableBabelTypeScriptPreset({
isTsx: true
})
;
We are using babel to transpile our javascript onto typescript because it is faster.
You can also rename the app.jsx
file into app.tsx
and rename it inside your webpack.config.json
You will also need this file next to your webpack.config.json, name it tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"moduleResolution": "node",
"declaration": false,
"noImplicitAny": false,
"jsx": "react",
"sourceMap": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true
},
"compileOnSave": false,
"exclude": [
"node_modules"
]
}
Since it removes types we will need an extra command to check all our types.
make yarn F="run tsc --noEmit"
We can now add types for react
make yarn F="add @types/react @types/react-dom --dev"
Now you're ready to use Typescript / React / Sass inside your Symfony Project!
Javascript and Typescript have their own way of checking syntaxic styles thanks to eslint and prettier They both have their advantages and we'll use both like here: https://medium.com/better-programming/eslint-vs-prettier-57882d0fec1d
We will also use one of the famous configuration of eslint which is the AirBnB one. It explains why each rules exists and how to enable/disable them.
First of all add all the needed dependencies:
make yarn F="add eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-config-airbnb-base eslint-import-resolver-webpack eslint-plugin-import eslint-plugin-prettier eslint-plugin-react prettier --dev"
Then an .eslintrc.js
file
module.exports = {
// parser: 'react-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
project: './tsconfig.json',
extraFileExtensions: ['.tsx'],
},
extends: [
'airbnb-base',
'plugin:import/typescript',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:prettier/recommended',
'prettier/@typescript-eslint',
'prettier/react',
],
settings: {
'import/resolver': ['node', 'webpack'],
},
rules: {
'import/prefer-default-export': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
'd.ts': 'never',
tsx: 'never',
},
],
},
overrides: [
{
files: ['*.ts', '*.tsx'],
rules: {
'no-useless-constructor': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
ts: 'never',
'd.ts': 'never',
tsx: 'never',
},
],
},
},
],
};
Then a .prettierrc
file :
{
"singleQuote": true,
"printWidth": 120,
"trailingComma": "es5"
}
Then you can add two targets inside your package.json
{
"scripts": {
"lint": "eslint assets/js/**/*.{js,ts,tsx,jsx}",
"lint-fix": "eslint assets/js/**/*.{js,ts,tsx,jsx} --fix"
}
}
Now you can run them both to check or to fix all rules.
make yarn F="run lint"
make yarn F="run lint-fix"
Thanks to Hugo Alliaume for the help to configure those tools.
We won't use the .enableEslintLoader
from webpack-encore because there is no documentation at all.
For form we'll use Formik which is now one of the standard in the react world. We'll use Yup for validations.
We've used it a lot inside Lambert project and we feel like it is not that hard to understand and helped us managing well our API calls + validation.
make yarn F="add formik yup"
We will do a simple form to submit an identity: firstName, lastName, title just to show you the principle.