Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

ReactJS talks to your API

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.

Remove the link. In base.html.twig, we already have a few JavaScript files that are included on every page. But now, I want to include some JavaScript on just this page - I don't need this stuff everywhere.

Page-Specific JavaScript (or CSS)

Remember from earlier that those script tags live in a javascripts block. Hey, that's perfect! In the child template, we can override that block: {% block javascripts %} then {% endblock %}:

... lines 1 - 23
{% block javascripts %}
... lines 25 - 36
{% endblock %}

Now, whatever JS we put here will end up at the bottom of the layout. Perfect, right?

No, not perfect! When you override blocks, you override them completely. With this code, it will completely replace the other scripts in the base template. I don't want that! I really want to append content to this block.

The secret awesome solution to this is the parent() function:

... lines 1 - 23
{% block javascripts %}
{{ parent() }}
... lines 26 - 36
{% endblock %}

This prints all of the content from the parent block, and then we can put our cool stuff below that.

Including the ReactJS Code

Here's the goal: add some JavaScript that will make an AJAX request to the notes API endpoint and use that to render them with the same markup we had before. We'll use ReactJS to do this. It's powerful... and super fun, but if it's new to you, don't worry. We're not going to learn it now, just preview it to see how to get our API working with a JavaScript frontend.

First, include three external script tags for React itself. Next, I'm going to include one more script tag that points to a file in our project: notes.react.js:

... lines 1 - 23
{% block javascripts %}
{{ parent() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>
... lines 31 - 36
{% endblock %}

Let's check that file out! Remember, it's in web/js/notes.react.js:

var NoteSection = React.createClass({
getInitialState: function() {
return {
notes: []
}
},
componentDidMount: function() {
this.loadNotesFromServer();
setInterval(this.loadNotesFromServer, 2000);
},
loadNotesFromServer: function() {
$.ajax({
url: '/genus/octopus/notes',
success: function (data) {
this.setState({notes: data.notes});
}.bind(this)
});
},
render: function() {
return (
<div>
<div className="notes-container">
<h2 className="notes-header">Notes</h2>
<div><i className="fa fa-plus plus-btn"></i></div>
</div>
<NoteList notes={this.state.notes} />
</div>
);
}
});
var NoteList = React.createClass({
render: function() {
var noteNodes = this.props.notes.map(function(note) {
return (
<NoteBox username={note.username} avatarUri={note.avatarUri} date={note.date} key={note.id}>{note.note}</NoteBox>
);
});
return (
<section id="cd-timeline">
{noteNodes}
</section>
);
}
});
var NoteBox = React.createClass({
render: function() {
return (
<div className="cd-timeline-block">
<div className="cd-timeline-img">
<img src={this.props.avatarUri} className="img-circle" alt="Leanna!" />
</div>
<div className="cd-timeline-content">
<h2><a href="#">{this.props.username}</a></h2>
<p>{this.props.children}</p>
<span className="cd-date">{this.props.date}</span>
</div>
</div>
);
}
});
window.NoteSection = NoteSection;

The ReactJS App

This is a small ReactJS app that uses our API to build all of the same markup that we had on the page before, but dynamically. It uses jQuery to make the AJAX call:

var NoteSection = React.createClass({
... lines 2 - 12
loadNotesFromServer: function() {
$.ajax({
url: '/genus/octopus/notes',
success: function (data) {
this.setState({notes: data.notes});
}.bind(this)
});
},
... lines 21 - 32
});
... lines 34 - 69

But I have a hardcoded URL right now - /genus/octopus/notes. Obviously, that's a problem, and lame. But ignore it for a second.

Back in the template, we need to start up the ReactJS app. Add a script tag with type="text/babel" - that's a React thing. To boot the app, add ReactDOM.render:

... lines 1 - 23
{% block javascripts %}
... lines 25 - 29
<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>
<script type="text/babel">
ReactDOM.render(
... lines 33 - 34
);
</script>
{% endblock %}

PhpStorm is not going to like how this looks, but ignore it. Render the NoteSection into document.getElementById('js-notes-wrapper'):

... lines 1 - 31
ReactDOM.render(
<NoteSection />,
document.getElementById('js-notes-wrapper')
);
... lines 36 - 38

Back in the HTML area, clear things out and add an empty div with this id:

... lines 1 - 4
{% block body %}
... lines 6 - 20
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 23 - 38

Everything will be rendered here.

Ya know what? I think we should try it. Refresh. It's alive! It happened quickly, but this is loading dynamically. In fact, I added some simple magic so that it checks for new comments every two seconds. Let's see if it'll update without refreshing.

In the controller, remove one of the notes - take out AquaWeaver in the middle. Back to the browser! Boom! It's gone. Now put it back. There it is! So, really cool stuff.

Generating the URL for JavaScript

But... we still have that hardcoded URL. That's still lame, and a problem. How you fix this will depend on if you're using AngularJS, ReactJS or something else. But the idea is the same: we need to pass the dynamic value into JavaScript. Change the URL to this.props.url:

var NoteSection = React.createClass({
... lines 2 - 12
loadNotesFromServer: function() {
$.ajax({
url: this.props.url,
... lines 16 - 18
});
},
... lines 21 - 32
});
... lines 34 - 69

This means that we will pass a url property to NoteSection. Since we create that in the Twig template, we'll pass it in there.

First, we need to get the URL to the API endpoint. Add var notesUrl = ''. Inside, generate the URL with twig using path(). Pass it genus_show_notes and the genusName set to name:

... lines 1 - 23
{% block javascripts %}
... lines 25 - 30
<script type="text/babel">
var notesUrl = '{{ path('genus_show_notes', {'genusName': name}) }}';
... lines 33 - 37
</script>
{% endblock %}

Yes, this is Twig inside of JavaScript. And yes, I know it can feel a little crazy.

Finally, pass this into React as a prop using url={notesUrl}:

... lines 1 - 33
ReactDOM.render(
<NoteSection url={notesUrl} />,
document.getElementById('js-notes-wrapper')
);
... lines 38 - 40

Try that out. It still works very nicely.

Go Deeper!

There is also an open-source bundle called FOSJsRoutingBundle that allows you to generate URLs purely from JavaScript. It's pretty awesome.

Congrats on making it this far: it means you're serious! We've just started, but we've already created a rich HTML page and an API endpoint to fuel some sweet JavaScript. And we're just starting to scratch the surface of Symfony.

What about talking to a database, using forms, setting up security or handling API input and validation? How and why should you register your own services? And what are event listeners? The answers to these will make you truly dangerous not just in Symfony, but as a programmer in general.

See you on the next challenge.

Leave a comment!

147
Login or Register to join the conversation
Default user avatar
Default user avatar Jade taboada | posted 4 years ago

TypeError: this.props.notes is undefined

3 Reply
Default user avatar

Check your js code in twig file. It's must be;
<script type="text/babel">
ReactDOM.render(
<notesection/>,
document.getElementById('js-notes-wrapper')
);
</script>

Instead of
ReactDOM.render(
<notesection url="{notesUrl}"/>,
document.getElementById('js-notes-wrapper')
);

Reply
Default user avatar
Default user avatar Marco Koopman | posted 4 years ago

Good tutorial but it would be easier to follow with just an JQuery AJAX call instead of the react stuff! I will have to follow the React course too to understand it I guess :P

1 Reply

Yea... I regret throwing React in here - the JavaScript isn't really the point of this tutorial and it trips people up :).

Reply
Maki V. Avatar

Hey, I don't agree. Your tutorials are so awesome! It helped me so much when I was joining my company. None else can describe so many things in such a cool way, as you do - AND - You motivated me to take a look at reactJS back in the day. Now I have been working in symfony for more than half year, every day and all because of this series of tutorial you have created. I like to rewatch them solely because of nostalgia purposes and quality it holds! :)

1 Reply

Milan V. you just made my day!!! What an awesome story - you ROCK!!! :) :) :)

1 Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 4 years ago

Hi,
A year has passed of this tutorial but I would like to mention that the 'notes.react.js' file doesn't load.

What's wrong?

NICE AND WONDERFUL TUTORIAL!

Kindest regards,

1 Reply

Hey Abelardo L. ,

What do you mean on "file doesn't load"? The file could not be found by your browser? Do you see 404 error for this file in "Network" tab og Google Chrome Dev Tools? We would love to help you fix this problem.

Cheers!

Reply
Abelardo Avatar
Abelardo Avatar Abelardo | victor | posted 4 years ago | edited

Hi victor ,
Thanks you for replying quickly.

It seems like that file isn't loaded by the browser.
How could I upload a screenshot of this issue?

Best regards.

Reply
Abelardo Avatar

The following files are only loaded: 'main.js' and 'jquery'.

I am using Opera 48.0.2685.39 on MAC OS El Capitan. It also happens in Google Chrome 61.0.3163.100.

Could be happen due to the newest version of React, jQuery or anything else?

Best regards, @Victor Bocharsky

1 Reply

I think it could be a problem related to version, but I'm not sure yet. Btw, did you download the course code and ran it from finish/ directory? Or, did you follow screencasts step by step and write all the code we show by yourself?

Reply
Abelardo Avatar

Hi Victor,
Yes, I followed screencasts step by step but I didn't run it from finish/ directory. I will do it.

Reply

Hey Abelardo L. ,

Great, I'd recommend you to do it separately of your code. So if it works from the downloaded archive, then we'll know exactly that the problem in *your* code only, so it'll be easy to debug things further.

Cheers!

Reply
Abelardo Avatar

Hi again,
I have just uploaded a screenshot when I run the 'finish' version of your app.
Please, check it out and let me know why these errors appear when I executed.
Best regards.
https://imgur.com/a/eZG6f

Reply

Hey Abelardo L.

Symfony couldnt find your autoloader file, look like you forgot to run `composer install`. Oh, also, there is a README file at the root of the project with setup instructions, you may find them useful

Cheers!

1 Reply
Abelardo Avatar

It worked! I forgot to run "composer install" (Y)

The notes were loaded too.

I am still investigating why the notes are not loaded into my app.

Cheers!

Reply

That's great! we moved one step further :)

How did it go? could you fix it?

Reply
Abelardo Avatar

With my last comment I referred to your 'finish' version, not mine.
I am investigating why my app doesn't work.
Whenever I fix it, I will post here. :)

Regards.

1 Reply

Please do it! :)

Reply

Hey Abelardo L. ,

Hm, I'm going to investigate the problem. What about screenshots - we do not allow to upload screenshots in Disqus comments, but you can upload it to any cloud, e.g. https://imgur.com/ and send a link to it in comments.

Cheers!

Reply

Hey Abelardo L. ,

Hm, that's interesting! So I see this file exists and you can get it by requesting http://localhost:8000/js/notes.react.js . But... I don't see it's loaded in Network tab, which means you don't have it in HTML code - probably it's due to cache! I bet if you press "Cmd + Option + U" (or right click -> View Page Source) you won't see the line:

<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>

Could you double check it doesn't exist in page source code to be sure? So it means, you just need to clear the cache, which make sense if you load Symfony in production environment - you should always clear the cache manually in prod after any change in templates or in configuration. Symfony regenerate the cache by itself in dev environment only.

In shorts, try to execute in your console:
bin/console cache:clear --env=prod

I'm 99% sure it should solve your problem.

Cheers!

Reply
Abelardo Avatar

Obviously, this file doesn't exist in view source code page. A new picture was uploaded to be verified by you.
I executed that line but the problem persists.
I deleted Opera cache and by using (cmd + R) and (cmd + alt + R) but none happens.
How can I change between dev-prod environments with Symfony?

Reply

Hey Abelardo L.
You only have to hit (inside web folder) `app.php` instead of `app_dev.php` file. Those files are you front controller, so it depends on which web server are you running, but basically you only have to switch your project public root path

Cheers!

1 Reply
Abelardo Avatar

Thanks :)

Reply

Hey Abelardo L. ,

Hm, I just double check it and it works for me well, i.e. browser is able to download "js/notes.react.js" file. I downloaded the course code, install composer deps and run "bin/console server:run" - that's it. So let's debug your code a bit. We include this file in one spot only: in "genus/show.html.twig" template.

1. Could you make sure you have: "<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>" exact line in "javascripts" block of that template? The full content of this block is:


{% block javascripts %}
{{ parent() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>

<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>
<script type="text/babel">
var notesUrl = '{{ path('genus_show_notes', {'genusName': name}) }}';

ReactDOM.render(
<notesection url="{notesUrl}"/>,
document.getElementById('js-notes-wrapper')
);
</script>
{% endblock %}

Is it the same you have?

2. Next, could you make sure you have this file in "web/js/notes.react.js" path?

3. Also, try to debug 404 error in Google Chrome dev tools, i.e. right click on "notes.react.js" -> "Open in new tab". What URL do you have? it should be exactly "http://localhost:8000/js/notes.react.js". Is it true for you?

4. Do you have Symfony's 404 error, i.e in the dev mode you should see something like:
> No route found for "GET /js/notes.react.js"

Try to follow those debug instructions step by step, I bet we'll fix it together. If you still have the same problem in the end, then provide some answers on my questions so I can move forward.

Cheers!

Reply
Abelardo Avatar

In the 2:30 of the screencast, you didn't write this line:
var notesUrl = '{{ path('genus_show_notes', {'genusName': name}) }}';

Reply

Yes, we add it a bit later, about 3:35. Haven't you seen the video to the end? If not, then what minute have you stuck at?

Reply
Abelardo Avatar

Yes, i saw the video to the end. But in that minute, 2:30, you can show that notes but I can't do it.
I am still investigating why I don't see the notes. Curious, I have the same code than you. ¿?

Reply
Abelardo Avatar

At that point because I can't see the notes.

Reply
Abelardo Avatar

Yes, I have seen it to the end but at that point I am stuck due to this error.

Reply
Abelardo Avatar

2) Yes, that file is in that path.
3) The 404 error isn't showed; simply, that file doesn't appear in the list of downloaded files.

Reply

2) Great!
3) I mean open Chrome dev tools -> Network tab and reload the page. If this page not found, you should see 404 status opposite this "notes.react.js". So you can perform right click on this line and choose "Open in new tab". After it, what URL do you have in the browser's address bar? is it "http://localhost:8000/js/notes.react.js"? Do you see an error or content of "notes.react.js" file in opened browser window?

Reply
Abelardo Avatar

No 404 error was showed while I had opened Chrome dev tools->Network tab

Reply
Abelardo Avatar

Definitively, that file doesn't appear in the list under network tab.

Reply
Abelardo Avatar

It seems like that file is not detected by the browsers Opera nor Chrome.

Reply
Abelardo Avatar

Its content.

Reply

That's great! It means you have this file and you can access it. So the problem is that you do not require it in HTML code, most probably it's the cache problem, you just need to clear the cache... or you wrote this line in a wrong file :)

Reply
Abelardo Avatar

No, I haven't written this line in a wrong file.
I think it's the cache problem.
I will check it out with FF.

Reply
Abelardo Avatar

By using Firefox (FF), it does happen the same: that .js file doesn't load. :S

Reply
Abelardo Avatar

4) No, I haven't got that error.

Reply

If you don't see this error, so then you should see content of "notes.react.js" file, right? Or, what do you see then in the browser's window when opens http://localhost:8000/js/notes.react.js URL?

Reply
Abelardo Avatar

The content of that file.

Reply
Abelardo Avatar

Yes, I see its content.

Reply
Default user avatar
Default user avatar Michael | posted 4 years ago

I am not able to load the comments dynamically, even after replacing my base.html.twig, show.html.twig, GenusController.php,notes.react.js and main.js with the files in your finished folder? Where should I look for the mistake?

There is one error I do have in the console:
Uncaught SyntaxError: embedded: Unexpected token (6:59)

4 | ReactDOM.render(

5 | <notesection url="{notesUrl}/">,

> 6 | document.getElementById('js-notes-wrapper');

| ^

7 | );

Also if I look at genus/octopus/notes page, every forward slash is escaped by a blackslash. I don't think that should cause any issues though.

Reply

Hi Michael!

As you listed here (good details!), the problem is happening earlier - when the React app is being initialized. I think it's a small syntax issue on line 5. Try this:


<notesection url="{notesUrl}"/>,

The NoteSection upper-casing is important, but the real problem is that you have an extra set of quotes around {notesUrl} and the ending / is *inside* of these quotes (that last detail I believe is causing the error).

Let me know if this helps!

Cheers!

Reply
Default user avatar
Default user avatar Enkhbilguun E. | posted 4 years ago

Hi Ryan,

Thanks for the great tutorial. Can you tell me how I can set the Symfony that I am working on Development Environment?

Unless I add /app_dev.php/ in the notes.react.js as below, it doesn't find http://domain/genus/octopus/notes.

Thanks.

Reply

Hey Enkhbilguun E.!

Ah yes, great question! When we run the built-in web server, when you go to http://localhost:8000/genus/octopus/notes, it automatically knows to use app_dev.php. So, that's why it works in the tutorial :).

But, the *real* solution is to basically do what you did - send the request to /app_dev.php/genus/octopus/notes. Of course, you don't want to hardcode this (then it would break on production!). What I would actually do is either:

A) Use FOSJsRoutingBundle - which allows you to generate URLs from routes right inside your JS code. It's awesome!

B) Generate the URL in Twig, set it on some global JS variable - e.g. window.notesUrl = '{{ path('...') }}'; - and access that inside of the React.js app.

I didn't want to dive into these details this early in the Symfony series - but it's a really valid question.

Cheers!

Reply
Default user avatar
Default user avatar Sheeran | posted 4 years ago

Hi, Ryan,

Well done with your colleagues, as a foreigner I am quite impressed to these tutorials, so I am planning to have the subscription for further courses, that must be great fun!
In the end of this episode, I run the course code perfectly ok, but when I downloaded the *react.js*, *react-dom.js* and *browser.min.js* to local machine, the dynamic effect and content doesn't exist anymore. I mean, the <div id="js-notes-wrapper"></div> part.
After my testing, I noticed that, if I change:
<script src="https://cdnjs.cloudflare.co..."></script>
into
<script src="{{ asset('js/browser.min.js') }}"></script>
then it stops just working, but *react.js* and *react-dom.js* can be used locally.
I am a newbie to this, so any advice or possible solutions will be appreciated!

Best regards,
Sheeran

Reply

Hey Sheeran!

Ah, awesome! I'm *thrilled* that these are useful (and fun!) for you - that's exactly what we're hoping for :).

So hmm, let's figure out your issue :). First, when it doesn't work, do you see any JavaScript errors in your browser's console? And also, when you switch from the cdnjs URL to the js/browser.min.js version, you said that "it stops working ,but react... can be used locally". What do you mean by "can be used locally"?

Let me know! And we'll find the problem.

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0" // v3.1.3
    }
}