Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Build the Static App First

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

One of our big goals in this tutorial is to create a repeatable path to success. And, we're already doing that! Step 1 is always to create an entry file. That file doesn't do much except render a React component onto your page.

Step 2, in that React component, build out an entirely static version of your app. First do this in pure HTML. Then, create some hardcoded variables and render those. For example, we first built one dummy tr element by hand and then created a hardcoded repLogs array and used that to build the rows.

So, step 2 for success is to build your entire UI statically... and then, soon, we will make things dynamic and fancy.

Adding the Static Form

The only UI that's missing form our app now is the form below the table. No problem! Go back into templates/lift/index.html.twig. Ah, the form lives in another template: _form.html.twig. Open that: it's just a normal, boring HTML form. That's perfect! Copy all of it, close the file, go back into RepLogApp and, under the table, paste!

... lines 1 - 2
export default class RepLogApp extends Component {
render() {
... lines 5 - 15
return (
<div className="col-md-7">
... lines 18 - 48
<form className="form-inline js-new-rep-log-form" noValidate
data-url="{{ path('rep_log_new') }}">
... lines 51 - 83
... line 86

Except, scroll up a little bit, because we need to do some cleanup! The form doesn't need this class anymore: that was used by the old JavaScript. The same is true for the data-url element. And noValidate disables HTML5 validation. But, HTML5 validation is nice to have: it will enforce the required attribute on the fields. So, remove it.

... lines 1 - 15
return (
... lines 17 - 48
<form className="form-inline">
... lines 50 - 85
... lines 87 - 89

Oh, but I want you to notice something! The attribute was noValidate with a capital "V"! In the original template, it was novalidate with a lowercase "v": that's how the property is called in HTML. When we pasted it, PhpStorm updated it for us. This is one of those uncommon situations - like class and className where the HTML attribute is slightly different than what you need to use in React. I want to point that out, but don't over-think it: almost everything is the same, and React will usually warn you if it's not.

Cool! Try it out: refresh! Awesome! We have a form!

Using form defaultValue

Hmm, but the styling is not quite right. And, we have a warning about using a defaultValue. Let's fix that first. We'll talk a lot more about forms in React later. But basically, React is a control freak, it really really* wants to manage the values of any elements on your form, including which option is selected. So, instead of using selected="selected", you can use defaultValue="" on the select element and set it to the value of the option you want. I'll skip that part because the first option will be automatically selected anyways.

Adding Ugly Manual Whitespace

Ok, back to the styling problem. Inspect element on the form itself, right click on it, and go to "Edit as HTML". Ah, React renders as one big line with no spaces in it. 99% of the time... we don't care about this: usually whitespace is meaningless. But, in this case, the form is an inline element: we need a space between the first two fields, and between the last field and the button. Without the space, everything renders "smashed" together.

The fix is both simple... and ugly: use JavaScript to print an extra space. Do it in both places. Yep, weird, but honestly, I rarely need to do this: it's just not a problem you have very often.

... lines 1 - 48
<form className="form-inline">
<div className="form-group">
... lines 51 - 66
{' '}
<div className="form-group">
... lines 70 - 77
{' '}
... lines 80 - 82
... lines 84 - 89

Try it again... yep! It looks much better! And we have our static app!

Using the Dev Server

Before we go kick more butt, I want to make one adjustment to our workflow. We're using Webpack Encore. We ran it with:

yarn run encore dev --watch

Thanks to that, each time we update a file, it notices, and re-builds our assets. But rather than using encore dev, use encore dev-server. This is pretty interesting: instead of writing physical files to our public/build directory, this starts a new web server in the background that serves the built assets.

Check this out: go back and refresh the app. No visible differences. But now, view the page source. Suddenly, instead of pointing locally, like /build/layout.css, every asset is pointing to that new server: http://localhost:8080! This magic URL changing is thanks to Webpack Encore and some config changes we made to our Symfony app in the Encore tutorial.

The web server - http://localhost:8080 - is the server that was just started by Webpack. When you request an asset from it, Webpack returns the latest, built version of that file. What's weird is that the built assets are no longer physically written to the filesystem. Nope, we just fetch the dynamic version from the new server.

Ultimately... this is just a different, fancier way to make sure that our code is always using the latest version of the built assets. But, as your app gets more complex, it may become possible for you to refresh your page before Webpack has been able to physically write the new files! However, if you use the dev-server and refresh too quickly, your browser will wait for the CSS or JavaScript files to be ready, before loading the page. And, as an added "nice thing", the dev server will cause our browser to automatically refresh whenever we make changes.

Anyways, or goal was to build our entire app with a static UI. And, we've done that! Sure, we have some fanciness: we learned that we can pass "props" into our components and then use those to render things dynamically. So, our app is "kind of" dynamic, because we can control different parts of how it looks by passing different props. But... once our component is rendered, it's static. For example, once we render RepLogApp with a heart... it will have a heart forever.

But, the whole point of using React is so that our UI will automagically update when data changes! And we'll do that with something very, very important called state.

Leave a comment!

Login or Register to join the conversation

I think In th end of the tutorial we gonna delete all the twig files ? can we use a template engine with react?

1 Reply

Hey ahmedbhs!

Great question! The answer is a bit more interesting than a yes/no :).

First, yes, by the end of the tutorial, we will delete all of the *markup* from our Twig template that is now being rendered by React. But we will not completely delete your Twig templates.

Second, you can of course create a pure single-page-app with React. In this case, you wouldn't need any Twig templates. Well, maybe you would just render one Twig template that has some basic markup... but that would be it (React would render everything else). Or, you can create a more traditional multi-page app (SymfonyCasts is a good example of this) where *some* parts of your app use React. In that case, you would render a Twig template that, for example, rendered most of the markup for your page. But, in *one* spot on the page, you would just have an empty div. Then, React would build everything inside this div.

This is actually what we'll do in this tutorial. By the end, we will still have a traditional (no React) login page. And, on the "main" page, the header/layout will still be done by Twig, as well as the leaderboard on the right. Only the table/form part will be done by React. So basically, you can do everything in Twig or everything in React or any mixture in between :).


2 Reply
Dan_M Avatar
Dan_M Avatar Dan_M | posted 4 years ago | edited

Hey guys!

The dev-server really speeds up the workflow, but I have a problem with it. My webpack.json has this entry:

    .addPlugin(new CopyWebpackPlugin([
        // copies to {output}/static
        { from: './assets/static', to: 'static' }

Dev-server seems to erase the build directory, including build/static, so I have no static assets on my page. Am I missing something in my webpack.json?


Hey Dan_M!

Yea, thw dev-server is great, once you figure out how it works (it was a little "weird" for me at first not to see my compiled assets - eventually I realized that they're generated automatically).

Anyways, I can't remember the exact behavior with dev-server and CopyWebpackPlugin. But, I think I know the issue. I believe that plugin, like all of Webpack itself, does not physically copy the files when you're in dev-server. Instead, if you make a request to the dev-server - e.g. http://localhost:8080/build/static/myfile.png - and it dynamically returns that to you. I'm not 100% sure about this, but I believe that the plugin works this way (it should).

The problem then, is that, in your templates, you're final HTML probably just looks like &t;img src="/build/static/myfile.png"&gt;, which means your requesting that file from your local, PHP server - not from the dev-server URL. In theory, if you're using the asset() function, that corrects things: it looks in manifest.json and sees that the URL needs the dev-server hostname in front of it. However, due to bad behavior with CopyWebpackPlugin, static assets are not included in manifest.json... and then we're all sad :(.

So, let me tell you 2 things:

1) If you remove the cleanupOutputBeforeBuild() call (which is not as important with dev-server anyways), you can probably work around this by running a normal build first, then doing dev-server. Kinda lame, but there you go.

2) In the upcoming/next release of Encore (0.21.0), we will have a new copyFile() method that works like the CopyWebpackPlugin, but correctly puts things into the manifest.json file. This should be the real solution.

Let me know if that makes sense! Or if I'm totally wrong! :)


Dan_M Avatar
Dan_M Avatar Dan_M | weaverryan | posted 4 years ago | edited


I used your "kinda lame" workaround and, of course, that worked for assets rendered with twig. Without the cleanupOutputBeforeBuild() call, all of my static assets were still there and were called as I would expect.

However, when I rendered an asset in React, it did not work using something like this:

render() {
    return (
        <img src="/build/static/myasset.png">

But if I change the img tag to

<img src="http://localhost:8080/build/static/myasset.png">

it works just fine.

Of course, that's not a solution. So, I'm still stuck.

Beyond that, your solution #2 sounds good, but I don't see how that works in React. Is there a jsx equivalent to asset() I should be using?


Hey Dan_M!

Oh, oh oh! You're doing something WAY more awesome than what I was thinking! You're referencing an asset from JavaScript! Brilliant! Just do this:

// make this the REAL relative path to the image file, relative to the original directory (not the built directory)
const assetPath = require('../images/myasset.png');

render() {
    return (
        <img src="{assetPath}">

That's it! You don't even need the copy plugin. Webpack natively (well, when you tell it what to do, which Encore does) knows how to require image files. By requiring the image file, it will move it into the build directory and will return back the final path. This is actually the proper way to render image paths :).

Let me know if this make sense!


Dan_M Avatar
Dan_M Avatar Dan_M | weaverryan | posted 4 years ago | edited


That works!

In my case, the render function was in a form class, so it looked like:

export default class MyForm extends Component {
    constructor(props) {
        this.assetPath = require('../../static/myimage.png');

    render() {
        return (
             <img src={this.assetPath}>

Perhaps simpler is to just do:

    render() {
        return (
                <img src={require('../../static/myimage.png')}>

In either case, it took me a minute to realize the path in the require statement is relative to the javascript source file, not the build directory.




Hey Dan_M

Yep, you are correct, the require function points to where the file lives. I'm glad to hear that your code is working again. Cheers!

Cat in space

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

This course uses Symfony 4, but as this is a JavaScript course, all the concepts apply fine to Symfony 5. Have fun!

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