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 SubscribeLook inside RepLogCreator
. The items in the drop-down are hardcoded. But, in reality, we can't just put whatever we want here: there is a specific set of valid options stored in our backend code.
We already know this is true because the last option is totally fake! When we send that to the server, it hits us with a validation error.
So, here is the question: instead of hardcoding these options, should we load them dynamically from the server?
The answer is... maybe? If these options won't ever change or change often, it's really not that big of a deal. The advantage is... simplicity!
But, if they will change often, or if having an invalid one on accident would cause a hugely critical or embarrassing bug, then yea, you should load them dynamically... so that you can sleep soundly at night.
Whenever your JavaScript app needs server data, there are two options. But, they both start the same way: by moving our itemOptions
up into RepLogApp
, which is the only component that's even aware a server exists!
Copy itemOptions
and then open RepLogApp
. On top, initialize a new itemOptions
state set to that array.
... lines 1 - 7 | |
constructor(props) { | |
... lines 9 - 10 | |
this.state = { | |
... lines 12 - 18 | |
itemOptions: [ | |
{id: 'cat', text: 'Cat'}, | |
{id: 'fat_cat', text: 'Big Fat Cat'}, | |
{id: 'laptop', text: 'My Laptop'}, | |
{id: 'coffee_cup', text: 'Coffee Cup'}, | |
{id: 'invalid_item', text: 'Dark Matter'} | |
] | |
}; | |
... lines 27 - 32 | |
} | |
... lines 34 - 157 |
Because all state is automatically passed as props to RepLogs
, go there and add the new prop type: itemOptions
as an array that is required.
... lines 1 - 99 | |
RepLogs.propTypes = { | |
... lines 101 - 112 | |
itemOptions: PropTypes.array.isRequired | |
}; |
Above, destructure that, then, below, pass it down to RepLogCreator
as itemOptions={itemOptions}
.
... lines 1 - 17 | |
export default function RepLogs(props) { | |
const { | |
... lines 20 - 31 | |
itemOptions | |
} = props; | |
... lines 34 - 39 | |
return ( | |
... lines 41 - 88 | |
<RepLogCreator | |
... lines 90 - 91 | |
itemOptions={itemOptions} | |
/> | |
... lines 94 - 96 | |
); | |
} | |
... lines 99 - 115 |
Copy the prop type, then do the same in RepLogCreator
: define the prop type at the bottom, then go to the top of the function to destructure out itemOptions
.
... lines 1 - 3 | |
export default class RepLogCreator extends Component { | |
... lines 5 - 45 | |
render() { | |
... line 47 | |
const { validationErrorMessage, itemOptions } = this.props; | |
... lines 49 - 92 | |
} | |
} | |
... line 95 | |
RepLogCreator.propTypes = { | |
... lines 97 - 98 | |
itemOptions: PropTypes.array.isRequired | |
}; |
Below, use the local itemOptions
variable for the map
function.
... lines 1 - 45 | |
render() { | |
... lines 47 - 49 | |
return ( | |
... lines 51 - 68 | |
{itemOptions.map(option => { | |
... lines 70 - 91 | |
); | |
} | |
... lines 94 - 101 |
When we refresh... cool! The options aren't dynamic yet, but they are stored as state. If you change a value... yep! It shows up.
Now that the data lives in our top-level component, let's talk about the two ways we can load this dynamically from the server. Actually, we already know the first way - we did it with repLogs
! We could set itemOptions
to an empty array, then make an AJAX call from inside componentDidMount()
. Of course, we would also need to create an API endpoint, but that's no big deal.
Or, you could use the second option: render a global variable inside Twig and read it in JavaScript. The advantage is that this data is available immediately: you can populate your app with some initial data, without waiting for the AJAX call.
Copy the options again and go into the entry file: rep_log_react.js
. This will not be the final home for these options - but it will get us one step closer. Create a new itemOptions
variable and paste! Now, pass these as a new prop: itemOptions={itemOptions}
.
... lines 1 - 6 | |
const itemOptions = [ | |
{id: 'cat', text: 'Cat'}, | |
{id: 'fat_cat', text: 'Big Fat Cat'}, | |
{id: 'laptop', text: 'My Laptop'}, | |
{id: 'coffee_cup', text: 'Coffee Cup'}, | |
{id: 'invalid_item', text: 'Dark Matter'} | |
]; | |
... line 14 | |
render( | |
<RepLogApp | |
... line 17 | |
itemOptions={itemOptions} | |
/>, | |
... line 20 | |
); |
Thanks to this, RepLogApp
will now receive a new itemOptions
prop. Remove the state entirely.
At the bottom, set this prop type: itemOptions
is an array, and you could make it required - I'll talk more about that in a minute.
... lines 1 - 147 | |
RepLogApp.propTypes = { | |
... line 149 | |
itemOptions: PropTypes.array, | |
}; |
Oh, and this is cool! We deleted the itemOptions
state but added an itemOptions
prop. And because we're passing all props & state to RepLogs
, it is still receiving an itemOptions
prop. In other words, this just works.
Side note: I originally set itemOptions
to state because this is needed if you wanted to make an AJAX call to populate them: they would be empty at first, then change a moment later when the request finished. But really, itemOptions
don't ever need to change. So once we passed them as props, we could remove the state.
But, if the item options really did need to be state - if this was something that changed throughout the life of our app - we could still use this strategy. We could use the itemOptions
prop to set the initial value of the state. This literally means that you would still have an itemOptions
state, and it would be initialized to this.props.itemOptions
.
I might even call the prop initialItemOptions
for clarity... though if you do have a state and prop with the same name, that's fine. If you look down in render()
, the state would override the prop, because the ...state
comes second.
Anyways, down in propTypes
, I did not make itemOptions
a required prop. In a real application, I probably would: I don't want the select to ever be empty. But sometimes, you will create a component where you want a prop to be truly optional. And in those cases, you need to be careful: if we didn't pass the itemOptions
prop, our code would explode! itemOptions
would be undefined instead of an array... which would be a problem when RepLogCreator
calls .map
on it.
To solve this, you can give any prop a default value. It's super easy: add RepLogApp.defaultProps =
an object with itemOptions
set to an empty array.
... lines 1 - 152 | |
RepLogApp.defaultProps = { | |
itemOptions: [] | |
}; |
Ok: we have removed the hardcoded itemOptions
from our React app entirely. But... we're not done: they're still hardcoded in rep_log_react.js
. We need to fetch this value dynamically from the server. Let's do that next!
// 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
}
}
Thanks for the great videos as always. Having worked with react before I'd like to point out that you may be discouraging people from using React with Symfony by creating so much manual and hard to manage code. You talk about best practise in the next video, though the moment you have to go up 4 levels to pass some data, I feel it would have really been easier and better to use the other option that you mentioned; fetch through an API endpoint or start using global state (e.g. Redux) instead. For example, a collection could have been gotten from a (static) global state, then later when it needs to become dynamic you can still create the API.