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 SubscribeOur RepLogApp
component is getting kinda big! I'm so proud! It's not only the amount of HTML, but also its complexity. We're now handling an event, updating state in a handler function and, below, this repLogs
row stuff is pretty complex on its own!
In PHP, if you're working on a class, sometimes that class can become so big or so complex that, for your own sanity, you choose to create a new class and move some of that logic into it. Another reason you might create a new class is if you want to make some of your logic re-usable.
Well, that exact idea is true in React: when a component becomes too big or too complex & confusing, you can choose to move part of it into a new component. This isn't some ground-breaking strategy: it just simple code organization! And, in theory, you could re-use the new component in multiple places.
Because the rep log table is already pretty complex, let's move that into its own component first. In the RepLog
directory, create a new file: how about, RepLogList.js
. Inside, every React component begins the same way: import React
and { Component }
from react
. Then, export default class RepLogList extends Component
. Add the one required method: render()
.
import React, { Component } from 'react'; | |
... line 2 | |
export default class RepLogList extends Component { | |
render() { | |
... lines 5 - 26 | |
} | |
} |
So... hmm... I basically want to move my rep log rows into that component. We could move the whole table, or just the inside - don't over-think it. Let's copy all of the tbody
. Then, return, add parenthesis so we can use multiple lines and, paste!
Cool! Of course, we're missing the repLogs
variable! Right now, because that's still hardcoded, let's just move that variable over into the render()
method of the new component.
... lines 1 - 3 | |
render() { | |
const repLogs = [ | |
{ id: 1, reps: 25, itemLabel: 'My Laptop', totalWeightLifted: 112.5 }, | |
{ id: 2, reps: 10, itemLabel: 'Big Fat Cat', totalWeightLifted: 180 }, | |
{ id: 8, reps: 4, itemLabel: 'Big Fat Cat', totalWeightLifted: 72 } | |
]; | |
return ( | |
<tbody> | |
{repLogs.map((repLog) => ( | |
<tr | |
key={repLog.id} | |
className={highlightedRowId === repLog.id ? 'info' : ''} | |
onClick={(event) => this.handleRowClick(repLog.id, event)} | |
> | |
<td>{repLog.itemLabel}</td> | |
<td>{repLog.reps}</td> | |
<td>{repLog.totalWeightLifted}</td> | |
<td>...</td> | |
</tr> | |
))} | |
</tbody> | |
) | |
} | |
... lines 28 - 29 |
But, we do still have one problem: highlightedRowId
. Um, ignore that for a minute. Back in RepLogApp
, delete the tbody
. At the top, this is cool: import RepLogList from './RepLogList'
. And because RepLogList
is a component, we can render it just like we did with RepLogApp
: go into the middle of the markup and add <RepLogList />
.
... line 1 | |
import RepLogList from './RepLogList'; | |
... line 3 | |
export default class RepLogApp extends Component { | |
... lines 5 - 16 | |
render() { | |
... lines 18 - 25 | |
return ( | |
... lines 27 - 29 | |
<table className="table table-striped"> | |
... lines 31 - 38 | |
<RepLogList/> | |
... lines 40 - 47 | |
</table> | |
... lines 49 - 86 | |
); | |
} | |
} |
That's it! We have successfully broken our big component into a smaller piece. Well, I guess we shouldn't celebrate too much, because, when we refresh, in the console, yep! React is always trying to bring us down: the highlightedRowId
variable is not defined in RepLogList
!
That makes perfect sense: our child component - RepLogList
- needs to know this value so that it can add the info
class. But... hmm... we have a problem! The highlightedRowId
state lives in a different component: our top-level RepLogApp
component! So, how can access the state of our parent component?
Well, before I answer that, there is technically another option: we could just move the highlightedRowId
state into the RepLogList
component. And, technically, this would work! Look closely: RepLogApp
isn't using that data anywhere else! So if we moved the state, everything would work!
But... for a reason I can't fully explain yet, I don't want you to do that. Nope, I want you to leave all of your state in the top level component of your app. That means, I want all of your child components to have zero state. Don't worry: we'll talk a lot more about why later.
But, because I'm being rude and forcing you to keep all of your state in RepLogApp
, the question becomes: how can we pass this highlightedRowId
state into RepLogList
?
Guess what? We already know the answer! We already know how to pass data into a component: props. We have the highlightedRowId
variable that's coming from state. Scroll down to RepLogList
and add a new prop: highlightedRowId={}
and pass that variable.
... lines 1 - 38 | |
<RepLogList highlightedRowId={highlightedRowId}/> | |
... lines 40 - 90 |
And now we can go back into RepLogList
and use this in render()
! At the top, let's continue to destructure our props & state: const { highlightedRowId } = this.props
. And, just like earlier, ignore this error about props validation: we'll talk about that soon.
... lines 1 - 3 | |
render() { | |
const { highlightedRowId } = this.props; | |
... lines 6 - 28 | |
} | |
... lines 30 - 31 |
Ok... we're done! Move back to your browser and, refresh! It works! And if you check out the React dev tools, you can still see RepLogApp
on top... but down here, hey! There is the embedded RepLogList
. Now, things get fun: click back on RepLogApp
and change the state to 2. This causes React to re-render that component. Check out RepLogList
again - yea! You can see that its prop automatically updated!
This highlights one really, really important detail: while you may have multiple components that have some state, each piece of state like the highlightedRowId
- needs to live in exactly one component. What I mean is: you are not allowed to have, for example, a highlightedRowId
state in RepLogApp
and also a highlightedRowId
state in RepLogList
. Nope! That would duplicate that data. Instead, each piece of state will live in just one component. And then, if a child component needs that data, we'll pass it as a prop.
We already know that whenever something updates the state of a component, React automatically re-renders that component by calling render()
. And actually, the same is true for props. When the highlightedRowId
state changes, this changes the props of RepLogList
and that causes it to also re-render. Which, is exactly what we want!
But, earlier, I told you that props are immutable: that props can never be changed. That's true, but it's maybe not the best way to explain it. In RepLogApp
, when the highlightedRowId
state changes, we will pass a new value to RepLogList
for the highlightedRowId
prop. But, here's the important part: once RepLogList
receives that prop, it never changes it. You will never change something on this.props
.
We're going to see this pattern over and over again: we hold state in one component, change the state in that component and then pass that state to any child component that needs it as a prop. And now we know that when that state changes, all the child components that use it will automatically re-render.
But... our click handling code is now broken! Let's fix it!
Hey Ibrahim E.
That's a good observation and the thing here is in the React world everything renders from top to bottom, in this case, inside the RepLogApp component we render RepLogList components, so it's valid to say that RepLogList is a child of RepLogApp but this does not have anything to do with any OOP principles (Actually, JavaScript it's not a real OO language).
Cheers!
I'm getting the following warning.
#1 RepLogList.js:20 Uncaught TypeError: _this2.handleRowClick is not a function
at onClick (RepLogList.js:20)
at HTMLUnknownElement.callCallback (react-dom.development.js:149)
at Object.invokeGuardedCallbackDev (react-dom.development.js:199)
at invokeGuardedCallback (react-dom.development.js:256)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:270)
at executeDispatch (react-dom.development.js:561)
at executeDispatchesInOrder (react-dom.development.js:583)
at executeDispatchesAndRelease (react-dom.development.js:680)
at executeDispatchesAndReleaseTopLevel (react-dom.development.js:688)
at forEachAccumulated (react-dom.development.js:662)
#2 react-dom.development.js:210 Uncaught Error: A cross-origin error was thrown. React doesn't have access to the actual error object in development. See https://fb.me/react-crossor... for more information.
at Object.invokeGuardedCallbackDev (react-dom.development.js:210)
at invokeGuardedCallback (react-dom.development.js:256)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:270)
at executeDispatch (react-dom.development.js:561)
at executeDispatchesInOrder (react-dom.development.js:583)
at executeDispatchesAndRelease (react-dom.development.js:680)
at executeDispatchesAndReleaseTopLevel (react-dom.development.js:688)
at forEachAccumulated (react-dom.development.js:662)
at runEventsInBatch (react-dom.development.js:816)
at runExtractedEventsInBatch (react-dom.development.js:824)
Hey guys,
I am having the same problem. I don't mind ignoring errors, but the row is not highlighting. I checked to make sure I am using the arrow function as stated in the comments here, but still not able to get it to work.
Doh! Nevermind. I just realized that when you were testing to see if the code worked, you did not click it. Instead you changed the value in the React tab of Chrome Inspector. As I plunged ahead anyway, you mention that the click handler is broken and needs to be fixed. So I will keep on, keep'n on...
Hey Abhimanyu!
Hmm, let's see! So, the error (based on the "handleRowClick() is not a function" and because it says it's on line 20 of RepLogList.js) is likely this line:
onClick={(event) => this.handleRowClick(repLog.id, event)}
From this code block: https://symfonycasts.com/screencast/reactjs/child-component#codeblock-57285e7743
Notice the error is complaining about "_this2", not "this". That is likely just because of how Webpack / Babel is rewriting the code - so "_this2" almost definitely is referring to "this". So, the question is: why is there handleRowClick() not a function? My guess is that: that you're not using an "arrow" function on this line. Notice the =>
in the code above - do you have that? If you do NOT - if you have a normal function()
, then the "this" variable is being changed. That's actually why we use arrow functions. We talk a lot about them here: https://symfonycasts.com/screencast/javascript-es6/arrow-functions#the-arrow-functions-secret-superpower-this
Let me know if that's the issue!
weaverryan
weaverryan OK - I just had to play the first minute of the next chapter to realise that this is totally expected :)
Yes - I mean, the error is actually there. One could get alarmed by the fact that there is an error as it's not mentioned in the video. Luckily it's first thing addressed in the subsequent chapter.
weaverryan same problem here for me. I've double checked line 20 and confirm that is using an arrow function as follows:
onClick={(event) => this.handleRowClick(repLog.id, event)}
// 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
}
}
Hello, why we call RepLogList Com is a child of RepLogApp? in OOP Concept, if class B is a child of class A, we must first define the relationship (using 'extends' key word for example), because class B can recognise its parent class and inherits properties and functions from the base class. However, in OOP, class A can use class B by declaring an object from class B inside it but we call this Class A 'USE or has-a relationship' class B. So here why we do not say RepLogApp Com uses or has-a RepLogList Com? instead of saying RepLogList is a child of RepLogApp Com?