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

Reusable 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

React's entire architecture is centered around making components that are reusable. This is especially easy to see with the dumb, presentational components: all they do is receive props and... render! It would be very easy to render those components with different props in different places.

But, in reality, a lot of components aren't really meant to be reused: RepLogCreator, RepLogList and RepLogs... yea, it's pretty unlikely we'll use those on other parts of our site... except maybe for RepLogCreator, which could be used to edit rep logs.

But, there are a lot of nice use-cases for building truly, re-usable components, basically, tools. For example, it's pretty simple, but, suppose we had a lot of Bootstrap submit buttons and we want to turn the submit button into its own React component. We can totally do that, and it's pretty awesome.

In the assets/js directory, create a new directory called components/ and inside, a new file called Button.js. I'm not putting this in RepLog because this could be used on other parts of the site.

This will be a dumb, presentation component. So, copy the import lines from the top of RepLogCreator, and then say export default function Button with the normal props argument. Inside, return the markup for a button with className="btn", because every button at least has that class.

import React from 'react';
import PropTypes from 'prop-types';
export default function Button(props) {
return (
<button
className="btn"
... lines 8 - 9
);
}
... lines 12 - 16

Spreading the Props

Go back to RepLogCreator and scroll down. Ok, this button has type="submit". We could add that to our Button component, but not all buttons need this. But, no worries: we can allow this to be passed in as a prop! In fact, we might need to pass a bunch of different attributes as props.

To allow that, use the attribute spread operator ...props. It's simple: any prop passed to this component will be rendered as an attribute. And for the text, hmm: how about a prop called text: props.text. Close the button tag. At the bottom, add Button.propTypes = and define text as a string that's required.

... lines 1 - 3
export default function Button(props) {
return (
<button
... line 7
{...props}
>{props.text}</button>
);
}
... line 12
Button.propTypes = {
text: PropTypes.string.isRequired
};

Perfect!

Back in RepLogCreator, head up top and bring this in: import Button from ./Components/Button.

... lines 1 - 2
import Button from '../Components/Button';
... lines 4 - 100

Then all the way down at the bottom, use <Button type="submit" /> and also the text prop. Copy the original text and paste it here.

... lines 1 - 46
render() {
... lines 48 - 50
return (
... lines 52 - 88
<Button type="submit" text="I Lifted it!" />
... line 90
);
}
... lines 93 - 100

We are going to temporarily lose the btn-primary class. That is a problem, and we'll fix it soon. Delete the old button. This should work! Move over and refresh! There it is! The button has the class, type="submit" and the text. Hmm, but it also has a text= attribute... which makes perfect sense: we added that as a prop! Of course, we don't actually want that attribute, so we'll need to fix that.

Using props.children

But first, we have a bigger problem! What if I wanted to add a Font Awesome icon to the text? Normally we would add a <span className=""> and then the classes. But... this doesn't look right: I'm putting HTML inside of this string. And, actually, this wouldn't even work, because React escapes HTML tags in strings.

New idea: what if we could remove this text prop and treat the Button like a true HTML element by putting the text inside. That looks awesome. This is not only possible, this is ideal! By doing it this way, we can include text, HTML elements or even other React components! Woh!

If you pass something in the body of a Component, that is known as its children, and you can access it automatically with props.children. It's that simple.

... lines 1 - 4
return (
<button
... lines 7 - 8
>{props.children}</button>
);
... lines 11 - 12

Oh, and ESLint is angry because we're missing props validation for children. I'm going to ignore that because the children prop is a special case and, we don't really care of its text, a component or something else. But, you could add it with the PropTypes "any" type.

Remove the propTypes for now. Let's try it! Move over and refresh! Looking good. To prove that using children is awesome, add a new <span> with className="fa fa-plus-circle".

... lines 1 - 88
<Button type="submit">
I Lifted it <span className="fa fa-plus-circle"></span>
</Button>
... lines 92 - 102

Go check that out. Nice!

Merging Prop Values in your Component

Ok, we're still missing the btn-primary class. This is a bit trickier. We can't just pass a className prop here, because, in the Button, we're already passing a className prop. So, let's just be smarter! Enter JavaScript, add ticks, then print btn, then ${props.className}.

... lines 1 - 4
return (
<button
className={`btn ${props.className}`}
... lines 8 - 9
);
... lines 11 - 19

That should do it! We're not passing this prop yet, but try it: refresh. Oh man, undefined! Of course! Let's go clean things up.

First, add Button.propTypes to advertise that we accept a className prop that's a string. We could make this required... or we can allow it to be optional, but fix that undefined problem. To do that, set Button.defaultProps to an object with className set to empty quotes.

... lines 1 - 11
Button.propTypes = {
className: PropTypes.string
};
Button.defaultProps = {
className: ''
};

Problem solved! Try it again. Wait! What? Now the class attribute is empty? How is that even possible? To see why, go back to RepLogCreator and pass a className prop here: btn-primary.

... lines 1 - 88
<Button type="submit" className="btn-primary">
... lines 90 - 102

Go refresh again. Huh, now it has that class... but not the btn class. Here's the deal: sure, we have this className prop here. But, thanks to the ...props, the className prop we're passing in overrides the first one! We could move the ...props first, but, in general, we do want to allow whoever uses this component to override its props.

So, hmm: we basically want to print all of the props here... except for className. We can do that, and it's cool! Up top, let's destructure: const {} = props, then get out className. Use that below.

Then - this is the cool part - destructure a second variable, ...otherProps. Use that below in the button.

... lines 1 - 4
const { className, ...otherProps } = props;
... line 6
return (
<button
className={`btn ${className}`}
... lines 10 - 11
);
... lines 13 - 21

Yep, the ...otherProps will be all of the props, except for any that we specifically destructured before it. It's an awesome little trick.

Ok, try it out: move over, refresh and... we got it! It looks perfect! I hope this tiny component gets you excited about the possibilities of reusing code with React.

Leave a comment!

12
Login or Register to join the conversation

Hey Guys,

Is there a way to deal with phpstorm and the ESLint: 'children' is missing in props validation?

Reply

Hey Skylar!

Yes...ish - I checked into this too, because it also bothered me. Basically, as far as I can tell, if you care enough, you simply *do* need to manually define it as a valid prop - e.g. https://github.com/yannickc... - here's even more background - https://github.com/yannickc...

Cheers!

Reply

Will there be a problem if I add this to the Button.propTypes?

Button.propTypes = {<br /> <b>children: PropTypes.node.isRequired,</b><br /> className: PropTypes.string<br />};

Reply

I don't think there should be a problem unless the Button component doesn't receive any children passed in

Reply
Graymath technology Avatar
Graymath technology Avatar Graymath technology | posted 4 years ago

Just wanted to verify so React is intelligent enough not to render children as attributes. Since although we are getting the attributes from {otherProps} the children props didn't get set

Reply

Hey Graymath technology

So, React excludes the children props of a component from props field by default?

Cheers!

1 Reply
Graymath technology Avatar
Graymath technology Avatar Graymath technology | MolloKhan | posted 4 years ago

What I meant was that the code below
<button classname="{`btn" ${classname}`}="" {...otherprops}="">{props.children}</button>
when I inspect this section in browser I don't see any of the child attributes populated in front of the class className attribute. But if I check otherProps in console I can see the children key is set on it

Reply

Ohh, so as you said, React detects that you are passing as an attribute the "children" property and it just ignores it. Right? :)

Reply
GDIBass Avatar
GDIBass Avatar GDIBass | posted 5 years ago

...otherProps

Woah, that's super nifty. Yay destructuring!

Reply

Haha, right!? :D

1 Reply
Amy anuszewski Avatar
Amy anuszewski Avatar Amy anuszewski | posted 5 years ago

What would best practices be for source control for my re-usable components? Lets say I want to package my components so they could be used in other projects and by other developers?

Reply

Hey Amy anuszewski!

That's a great question! And when you start wanting to allow your components to be re-usable, several things change :). Most importantly, if you want to share your components, they're should be distributed as an node package. Npm - the registry node packages - does have paid accounts for private packages.

Typically, this also means that the components you want to reuse will need to live in their own git repository. However, I don't believe that's *required*. Especially when you first start developing something that will be reused later, it's a pain to store them in a separate repository, because you're probably hacking quickly on both those components and the first application that will use them. Having 2 separate repositories is clean, but can cause that initial building to slow things down. So, with npm, another option is that you *can* continue to store your reusable package inside your project... just in their own directory. When you publish your npm package (out of scope here, but I know a few things about this), you can literally just publish that *one* directory to npm. Suddenly, you only have 1 repository, but you have successfully published one of its directories as a private npm package. Other developers would install & use your package like normal.

If you have any other questions, I'd be happy to help answer them :).

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