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

Smart vs Dumb Components

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

So far, only RepLogApp has state. But, any component is allowed to have state, as long as each specific piece of state like highlightedRowId lives in just one place and isn't duplicated. But, yea, in general, any component can have state.

However, I'm going to create a rule, for now. And later, we'll talk about when we can bend this rule. For now, I want you to keep all of your state on the one, top level component. This means that all of your other components, which, right now is just one, will be stateless.

Hmm, if you think about this, it's a bit like how controllers and templates work in PHP. RepLogApp is like a controller: it's the place that controls all the data and logic. It updates state, and will eventually load and save things via AJAX calls.

Then, RepLogList is like a template. It does... nothing. It's dumb! It just receives data and prints that data. This separation is intentional. It means that we have two types of components: smart, stateful components, sometimes called "container components". And dumb, stateless components, sometimes called presentation components.

Stateless, Functional Components

Most dumb, stateless components also have something else in common: they usually only have one method: render(). So, purely for convenience, or I guess, laziness, you'll often see stateless components written, not as a class, but as a function.

Update this to: export default function RepLogList, but now without extends Component. When a component is a function, React passes you one arg: props. Now, we can remove one function level and... I'll unindent everything.

... lines 1 - 2
export default function RepLogList(props) {
... lines 4 - 27
}

Yep, the component is now just the render function... because that's all we needed! Refresh to try it! Oh, big error:

cannot read property props of undefined

Of course! Once a component is a function, there is no this anymore! That's fine, just change the code to use props. Hmm, destructuring everything in one place made that easy...

... lines 1 - 3
const { highlightedRowId, onRowClick } = props;
... lines 5 - 29

Try it again! Move over and... reload! Yeehaw! Success!

We just saw another important pattern: we will have one smart component on top, and then, because all its children are stateless and dumb, we will write those components as functions. We are going to bend this rule later: you can have more than one smart component and sometimes a "dumb" component can have state. But, until then, internalize this rule: one smart component on top, and then all dumb, functional, components inside of it that just receive data.

Smart Components should not have HTML

Ok, so, a smart component is like a controller in Symfony. Except... check out RepLogApp. It's a mixture of logic and... gasp! Markup! We would never put HTML code into our Symfony controllers: controllers should be pure logic.

And... surprise! We're going to follow that same rule with React components. New rule: a smart component should hold state & logic, but no, or, very little markup. To make this possible, a smart component should always wrap a dumb component.

A Smart Component Wraps a Dumb Component

Yep, we're going to split RepLogApp into two components: one with all the logic and another will all the markup. Create a new file called RepLogs.js: this will be our stateless, dumb component. So, this will look a lot like RepLogList: import React from react. And then, export default function RepLogs.

import React from 'react';
... line 2
export default function RepLogs(props) {
... lines 4 - 72
}

Next, go copy all of the code from RepLogApp's render() function and, paste it here.

... lines 1 - 2
export default function RepLogs(props) {
let heart = '';
if (withHeart) {
heart = <span>❤️</span>;
}
return (
<div className="col-md-7">
<h2>Lift Stuff! {heart}</h2>
<table className="table table-striped">
... lines 14 - 70
</div>
);
}

This new component has basically no logic, except for a tiny bit on top that's related to the markup itself.

Back in RepLogApp, delete all of that code! Instead, on top, import RepLogs from ./RepLogs. And then, in render, all we need is <RepLogs />.

... lines 1 - 2
import RepLogs from './RepLogs';
... line 4
export default class RepLogApp extends Component {
... lines 6 - 19
render() {
... lines 21 - 23
return <RepLogs/>
}
}

That is it! Oh, it's great: look how pure & clean the top level component is! Our business logic is much easier to read. And all the markup responsibilities now belong to RepLogs.

By the way, this is why smart components are often called "container" components: they are a container around a dumb, presentational component. People often even use that to name their components, like RepLogsContainer instead of RepLogApp.

Passing Props to the Dumb Component

Anyways, I'm sitting here celebrating our genius, but this won't actually work yet: RepLogs needs a few pieces of data: withHeart and highlightedRowId. Both of these are available in RepLogApp. Oh, and we also need to pass a prop for the handleRowClick callback: that method also lives in RepLogApp.

But, before we fix that, add the missing import on top: import RepLogList from ./RepLogList.

... line 1
import RepLogList from './RepLogList';
... lines 3 - 76

Then, while we're here, let's destructure the props we're about to receive: const { withHeart, highlightedRowId, onRowClick } = props.

... lines 1 - 3
export default function RepLogs(props) {
const { withHeart, highlightedRowId, onRowClick } = props;
... lines 6 - 75
}

Use the new onRowClick variable down below: pass this into RepLogList.

... lines 1 - 11
return (
... lines 13 - 24
<RepLogList
... line 26
onRowClick={onRowClick}
/>
... lines 29 - 74
);

Finally, head back to RepLogApp so that we can pass these props. I'll break things onto multiple lines, then add: withHeart={withHeart}, highlightedRowId={highlightedRowId} and onRowClick={this.handleRowClick}... being sure not to actually call that function, even though PhpStorm is trying to trick us!

... lines 1 - 18
render() {
... lines 20 - 22
return (
<RepLogs
highlightedRowId={highlightedRowId}
withHeart={withHeart}
onRowClick={this.handleRowClick}
/>
)
}
... lines 31 - 32

Oh, and I made a big ugly mistake! In RepLogs, import from RepLogList: I was trying to import myself! Strange and creepy things happen if you try that...

Ok, let's do this! Refresh! Yes! It still looks nice.

So here is our current system: one smart component on top, which acts like a controller in Symfony. Then, it renders a dumb, presentation component, just like how a controller renders a template. After that, you may choose to also render other dumb components, just to help keep things organized. Heck, we do that same thing in Symfony: a template can include another template.

This "pattern" is not an absolute rule, and, we'll talk more about how and when you'll bend it. But, generally speaking, by following this pattern upfront - even if you don't completely understand why it's important - it will save you big time later.

Leave a comment!

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

Hello, I am having this error Uncaught TypeError: onRowClick is not a function
at onClick (RepLogList.js:16)
my code looks exactly as in the Video, any though?

Edit:
I solved the issue - there was a mistake in the name of onRowClick, thank you

Reply
Amine Avatar
Amine Avatar Amine | posted 2 years ago | edited

Hello

I followed your tutorial up to this chapter

I got the following error

<br />react-dom.development.js:67 Warning: Cannot update during an existing state transition (such as within render`). Render methods should be a pure function of props and state.

at RepLogApp (http://127.0.0.1:8000/build/rep_log_react.js:96:5)
at div

printWarning @ react-dom.development.js:67
error @ react-dom.development.js:43
warnAboutRenderPhaseUpdatesInDEV @ react-dom.development.js:24011
scheduleUpdateOnFiber @ react-dom.development.js:21836
enqueueSetState @ react-dom.development.js:12467
push../node_modules/react/cjs/react.development.js.Component.setState @ react.development.js:365
handleRowClick @ RepLogApp.js:24
render @ RepLogApp.js:35
finishClassComponent @ react-dom.development.js:17485
updateClassComponent @ react-dom.development.js:17435
beginWork @ react-dom.development.js:19073
beginWork$1 @ react-dom.development.js:23940
performUnitOfWork @ react-dom.development.js:22776
workLoopSync @ react-dom.development.js:22707
renderRootSync @ react-dom.development.js:22670
performSyncWorkOnRoot @ react-dom.development.js:22293
scheduleUpdateOnFiber @ react-dom.development.js:21881
updateContainer @ react-dom.development.js:25482
(anonymous) @ react-dom.development.js:26021
unbatchedUpdates @ react-dom.development.js:22431
legacyRenderSubtreeIntoContainer @ react-dom.development.js:26020
render @ react-dom.development.js:26103
./assets/js/rep_log_react.js @ rep_log_react.js:31
__webpack_require__ @ bootstrap:79
checkDeferredModules @ bootstrap:45
webpackJsonpCallback @ bootstrap:32
(anonymous) @ rep_log_react.js:1


Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    at checkForNestedUpdates (react-dom.development.js:23803)
    at scheduleUpdateOnFiber (react-dom.development.js:21835)
    at Object.enqueueSetState (react-dom.development.js:12467)
    at RepLogApp.push../node_modules/react/cjs/react.development.js.Component.setState (react.development.js:365)
    at RepLogApp.handleRowClick (RepLogApp.js:24)
    at RepLogApp.render (RepLogApp.js:35)
    at finishClassComponent (react-dom.development.js:17485)
    at updateClassComponent (react-dom.development.js:17435)
    at beginWork (react-dom.development.js:19073)
    at beginWork$1 (react-dom.development.js:23940)
`



Any idea plz ?
Reply

Hey Amine

About your first error it seems like you are calling `this.setState()` inside the `render()` method, which is not allowed by React as the error describes

And about your second error, please double check the methods `componentWillUpdate()` and `componentDidUpdate()`. If you implemented those, it's likely that you're generating an infinite loop due to your logic

Cheers!

Reply
Amine Avatar
Amine Avatar Amine | MolloKhan | posted 2 years ago | edited

Hy MolloKhan

Thank you very much I really follow the tutorial, so at this point I have yet to define other methods

the problem was with the RepLogApp.js file

`
render() {

    const { highlightedRowId } = this.state;
    const { withHeart } = this.props;

    return (
        <RepLogs
            withHeart={withHeart}
            highlightedRowId={highlightedRowId}
            onRowClick={this.handleRowClick()}
        />
    )
}

`

it was necessary to put right onRowClick={this.handleRowClick and not with () onRowClick={this.handleRowClick()

I'm new to ReactJS so I don't understand 100% how it works yet

Thanks

Reply

Hey Amine

Sorry for my slow reply. SymfonyWorld week was crazy. When you do this onRowClick="{this.handleRowClick()}" it will execute the method and then, set its result on the property, that's why you need to pass just the reference of the method this.handleRockClick

Cheers!

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