If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOk people: it's time to make our React app legit, by loading and saving data to the server via AJAX. Our Symfony app already has a functional set of API endpoints to load, delete and save rep logs. We're not going to spend a lot of time talking about the API side of things: we'll save that for a future tutorial. That's when we'll also talk about other great tools - like ApiPlatform - that you won't see used here.
Anyways, I want to at least take you through the basics of my simple, but very functional, setup.
Open src/Controller/RepLogController.php
. As you can see, we already have API endpoints for returning all of the rep logs for a user, a single rep log, deleting a rep log and adding a new rep log. Go back to the browser to check this out: /reps
. Boom! A JSON list of the rep logs.
This endpoint is powered by getRepLogsAction()
. The findAllUsersRepLogModels()
method lives in the parent class - BaseController
, which lives in this same directory. Hold Command or Ctrl and click to jump into it.
The really important part is this: I have two rep log classes. First, the RepLog
entity stores all the data in the database. Second, in the Api
directory, I have another class called RepLogApiModel
. This is the class that's transformed into JSON and used for the API: you can see that it has the same fields as the JSON response.
The findAllUsersRepLogModels
method first queries for the RepLog
entity objects. Then, it loops over each and transforms it into a RepLogApiModel
object by calling another method, which lives right above this. The code is super boring and not fancy at all: it simply takes the RepLog
entity object and, piece by piece, converts it into a RepLogApiModel
.
Finally, back in getRepLogsAction()
, we return $this->createApiResponse()
and pass it that array of RepLogApiModel
objects. This method also lives inside BaseController
and it's dead-simple: it uses Symfony's serializer to turn the objects to JSON, then puts that into a Response
.
That's it! The most interesting part is that I'm using two classes: the entity and a separate class for the serializer. Having 2 classes means that you need to do some extra work. However... it makes it really easy to make your API look exactly how you want! But, in a lot of cases, serializing your entity object directly works great.
So here's our first goal: make an API request to /reps
and use that to populate our initial repLogs
state so that they render in the table.
In the assets/js
directory, create a new folder called api
and then a new file called rep_log_api.js
. This new file will contain all of the logic we need for making requests related to the rep log API endpoints. As our app grows, we might create other files to talk to other resources, like "users" or "products".
You probably also noticed that the filename is lowercase. That's a minor detail. This is because, instead of exporting a class, this module will export some functions... so that's just a naming convention. Inside, export function getRepLogs()
.
... lines 1 - 5 | |
export function getRepLogs() { | |
... lines 7 - 12 |
The question now is... how do we make AJAX calls? There are several great libraries that can help with this. But... actually... we don't need them! All modern browsers have a built-in function that makes AJAX calls super easy. It's called fetch()
!
Try this: return fetch('/reps')
. fetch()
returns a Promise
object, which is a super important, but kinda-confusing object we talked a lot about in our ES6 tutorial. To decode the JSON from our API into a JavaScript object, we can add a success handler: .then()
, passing it an arrow function with a response
argument. Inside, return response.json()
.
... lines 1 - 5 | |
export function getRepLogs() { | |
return fetch('/reps') | |
.then(response => { | |
return response.json(); | |
}); | |
} |
With this code, our getRepLogs()
function will still return a Promise
. But the "data" for that should now be the decoded JSON. Don't worry, we'll show this in action.
By the way, I mentioned that fetch
is available in all modern browsers. So yes, we do need to worry about what happens in older browsers. We'll do that later.
Go back to RepLogApp
. Ok, as soon as the page loads, we want to make an AJAX call to /reps
and use that to populate the state. The constructor seems like a good place for that code. Oh, but first, bring it in: import { getRepLogs }
from ../api/rep_log_api
. For the first time, we're not exporting a default
value: we're exporting a named function. We'll export more named functions later, for inserting and deleting rep logs.
... lines 1 - 4 | |
import { getRepLogs } from '../api/rep_log_api'; | |
... lines 6 - 83 |
Oh, and, did you see how PhpStorm auto-completed that for me? That was awesome! And it wasn't video magic: PhpStorm was cool enough to guess that correct import path.
Down below, add getRepLogs()
and chain .then()
. Because we decoded the JSON already, this should receive that decoded data
. Just log it for now.
... lines 1 - 7 | |
constructor(props) { | |
... lines 9 - 10 | |
getRepLogs() | |
.then((data) => { | |
console.log(data); | |
}); | |
... lines 15 - 29 | |
} | |
... lines 31 - 83 |
Ok... let's try it! Move over and, refresh! Oof! An error:
Unexpected token in JSON at position zero
Hmm. It seems like our AJAX call might be working... but it's having a problem decoding the JSON. It turns out, the problem is authentication. Let's learn how to debug this and how to authenticate our React API requests next.
Hey Xmontero,
If you want to use the correct URLs in your JS files - the best way is to generate the code with path()
or url()
Twig functions, i.e. generate URLs in Twig templates and pass them into the JS code. This way you will be sure that URLs are generated correctly. You can pass pre-geenrated URLs in a few ways, e.g. setting a data attribute on a tag an then read it from the JS code, or setting a global JS variable in script HTML tag in a Twig template, etc.
I hope this helps!
Cheers!
Tested, it works!
Just in case it helps anyone, what I did is:
<meta>
tag in my header with the API base path.My javascript looked like this initially (hardcoded path, bad code):
// agent_api.js
export function getAgents() {
return fetch( 'http://127.0.0.1:22080/repos/hello-trip/telethon-parsec_agent-panel/public/api/agents' )
.then( response => response.json() );
}
To overcome this I've done those editions:
In the routing I added this non-callable route:
# api_agent_routes.yaml
api_root:
path: /api/
methods: []
[...]
In the base HTML template I added a meta tag before the javascripts, in the header:
[...]
<meta id="api-route" data-api-path="{{ path( 'api_root' ) }}">
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</head>
[...]
In the javascript I get the data attribute as explained here: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
export function getAgents() {
const apiRouteElement = document.querySelector( '#api-route' );
return fetch( apiRouteElement.dataset.apiPath + 'agents' )
.then( response => response.json() );
}
I hope this code helps someone serving from a subdir! Thanks @victor for the tip.
Hey Xmontero,
Awesome! Thanks for confirming it worked for you, and thanks for the details examples that could be useful to others!
Cheers!
// 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
}
}
// 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
}
}
Hi, friends, I'd need some help.
Instead of
RepLogs
I serveAgents
, but no matter. All the same following the tutorial.# I've a problem with the routing in the
fetch()
callMy "list of agents" API lives here
/api/agents
thanks to this route:The controller that paints the twig where the root component
<AgentApp />
is mounted is served here:/private/admin/react-test
in a routing like this:Okey, when I need routes in the twig, I easily use
path( 'api_agent_list' )
so the URL is represented correctly.But in this chapter the
rep_log_api.js
(in my caseagent_api.js
) does a fetch directly to the URL, in the example i the video to/reps
.Thing is...
I do not serve my project from the root, but from a directory, and the project is not aware of the directory it's served from.
In fact, my controller in
private/admin/react-test
is served specifically from herehttp://127.0.0.1:22080/repos/hello-trip/telethon-parsec_agent-panel/public/private/admin/react-test
This yields in a routing problem:
fetch( '/api/agents' )
with/
then the API is loading fromhttp://127.0.0.1:22080/api/agents
which does not exist.fetch( 'api/agents' )
without/
then the API is loading relative to the base of the controller path, soi it's trying to fetch from here:http://127.0.0.1:22080/repos/hello-trip/telethon-parsec_agent-panel/public/private/admin/api/agents
which is also incorrect.In twig we play with the routing name, in this case
api_agent_list
, so the symfony framework "builds" the proper route. Worth mentioning that when I configure webpack I need to change this.setPublicPath( '/build' )
into this.setPublicPath( 'build' )
so subdirectory access is allowed.It works... in twig.
# Question
What do I have to put inside the
fetch()
in the javascript so it loads the proper path given in the route and supporting subdirectory deploying?