Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

Child Component

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

Our 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.

Creating the RepLogList Component

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
);
}
}

Leaving State at the Top Level (for now)

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.

Passing Props down the Tree

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.

Props are Immutable... but Change

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!

Leave a comment!

12
Login or Register to join the conversation
Ibrahim E. Avatar
Ibrahim E. Avatar Ibrahim E. | posted 2 years ago

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?

Reply

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!

Reply
Abhimanyu Avatar
Abhimanyu Avatar Abhimanyu | posted 4 years ago

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)

Reply

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.

Reply

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...

Reply

Hey Skylar

Do you get the same exact error? double check this variable

Cheers!

Reply

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!

Reply

weaverryan

weaverryan OK - I just had to play the first minute of the next chapter to realise that this is totally expected :)

Reply

Excellent, so everything is working as expected, right?

Reply

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.

Reply

You are totally right, it could be alarming. I'm gonna add a note about it :)

Cheers!

Reply

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)}

Reply
Cat in space

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

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

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
    }
}
userVoice