Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

React Components

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

It works like this: we create React element objects and ask React to render them. But, React has another important object: a Component. Ooooooooo.

It looks like this: create a class called RepLogApp and extend something called React.Component. A component only needs to have one method render(). Inside, return whatever element objects you want. In this case I'm going to copy my JSX from above and, here, say return and paste.

... lines 1 - 3
class RepLogApp extends React.Component {
render() {
return <h2>Lift Stuff! <span>❤️</span></h2>;
}
}
... lines 9 - 12

This is a React component. Below, I'm going to use console.log() and then treat my RepLogApp as if it were an element: <RepLogApp />.

... lines 1 - 9
console.log(<RepLogApp />);
... lines 11 - 12

Finally, below, instead of rendering an element, we can render the component with that same JSX syntax: <RepLogApp />.

... lines 1 - 10
ReactDom.render(<RepLogApp />, document.getElementById('lift-stuff-app'));

Ok, go back and refresh! Awesome! We get the exact same thing as before! And, check out the console! The component becomes a React element!

The What & Why of Components

This React component concept is something we're going to use a lot. But, I don't want to make it seem too important: because it's a super simple concept. In PHP, we use classes as a way to group code together and give that code a name that makes sense. If we organize 50 lines of code into a class called "Mailer", it becomes pretty obvious what that code does... it spams people! I mean, it emails valued customers!

React components allow you to do the same thing for the UI: group elements together and give them a name. In this case, we've organized our h2 and span React elements into a React component called RepLogApp. React components are sort of a named container for elements.

By the way, React components do have one rule: their names must start with a capital letter. Actually, this rule is there to help JSX: if we tried using a <repLogApp /> component with a lowercase "r", JSX would actually think we wanted to create some new hipster repLogApp HTML element, just like how a <div> becomes a <div>. By starting the component name with a capital letter, JSX realizes we're referring to our component class, not some hipster HTML element with that name.

Making our Imports more Hipster

Anyways, a few minor housekeeping things. Notice that Component is a property on the React object. The way we have things now is fine. But, commonly, you'll see React imported like this: import React then { Component } from react. Thanks to this, you can just extend Component.

import React, { Component } from 'react';
... lines 2 - 3
class RepLogApp extends Component {
... lines 5 - 7
}
... lines 9 - 12

This is pretty much just a style thing. And... honestly... it's one of the things that can make React frustrating. What I mean is, React developers like to use a lot of the newer, fancier ES6 syntaxes. In this case, the react module exports an object that has a Component property. This syntax is "object destructuring": it grabs the Component key from the object and assigns it to a new Component variable. Really, this syntax is not that advanced, and actually, we're going to use it a lot. But, this is one of the challenges with React: you may not be confused by React, you may be confused by a fancy syntax used in a React app. And we definitely don't want that!

We can do the same thing with react-dom. Because, notice, we're only using the render key. So instead of importing all of react-dom, import { render } from react-dom. Below, use the render() function directly.

... line 1
import { render } from 'react-dom';
... lines 3 - 10
render(<RepLogApp />, document.getElementById('lift-stuff-app'));

This change is a little bit more important because Webpack should be smart enough to perform something called "tree shaking". That's not because Webpack hates nature, that's just a fancy way of saying that Webpack will realize that we only need the render() function from react-dom: not the whole module. And so, it will only include the code needed for render in our final JavaScript file.

Anyways, these are just fancier ways to import exactly what we already had.

Oh, but, notice: it looks like the React variable is now an unused import. What I mean is, we don't ever use that variable. So, couldn't we just remove it and only import Components?

Actually, no! Remember: the JSX code is transformed into React.createElement(). So, strangely enough, we are still using the React variable, even though it doesn't look like it. Sneaky React.

To make sure we haven't broken anything... yet, go back and refresh. All good.

One Component Per File

Just like in PHP, we're going to follow a pattern where each React component class lives in its own file. In the assets/js directory, create a new RepLog directory: this will hold all of the code for our React app. Inside, create a new file called RepLogApp. Copy our entire component class into that file.

import React, { Component } from 'react';
class RepLogApp extends Component {
render() {
return <h2>Lift Stuff! <span>❤️</span></h2>;
}
}

Woh. Something weird just happened. Did you see it? We only copied the RepLogApp class. But when we pasted, PhpStorm auto-magically added the import for us! Thanks PhpStorm! Gold star!

But, check out this error:

ESLint: React must be in scope when using JSX.

Oh, that's what we just talked about! This is one of those warnings that comes from ESLint. Update the import to also import React.

Now, to make this class available to other files, use export default class RepLogApp.

import React, { Component } from 'react';
... line 2
export default class RepLogApp extends Component {
... lines 4 - 6
}

Back in rep_log_react.js, delete the class and, instead, import RepLogApp from ./RepLog/RepLogApp. Oh, and it's not too important, but we're actually not using the Component import anymore. So, trash it.

import React from 'react';
... line 2
import RepLogApp from './RepLog/RepLogApp';
... lines 4 - 7

Awesome! Our code is a bit more organized! And when we refresh, it's not broken, which is always my favorite.

The Entry - Component Structure

And actually, this is an important moment because we've just established a basic structure for pretty much any React app. First, we have the entry file - rep_log_react.js - and it has just one job: render our top level React component. In this case, it renders RepLogApp. That's the only file that it needs to render because eventually, the RepLogApp component will contain our entire app.

So the structure is: the one entry file renders the one top-level React component, and it returns all the elements we need from its render() method.

And, that's our next job: to build out the rest of the app in RepLogApp. But first, we need to talk about a super-duper important concept called props.

Leave a comment!

14
Login or Register to join the conversation
Dominik Avatar

Hello Guys, im trying to execute the integration between Symfony 6 and React 18 but once I ran the npm run watch the encore compile the JS files twice and after the 3 modification it keep compiling without error BUT it does not update the files int the public folder anymore. it a fresh new project. is someone facing this issue? I already tryed react 17 and 18 the result is the same.

// webpack.config.js
const Encore = require('@symfony/webpack-encore');

if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('main', './assets/main.js')
    .splitEntryChunks()
    .enableSingleRuntimeChunk()
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    .enableVersioning(Encore.isProduction())
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = '3.23';
    })
    //.enableSassLoader()
    //.enableTypeScriptLoader()
    .enableReactPreset()
;

module.exports = Encore.getWebpackConfig();
// assets/main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './styles/app.css'


ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <App/>
    </React.StrictMode>
)
// assets/app.jsx
import React, {useState} from 'react'
const App = () =>{

    const [count,setCount] = useState(0);
    return (
        <div>
            <div>Hello World! count: {count}</div>
            <button
                onClick={() => setCount(count +1)}
            >
                CLICK
            </button>s
        </div>
    )
}
export default App;

and my twig file

<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
        {% block stylesheets %}
            {{ encore_entry_link_tags('main') }}
        {% endblock %}

        {% block javascripts %}
            {{ encore_entry_script_tags('main') }}
        {% endblock %}
    </head>
    <body>
        <div id="root"></div>
        {% block body %}{% endblock %}
    </body>
</html>
Reply

Hey @Dominik!

This is super weird behavior. It's even STRANGER because I've never heard of an issue like this... until 3 days ago when someone else had something that sounds VERY similar. I'm not sure if this will be relevant to you, but I hope it will be - https://github.com/symfony/webpack-encore/issues/1209#issuecomment-1560650833

Cheers!

Reply
Brice N. Avatar
Brice N. Avatar Brice N. | posted 3 years ago | edited

Hello ! I followed the tutorial but it looks like I have a javascript error in my browser's console


Uncaught TypeError: Cannot read property 'render' of undefined
    at Object../assets/js/react/form/registration.tsx (registration.tsx:14)
    at __webpack_require__ (bootstrap:79)
    at checkDeferredModules (bootstrap:45)
    at Array.webpackJsonpCallback [as push] (bootstrap:32)
    at registration_form.js:1

my code is


import React = require('react');
import ReactDOM from 'react-dom';

class RegistrationForm extends React.Component {
    render() {
        return (
            <div>
                <p>test</p>
            </div>
        );
    }
}

ReactDOM.render(<RegistrationForm/>, document.getElementById('registration-form'));

I may have done something wrong but I'm not sure of what, I've reviewed the actions made in the tutorials and I don't think to have missed anything

Reply

Hey Infrandomness,

Hm, could you follow the next steps:
$ rm -rf node_modules/
$ yarn install
$ yarn run encore dev --watch

I.e. just basically re-install all the Node dependencies and re-build the assets with Encore and try again. Do you see the same error? Btw, did you download the course code and bootstrap the project from start/ folder? Or do you apply changes on your own project?

Cheers!

Reply
Brice N. Avatar

HI ! Thanks for your answers !

First of all, I successfully fixed my project.
As I wrote my components in typescript, I forgot to install the @types packages of both react and react-dom

To answer your questions :
I apply the changes to my own project as I watch the tutorials

Reply

Hey Random,

I'm happy you fixed it, well done! And thanks for sharing your solution.

Cheers!

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | posted 4 years ago

Hi guys,

If you have separate pages on your app, would you have to create separate React apps? For example PostListApp, PostShowApp?

Reply

Hey Shaun T.!

Great question! Yes. Well, probably yes. In your example, these indeed sound like 2 totally separate "apps". So yes, I would set it up like you are (the cool thing is if there is some shared logic, you can extract that into components that each app uses). If you're building a traditional app, each page would have a different webpack "entry" that would initialize the correct "app". If you're building a single-page app, you'll likely use React router (a topic we didn't have time for). React router is a pretty simple idea: based on the URL, it "renders" different components. For example, you might configure /posts to render PostListApp and /posts/5 to render PostShowApp.

Cheers!

Reply
Jovan P. Avatar
Jovan P. Avatar Jovan P. | posted 5 years ago

If I vaguely recall, this was mentioned in another Encore course, **but**, once we externalize the RepLogApp's code into separate file, webpack does not pick it up automatically :) We need to restart it in order to keep things smooth and not be slapped by "RepLogApp is undefined" error :)

Version info:
└─ webpack@3.12.0
└─ @symfony/webpack-encore@0.19.0

Reply

Hey Jovan,

You need to restart webpack encore only if you change webpack.config.js, i.e. when add a new entry point. But if you just move a class into a separate file that you just want to import in an existent and known for webpack encore entry point - it will work, so you don't need to restart it.

Cheers!

Reply
Amy anuszewski Avatar
Amy anuszewski Avatar Amy anuszewski | posted 5 years ago

Stupid question time. We already have a class RepLogApp in our RepLogApp.js file . Why is nothing complaining about the duplicate name?

Reply

Hey Amy,

Because this is JavaScript? :p To be serious, probably because we moved that RepLogApp class into its own file assets/js/RepLog/RepLogApp.js. I mean, after we copied it from assets/js/rep_log_react.js and pasted into assets/js/RepLog/RepLogApp.js - we completely removed it from assets/js/rep_log_react.js and import RepLogApp in this file from assets/js/RepLog/RepLogApp.js. Does it makes sense to you? Oh, and yes, that's because of the scope, each file have its own scope, so I can declare variables, classes, etc. with the same names but in different files.

Cheers!

Reply
Amy anuszewski Avatar
Amy anuszewski Avatar Amy anuszewski | Victor | posted 5 years ago

Actually, I'm asking about js/Components/RepLogApp.js (which is powering the legacy app) and /js/RepLog/RepLogApp.js which is powering the new version. Both have the exact same class name. I see now that the import is directory based and therefore each entry point is able to use only the code it knows about, but at first it seems like it would cause all sorts of havoc.

Reply

Hey Amy,

Yeah, agree, and you should avoid duplicating class names in your project even if it works because you never import them both in one file. That's because we were doing refactoring step by step, but probably in a real project would be cool to change that legacy class name to something like LegacyRepLogApp etc. It requires a bit more work, but will be clearer.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.9.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
        "friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.4
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/monolog-bundle": "^3.1", // v3.3.0
        "symfony/polyfill-apcu": "^1.0", // v1.9.0
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/swiftmailer-bundle": "^3.1", // v3.2.3
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/validator": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/twig": "2.10.*" // v2.10.0
    },
    "require-dev": {
        "symfony/debug-pack": "^1.0", // v1.0.6
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.5", // v1.5.0
        "symfony/phpunit-bridge": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0" // v4.1.4
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "dependencies": {
        "@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
    },
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.5
        "@symfony/webpack-encore": "^0.26.0", // 0.26.0
        "babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
        "babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
        "bootstrap": "3", // 3.3.7
        "copy-webpack-plugin": "^4.4.1", // 4.5.1
        "core-js": "2", // 1.2.7
        "eslint": "^4.19.1", // 4.19.1
        "eslint-plugin-react": "^7.8.2", // 7.8.2
        "font-awesome": "4", // 4.7.0
        "jquery": "^3.3.1", // 3.3.1
        "promise-polyfill": "^8.0.0", // 8.0.0
        "prop-types": "^15.6.1", // 15.6.1
        "react": "^16.3.2", // 16.4.0
        "react-dom": "^16.3.2", // 16.4.0
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^7.0.0", // 7.3.1
        "sweetalert2": "^7.11.0", // 7.22.0
        "uuid": "^3.2.1", // 3.4.0
        "webpack-notifier": "^1.5.1", // 1.6.0
        "whatwg-fetch": "^2.0.4" // 2.0.4
    }
}
userVoice