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

Notifying Parent Components: Callback Props

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

We're storing the highlightedRowId state in our top level component and passing it down to our child component as a prop. Inside the child, we read that prop and use it in render(). It's all very zen.

Until... we go to the browser and click on one of the rows. Whoops! Our console exploded:

handleRowClick is not a function

This comes from RepLogList. Yep... it's right! There is no handleRowClick method on RepLogList. That method lives on the parent component: RepLogApp.

Communicating from Child to Parent

We've just hit another one of those very important moments. The parent component passes data down to the child via props. Thanks to this, the child component can be really dumb! It just receives props and renders them. That's it. Importantly, the child never communicates back up to the parent. Heck, the RepLogList component doesn't even know who its parent is! And, in theory, we could render RepLogList from 10 different parent components!

So... that leaves us in a pickle: inside onClick, we need to update the highlightedRowId state on our parent component. How can a child component update the state of a parent? The answer: it can't!

Wait wait, it's not time to panic... yet. Remember: the child component doesn't know who its parent is... heck! It doesn't even know that highlightedRowId is something that is stored in state! RepLogList just says:

I don't know, I'm just a React component. Somebody passes me a highlightedRowId prop and I render it. I don't know and I don't really care if it's stored in state.

So, how can we solve this problem? In the same way that we pass data from a parent component to a child component, we can also pass callback functions from parent to child. The child can effectively notify the parent when something happens by calling that function!

Passing a Callback Prop

It's best to see this in action. First, just to clean things up, in the handler function, remove the event argument: we were never using this anyways. Next, down where we render RepLogList, let's break this into multiple lines. Then pass in another prop called onRowClick set to this.handleRowClick. But, make sure you don't call that function: just pass it as a reference.

... lines 1 - 25
return (
... lines 27 - 38
<RepLogList
highlightedRowId={highlightedRowId}
onRowClick={this.handleRowClick}
/>
... lines 43 - 89
);
... lines 91 - 93

Thanks to this, in the child component, we have a fancy new onRowClick prop. Destructure this into a new variable. Then, onClick, we're dumb: we don't know anything about state, but we do know that we're passed an onRowClick prop, and that we're supposed to call this when the row is clicked! Cool! Call it and pass it repLog.id.

... lines 1 - 3
render() {
const { highlightedRowId, onRowClick } = this.props;
... lines 6 - 12
return (
... line 14
{repLogs.map((repLog) => (
<tr
... lines 17 - 18
onClick={() => onRowClick(repLog.id)}
>
... lines 21 - 25
))}
... line 27
)
}
... lines 30 - 31

That's it! When the user clicks the row, our dumb component will execute the callback and pass the rep log id. The parent maintains complete control of what to do when this happens.

Let's try it! Move over and refresh. Bah! We still have an error:

Cannot read property setState() of undefined

Whoops! Whenever we have a callback handler function, we need to guarantee that the this keyword is bound to this object. There are a few ways to do this, but I usually fix this in one consistent way: go to the constructor and add this.handleRowClick = this.handleRowClick.bind(this).

... lines 1 - 3
export default class RepLogApp extends Component {
constructor(props) {
... lines 6 - 11
this.handleRowClick = this.handleRowClick.bind(this);
}
... lines 14 - 93
}

Now, no matter who calls this method, this will always refer to this RepLogApp instance.

Refresh one more time. And, click! We got it!

We just saw a massively important pattern. Our state lives on our top level component. Then, we communicate to our children by passing data as props and we allow those children to communicate back to the parent component by passing them callback functions that should be executed when some interaction happens.

Oh, and by following this pattern, we've started to identify two types of components: stateful smart component and stateless dumb components. An important distinction we'll talk about next.

Leave a comment!

9
Login or Register to join the conversation
Graymath technology Avatar
Graymath technology Avatar Graymath technology | posted 4 years ago

Hi

what would be the non equivalent for
() => onRowClick(repLog.id)
if it were to be used

1 Reply

Hey Graymath technology!

Hmm, I don't think I understand. What do you mean by the "non equivalent"?

Cheers!

Reply
Abdul mannan Avatar

sorry for the typo I meant if this were to be written without the arrow function syntax how would it look like in that case

just trying to wrap my head around the arrow functions still

Reply

Hey Abdul mannan!

No worries :). They are confusing at first... but you will learn to love them (I also didn't love them at first).

So, we're using this:


onClick={() => onRowClick(repLog.id)}

The super-power of the arrow function is that, inside, the "this" variable remains what you expect it to be - in this case it would be the RepLogList object. However... in this situation... we're not using "this" inside the callback function! So, a non-arrow callback should work identically:


onClick={ function() { onRowClick(repLog.id)} }

Does that help? Both work fine! You're basically just saying: onClick, call this function. And because we're not referencing "this", both formats should work identically. But because arrow functions are typically more flexible, they tend to be used everywhere.

Cheers!

1 Reply
Ruslan Avatar

When we set prop:
this.handleRowClick = this.handleRowClick.bind(this)

Does it mean "this" always will be parent context or child ?

Reply

Hey @Ruslan!

By calling:

this.handleRowClick = this.handleRowClick.bind(this)

It guarantees that, inside of the handleRowClick() method, this will always by the "component" object: the RepLogApp object. Basically, it guarantees that the handleRowClick() method (which lives inside RepLogApp ) behaves how you expect: where this is the instance of the object it's inside of (RepLogApp). If you don't to the .bind(this) trick, then JavaScript would do weird stuff and the this variable inside of handleRowClick() would not refer to the RepLogApp instance.

Let me know if that helps :).

Cheers!

1 Reply
Ruslan Avatar

Thank you.

Reply

> Refresh... Refresh... Refresh...

Few videos ago you switched encore to dev-server = there's no need to refresh now...

Reply

Haha, yep :). Well... bad habit. And actually, in a few cases, when I switched over, I was too quick, and the page *hadn't* refreshed yet (and it was still finishing building, so wouldn't refresh for another second or two) and then the feature wouldn't work as expected. So... I took the "hardcore" option to avoid any issues :).

But I do say refresh a lot. I try to find other words to use... because it's even weird for me to say this so much! :D

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