We're going to build a burger application so an real application where we can dynamically build a burger, add ingredients and then in the end of course also purchase it. We'll start with the planning phase. But how do we plan a React application?
How do we plan a React application?
Here are three very important steps (but we can split these up into more granular steps).
- Component Tree / Component Structure
- Application State (Data)
- Components vs Containers
(1) It's super important to have an idea about what should go into its own component and what not. (2) We can also translate this with the data we plan on using and manipulating in our application. For example, in our burger application, we need to keep track about the ingredients a user added because that will determine what we need to render and also what the user needs to pay in the end. (3) Which components in our application should be stateless (= dumb components) so basically components that are functional and don't use hooks or even class-based components that don't use state. And which components have to be stateful components, so either functional components using their useState hook or class-based components using the state property.
Application layout:
Component tree:
What should be a stateless component and what should be a stateful component.
We should manage the state in the BurgerBuilder
component (and not the App
component). The state we just listed is really just related to building a burger. So the burger builder should be a stateful component and the other pages should be stateless.
We're going to use a styling solution named "CSS modules". In more recent project versions created by CRA, support for CSS modules is already built-in and we can use that feature without ejecting, here is more information.
yarn install
npm run eject
// config/webpack.config.dev.js
//...
{
test: /\.css$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: true, // ADD
localIdentName: '[name]__[local]__[hash:base64:5]', // ADD
},
},
//...
//...
// config/webpack.config.prod.js
//...
{
test: /\.css$/,
loader: ExtractTextPlugin.extract(
Object.assign(
{
fallback: {
loader: require.resolve('style-loader'),
options: {
hmr: false,
},
},
use: [
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
minimize: true,
sourceMap: shouldUseSourceMap,
modules: true, // ADD
localIdentName: '[name]__[local]__[hash:base64:5]', // ADD
},
},
//...
//...
//...
//...
//...
Let's create 2 sub-folders: /components
and /containers
. containers are stateful components (= components created with the class keyword or functional components using useState
and components going into the components folder are dumb or presentational components that don't manage state.
// src/hoc/Aux.js
const aux = (props) => props.children;
export default aux;
// src/components/Layout/Layout.js
import React from 'react';
import Aux from '../../hoc/Aux';
const Layout = (props) => {
return (
<Aux>
<div>Toolbar, SideDrawer, Backdrop</div>
<main>{props.children}</main>
</Aux>
);
};
export default Layout;
// src/App.js
import React, { Component } from 'react';
import Layout from './components/Layout/Layout';
class App extends Component {
render() {
return (
<Layout>
<p>Test</p>
</Layout>
);
}
}
export default App;
// src/containers/BurgerBuilder/BurgerBuilder.js
import React, { Component } from 'react';
import Aux from '../../hoc/Aux';
export class BurgerBuilder extends Component {
render() {
return (
<Aux>
<div>Burger</div>
<div>Build Controls</div>
</Aux>
);
}
}
export default BurgerBuilder;
/* src/components/Layout/Layout.css */
.Content {
margin-top: 16px;
}
// src/components/Layout/Layout.js
import React from 'react';
import Aux from '../../hoc/Aux';
import classes from './Layout.css';
const Layout = (props) => {
return (
<Aux>
<div>Toolbar, SideDrawer, Backdrop</div>
<main className={classes.Content}>{props.children}</main>
</Aux>
);
};
export default Layout;
It can be hard to manage all the components. A good way of structuring our project, in React to create granular components and not big chunks (it is a good practice). Now if we have a lot of components, we have a lot of files and to keep these files manageable, we want to create a folder structure which is not only divided in components and containers but where inside the components and containers, we also divide it up by feature area so that we quickly know if we need to work on the burger side, we have to go into the burger folder.
// src/components/Burger/BurgerIngredient/BurgerIngredient.js
import React from 'react';
import classes from './BurgerIngredient.css';
const BurgerIngredient = (props) => {
let ingredient = null;
switch (props.type) {
case 'bread-bottom':
ingredient = <div className={classes.BreadBottom}></div>;
break;
case 'bread-top':
ingredient = (
<div className={classes.BreadTop}>
<div className={classes.Seeds1}></div>
<div className={classes.Seeds2}></div>
</div>
);
break;
case 'meat':
ingredient = <div className={classes.Meat}></div>;
break;
case 'cheese':
ingredient = <div className={classes.Cheese}></div>;
break;
case 'salad':
ingredient = <div className={classes.Salad}></div>;
break;
case 'bacon':
ingredient = <div className={classes.Bacon}></div>;
break;
default:
ingredient = null;
break;
}
return ingredient;
};
export default BurgerIngredient;
Note: we have added the css file src/components/Burger/BurgerIngredient/BurgerIngredient.css
.
// src/components/Burger/BurgerIngredient/BurgerIngredient.js
import React from 'react';
import PropTypes from 'prop-types';
import classes from './BurgerIngredient.css';
const BurgerIngredient = (props) => {
//...
};
BurgerIngredient.propTypes = {
type: PropTypes.string.isRequired,
};
export default BurgerIngredient;
// src/components/Burger/Burger.js
import React from 'react';
import BurgerIngredient from './BurgerIngredient/BurgerIngredient';
import classes from './Burger.css';
const Burger = () => {
return (
<div className={classes.Burger}>
<BurgerIngredient type="bread-top" />
<BurgerIngredient type="cheese" />
<BurgerIngredient type="meat" />
<BurgerIngredient type="bread-bottom" />
</div>
);
};
export default Burger;
/* src/components/Burger/Burger.css */
.Burger {
width: 100%;
margin: auto;
height: 250px;
overflow: scroll;
text-align: center;
font-weight: bold;
font-size: 1.2rem;
}
@media (min-width: 500px) and (min-height: 400px) {
.Burger {
width: 350px;
height: 300px;
}
}
@media (min-width: 500px) and (min-height: 401px) {
.Burger {
width: 450px;
height: 400px;
}
}
@media (min-width: 1000px) and (min-height: 700px) {
.Burger {
width: 700px;
height: 600px;
}
}
//...
// state from the BurgerBuilder to the Burger component
state = {
ingredients: {
salad: 1,
bacon: 1,
cheese: 2,
meat: 2,
},
};
//...
// src/components/Burger/Burger.js
import React from 'react';
import BurgerIngredient from './BurgerIngredient/BurgerIngredient';
import classes from './Burger.css';
const Burger = ({ ingredients }) => {
const transformedIngredients = Object.keys(ingredients).map((igKey) => {
return [...Array(ingredients[igKey])].map((_, idx) => {
return <BurgerIngredient key={igKey + idx} type={igKey} />;
});
});
return (
<div className={classes.Burger}>
<BurgerIngredient type="bread-top" />
{transformedIngredients}
<BurgerIngredient type="bread-bottom" />
</div>
);
};
export default Burger;
// src/components/Burger/Burger.js
import React from 'react';
import BurgerIngredient from './BurgerIngredient/BurgerIngredient';
import classes from './Burger.css';
const Burger = ({ ingredients }) => {
let transformedIngredients = Object.keys(ingredients)
.map((igKey) => {
return [...Array(ingredients[igKey])].map((_, idx) => {
return <BurgerIngredient key={igKey + idx} type={igKey} />;
});
})
// before reduce we could have [[], [], [], []] if we pass 0 value to our ingredients
.reduce((acc, cur) => {
return acc.concat(cur);
}, []);
if (!transformedIngredients.length) {
transformedIngredients = <p>Please start adding ingredients!</p>;
}
return (
<div className={classes.Burger}>
<BurgerIngredient type="bread-top" />
{transformedIngredients}
<BurgerIngredient type="bread-bottom" />
</div>
);
};
export default Burger;
// src/components/Burger/BuildControls/BuildControls.js
import React from 'react';
import classes from './BuildControls.css';
const BuildControls = (props) => {
return <div className={classes.BuildControls}></div>;
};
export default BuildControls;
// src/components/Burger/BuildControls/BuildControl/BuildControl.js
import React from 'react';
import classes from './BuildControl.css';
const BuildControl = (props) => {
return (
<div className={classes.BuildControl}>
<div className={classes.Label}>{props.label}</div>
<button className={classes.Less}>Less</button>
<button className={classes.More}>More</button>
</div>
);
};
export default BuildControl;
// src/components/Burger/BuildControls/BuildControls.js
import React from 'react';
import classes from './BuildControls.css';
import BuildControl from './BuildControl/BuildControl';
const controls = [
{ label: 'Salad', type: 'salad' },
{ label: 'Bacon', type: 'bacon' },
{ label: 'Cheese', type: 'cheese' },
{ label: 'Meat', type: 'meat' },
];
const BuildControls = (props) => {
return (
<div className={classes.BuildControls}>
{controls.map((control) => (
<BuildControl key={control.label} label={control.label} />
))}
</div>
);
};
export default BuildControls;
// src/containers/BurgerBuilder/BurgerBuilder.js
import React, { Component } from 'react';
import Aux from '../../hoc/Aux';
import Burger from '../../components/Burger/Burger';
import BuildControls from '../../components/Burger/BuildControls/BuildControls';
const INGREDIENT_PRICE = {
salad: 0.5,
bacon: 0.7,
cheese: 0.4,
meat: 1.3,
};
export class BurgerBuilder extends Component {
state = {
ingredients: {
salad: 1,
bacon: 1,
cheese: 2,
meat: 2,
},
totalPrice: 4, // base price $4
};
addIngredientHandler = (type) => {
const oldCount = this.state.ingredients[type];
const updatedCounted = oldCount + 1;
const updatedIngredients = {
...this.state.ingredients,
};
updatedIngredients[type] = updatedCounted;
const priceAddition = INGREDIENT_PRICE[type];
const oldPrice = this.state.totalPrice;
const newPrice = oldPrice + priceAddition;
this.setState({ ingredients: updatedIngredients, totalPrice: newPrice });
};
removeIngredientHandler = (type) => {};
render() {
return (
<Aux>
<Burger ingredients={this.state.ingredients} />
<BuildControls ingredientAdded={this.addIngredientHandler} />
</Aux>
);
}
}
export default BurgerBuilder;
// src/components/Burger/BuildControls/BuildControls.js
import React from 'react';
import classes from './BuildControls.css';
import BuildControl from './BuildControl/BuildControl';
const controls = [
{ label: 'Salad', type: 'salad' },
{ label: 'Bacon', type: 'bacon' },
{ label: 'Cheese', type: 'cheese' },
{ label: 'Meat', type: 'meat' },
];
const BuildControls = (props) => {
return (
<div className={classes.BuildControls}>
{controls.map((control) => (
<BuildControl
key={control.label}
label={control.label}
added={() => props.ingredientAdded(control.type)}
/>
))}
</div>
);
};
export default BuildControls;
// src/components/Burger/BuildControls/BuildControl/BuildControl.js
import React from 'react';
import classes from './BuildControl.css';
const BuildControl = (props) => {
return (
<div className={classes.BuildControl}>
<div className={classes.Label}>{props.label}</div>
<button className={classes.Less}>Less</button>
<button onClick={props.added} className={classes.More}>
More
</button>
</div>
);
};
export default BuildControl;
// src/containers/BurgerBuilder/BurgerBuilder.js
//...
render() {
// ADD
const disabledInfo = {
...this.state.ingredients,
};
for (const key in disabledInfo) {
disabledInfo[key] = disabledInfo[key] <= 0;
}
// for example `disabledInfo`: {salad: true, meat: false,...}
return (
<Aux>
<Burger ingredients={this.state.ingredients} />
<BuildControls
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo} // ADD
/>
</Aux>
);
}
//...
// src/components/Burger/BuildControls/BuildControls.js
import React from 'react';
import classes from './BuildControls.css';
import BuildControl from './BuildControl/BuildControl';
//...
const BuildControls = (props) => {
return (
<div className={classes.BuildControls}>
{controls.map((control) => (
<BuildControl
key={control.label}
label={control.label}
added={() => props.ingredientAdded(control.type)}
removed={() => props.ingredientRemoved(control.type)}
disabled={props.disabled[control.type]} // ADD
/>
))}
</div>
);
};
export default BuildControls;
// src/components/Burger/BuildControls/BuildControl/BuildControl.js
import React from 'react';
import classes from './BuildControl.css';
const BuildControl = (props) => {
return (
<div className={classes.BuildControl}>
<div className={classes.Label}>{props.label}</div>
<button
onClick={props.removed}
className={classes.Less}
disabled={props.disabled} // ADD
>
Less
</button>
<button onClick={props.added} className={classes.More}>
More
</button>
</div>
);
};
export default BuildControl;
// src/components/Burger/BuildControls/BuildControls.js
//...
const BuildControls = (props) => {
return (
<div className={classes.BuildControls}>
<p>
Current Price: <strong>{props.price.toFixed(2)}</strong>
</p>
{...}
</div>
);
};
//...
We keep ALL the state in the BurgerBuilder
! Let's add a purchaseable
state which will disable (or not) the "ORDER NOW" button.
// src/containers/BurgerBuilder/BurgerBuilder.js
import React, { Component } from 'react';
import Aux from '../../hoc/Aux';
import Burger from '../../components/Burger/Burger';
import BuildControls from '../../components/Burger/BuildControls/BuildControls';
//...
export class BurgerBuilder extends Component {
state = {
ingredients: {
salad: 0,
bacon: 0,
cheese: 0,
meat: 0,
},
totalPrice: 4,
purchaseable: false, // ADD
};
updatePurchaseState(ingredients) {
// ADD, information from `addIngredientHandler` and `removeIngredientHandler`
const sum = Object.keys(ingredients)
.map((igKey) => ingredients[igKey])
.reduce((acc, cur) => {
return acc + cur;
}, 0);
this.setState({ purchaseable: sum > 0 });
}
addIngredientHandler = (type) => {
//...
const updatedIngredients = {
...this.state.ingredients,
};
updatedIngredients[type] = updatedCounted;
//...
this.setState({ ingredients: updatedIngredients, totalPrice: newPrice });
this.updatePurchaseState(updatedIngredients); // ADD, we needed to pass updatedIngredients otherwise or state wasn't upadted and we had to add 2 ingredients to see the button enable
};
removeIngredientHandler = (type) => {
//...
const updatedIngredients = {
...this.state.ingredients,
};
updatedIngredients[type] = updatedCounted;
//...
this.setState({ ingredients: updatedIngredients, totalPrice: newPrice });
this.updatePurchaseState(updatedIngredients); // ADD
};
render() {
//...
return (
<Aux>
<Burger ingredients={this.state.ingredients} />
<BuildControls
price={this.state.totalPrice}
purchaseable={this.state.purchaseable}
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo} // ADD
/>
</Aux>
);
}
}
export default BurgerBuilder;
We need to disabled={!props.purchaseable}
on the "ORDER NOW" button.
// src/components/Burger/BuildControls/BuildControls.js
import React from 'react';
import classes from './BuildControls.css';
import BuildControl from './BuildControl/BuildControl';
//...
const BuildControls = (props) => {
return (
<div className={classes.BuildControls}>
{...}
<button className={classes.OrderButton} disabled={!props.purchaseable}>
ORDER NOW
</button>
</div>
);
};
export default BuildControls;
We want to make sure that once we click the button, we open a modal with the order summary. We need a modal, we need a backdrop and we need to show some order summary. The idea behind the modal is to have a wrapping element which provides the styling which then simply wraps itself about any content we want to show in that modal.
// src/components/UI/Modal/Modal.js
import React from 'react';
import classes from './Modal.css';
const Modal = (props) => {
return <div className={classes.Modal}>{props.children}</div>;
};
export default Modal;
// src/components/Burger/OrderSummary/OrderSummary.js
import React from 'react';
import Aux from '../../../hoc/Aux';
const OrderSummary = ({ ingredients }) => {
const ingredientSummary = Object.keys(ingredients).map((igKey, idx) => {
return (
<li key={igKey + idx}>
<span style={{ textTransform: 'capitalize' }}>{igKey}</span>:{' '}
{ingredients[igKey]}
</li>
);
});
return (
<Aux>
<h3>Your Order</h3>
<p>Delicious burger with the following ingredients: </p>
<ul>{ingredientSummary}</ul>
<p>Continue to Checkout</p>
</Aux>
);
};
export default OrderSummary;
Now we can pass our OrderSummary
to the Modal in the BurgerBuilder
:
//...
<Modal>
<OrderSummary ingredients={this.state.ingredients} />
</Modal>
//...
// src/containers/BurgerBuilder/BurgerBuilder.js
//...
export class BurgerBuilder extends Component {
state = {
//...
purchaseable: false,
purchasing: false, // ADD
};
//...
purchaseHandler = () => {
this.setState({ purchasing: !this.state.purchasing }); // ADD
};
render() {
//...
return (
<Aux>
<Modal show={this.state.purchasing}>
<OrderSummary ingredients={this.state.ingredients} />
</Modal>
<Burger ingredients={this.state.ingredients} />
<BuildControls
price={this.state.totalPrice}
purchaseable={this.state.purchaseable}
ordered={this.purchaseHandler} // ADD
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo}
/>
</Aux>
);
}
}
export default BurgerBuilder;
Let's make some changes with our modal to create a CSS animation.
// src/components/UI/Modal/Modal.js
import React from 'react';
import classes from './Modal.css';
const Modal = (props) => {
return (
<div
style={{
transform: props.show ? 'translateY(0)' : 'translateY(-100vh)',
opacity: props.show ? 1 : 0,
}}
className={classes.Modal}
>
{props.children}
</div>
);
};
export default Modal;
// src/components/UI/Backdrop/Backdrop.js
import React from 'react';
import classes from './Backdrop.css';
const Backdrop = (props) => {
return props.show ? (
<div className={classes.Backdrop} onClick={props.clicked}></div>
) : null;
};
export default Backdrop;
// src/components/UI/Modal/Modal.js
import React from 'react';
import classes from './Modal.css';
import Aux from '../../../hoc/Aux';
import Backdrop from '../Backdrop/Backdrop';
const Modal = (props) => {
return (
<Aux>
<Backdrop show={props.show} clicked={props.modalClosed} />
<div
style={{
transform: props.show ? 'translateY(0)' : 'translateY(-100vh)',
opacity: props.show ? 1 : 0,
}}
className={classes.Modal}
>
{props.children}
</div>
</Aux>
);
};
export default Modal;
// src/containers/BurgerBuilder/BurgerBuilder.js
//...
return (
<Aux>
<Modal
show={this.state.purchasing}
modalClosed={this.purchaseCancelHandler}
>
<OrderSummary ingredients={this.state.ingredients} />
</Modal>
<Burger ingredients={this.state.ingredients} />
{...}
</Aux>
);
//...
// src/components/UI/Button/Button.js
import React from 'react';
import classes from './Button.css';
const Button = (props) => {
return (
<button
className={[classes.Button, classes[props.btnType]].join(' ')}
onClick={props.clicked}
>
{props.children}
</button>
);
};
export default Button;
/* src/components/UI/Button/Button.css */
.Button {
background-color: transparent;
border: none;
color: white;
outline: none;
cursor: pointer;
font: inherit;
padding: 10px;
margin: 10px;
font-weight: bold;
}
.Button:first-of-type {
margin-left: 0;
padding-left: 0;
}
.Success {
color: #5c9210;
}
.Danger {
color: #944317;
}
// src/components/Burger/OrderSummary/OrderSummary.js
import React from 'react';
import Aux from '../../../hoc/Aux';
import Button from '../../UI/Button/Button';
const OrderSummary = (props) => {
const { ingredients, purchaseCanceled, purchaseContinued } = props; // ADD
const ingredientSummary = Object.keys(ingredients).map((igKey, idx) => {
return (
<li key={igKey + idx}>
<span style={{ textTransform: 'capitalize' }}>{igKey}</span>:{' '}
{ingredients[igKey]}
</li>
);
});
return (
<Aux>
<h3>Your Order</h3>
<p>Delicious burger with the following ingredients: </p>
<ul>{ingredientSummary}</ul>
<p>Continue to Checkout</p>
<Button btnType="Danger" clicked={purchaseCanceled}>
CANCEL
</Button>
<Button btnType="Success" clicked={purchaseContinued}>
CONTINUE
</Button>
</Aux>
);
};
export default OrderSummary;
We just added the price prop
to the OrderSummary. Easy!
// src/components/Navigation/Toolbar/Toolbar.js
import React from 'react';
import classes from './Toolbar.css';
const Toolbar = (props) => {
return (
<header className={classes.Toolbar}>
<div>MENU</div>
<div>LOGO</div>
<nav>
<ul>
<li>XXX</li>
<li>YYY</li>
</ul>
</nav>
</header>
);
};
export default Toolbar;
And let's add the Toolbar
to the Layout
.
// src/components/Layout/Layout.js
import React from 'react';
import Aux from '../../hoc/Aux';
import classes from './Layout.css';
import Toolbar from '../Navigation/Toolbar/Toolbar';
const Layout = (props) => {
return (
<Aux>
<Toolbar />
<main className={classes.Content}>{props.children}</main>
</Aux>
);
};
export default Layout;
// src/components/Logo/Logo.js
import React from 'react';
import burgerLogo from '../../assets/images/burger-logo.png'; // image from /assets folder (Webpack will understand it)
import classes from './Logo.css';
const Logo = () => {
return (
<div className={classes.Logo}>
<img src={burgerLogo} alt="MyBurger" />
</div>
);
};
export default Logo;
// src/components/Navigation/NavigationItems/NavigationItems.js
import React from 'react';
import classes from './NavigationItems.css';
import NavigationItem from './NavigationItem/NavigationItem';
const NavigationItems = () => {
return (
<ul className={classes.NavigationItems}>
<NavigationItem link="/" active>
Burger Builder
</NavigationItem>
<NavigationItem link="/checkout">Checkout</NavigationItem>
</ul>
);
};
export default NavigationItems;
// src/components/Navigation/NavigationItems/NavigationItem/NavigationItem.js
import React from 'react';
import classes from './NavigationItem.css';
const NavigationItem = (props) => {
return (
<li className={classes.NavigationItem}>
<a href={props.link} className={props.active ? classes.active : null}>
{props.children}
</a>
</li>
);
};
export default NavigationItem;
// src/components/Navigation/SideDrawer/SideDrawer.js
import React from 'react';
import Logo from '../../Logo/Logo';
import NavigationItems from '../NavigationItems/NavigationItems';
import classes from './SideDrawer.css';
const SideDrawer = () => {
//...
return (
<div className={classes.SideDrawer}>
<Logo />
<nav>
<NavigationItems />
</nav>
</div>
);
};
export default SideDrawer;
We manage the animation as below:
/* src/components/Navigation/SideDrawer/SideDrawer.css */
.SideDrawer {
position: fixed;
width: 280px;
max-width: 70%;
height: 100%;
left: 0;
top: 0;
z-index: 200;
background-color: white;
padding: 32px 16px;
box-sizing: border-box;
transition: transform 0.3s ease-out;
}
@media (min-width: 500px) {
.SideDrawer {
display: none;
}
}
.Open {
transform: translateX(0);
}
.Close {
transform: translateX(-100%);
}
We add the new SideDrawer
in our Layout
component.
// src/components/Layout/Layout.js
import React from 'react';
import Aux from '../../hoc/Aux';
import classes from './Layout.css';
import Toolbar from '../Navigation/Toolbar/Toolbar';
import SideDrawer from '../Navigation/SideDrawer/SideDrawer';
const Layout = (props) => {
return (
<Aux>
<Toolbar />
<SideDrawer />
<main className={classes.Content}>{props.children}</main>
</Aux>
);
};
export default Layout;
We could add a prop to Logo height="11%"
.
// src/components/Navigation/SideDrawer/SideDrawer.js
//...
const SideDrawer = () => {
//...
return (
<div className={classes.SideDrawer}>
<Logo height="11%" />
<nav>
<NavigationItems />
</nav>
</div>
);
};
export default SideDrawer;
// src/components/Logo/Logo.js
//...
const Logo = (props) => {
return (
<div className={classes.Logo} style={{ height: props.height }}>
<img src={burgerLogo} alt="MyBurger" />
</div>
);
};
export default Logo;
Or another approach, we could wrap Logo
into a div
and add a className={classes.Logo}
. We can define in our SideDrawer.css
:
.Logo {
height: 11%;
}
// src/components/Navigation/SideDrawer/SideDrawer.js
//...
const SideDrawer = () => {
//...
return (
<div className={classes.SideDrawer}>
<div className={classes.Logo}>
<Logo />
</div>
<nav>
<NavigationItems />
</nav>
</div>
);
};
export default SideDrawer;
/* src/components/Navigation/NavigationItems/NavigationItems.css */
.NavigationItems {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-flow: column;
align-items: center;
height: 100%;
}
@media (min-width: 500px) {
.NavigationItems {
flex-flow: row;
}
}
/* src/components/Navigation/Toolbar/Toolbar.css */
@media (max-width: 499px) {
.DesktopOnly {
display: none;
}
}
// src/components/Layout/Layout.js
//...
export class Layout extends Component {
state = {
showSideDrawer: true,
};
sideDrawerClosedHandler = () => {
this.setState({ showSideDrawer: false });
};
render() {
return (
<Aux>
<Toolbar />
<SideDrawer
open={this.state.showSideDrawer}
closed={this.sideDrawerClosedHandler}
/>
<main className={classes.Content}>{this.props.children}</main>
</Aux>
);
}
}
export default Layout;
// src/components/Navigation/SideDrawer/SideDrawer.js
import React from 'react';
import Logo from '../../Logo/Logo';
import NavigationItems from '../NavigationItems/NavigationItems';
import Backdrop from '../../UI/Backdrop/Backdrop';
import Aux from '../../../hoc/Aux';
import classes from './SideDrawer.css';
const SideDrawer = (props) => {
let attachedClasses = [classes.SideDrawer, classes.Close];
if (props.open) {
attachedClasses = [classes.SideDrawer, classes.Open];
}
return (
<Aux>
<Backdrop show={props.open} clicked={props.closed} />
<div className={attachedClasses.join(' ')}>
<div className={classes.Logo}>
<Logo />
</div>
<nav>
<NavigationItems />
</nav>
</div>
</Aux>
);
};
export default SideDrawer;
// src/components/Layout/Layout.js
import React, { Component } from 'react';
import Aux from '../../hoc/Aux';
import classes from './Layout.css';
import Toolbar from '../Navigation/Toolbar/Toolbar';
import SideDrawer from '../Navigation/SideDrawer/SideDrawer';
export class Layout extends Component {
state = {
showSideDrawer: true,
};
sideDrawerClosedHandler = () => {
this.setState({ showSideDrawer: false });
};
sideDrawerToggleHandler = () => {
this.setState((prevState) => {
return { showSideDrawer: !prevState.showSideDrawer };
});
};
render() {
return (
<Aux>
<Toolbar drawerToggleClicked={this.sideDrawerToggleHandler} />
<SideDrawer
open={this.state.showSideDrawer}
closed={this.sideDrawerClosedHandler}
/>
<main className={classes.Content}>{this.props.children}</main>
</Aux>
);
}
}
export default Layout;
// src/components/Navigation/SideDrawer/DrawerToggle/DrawerToggle.js
import React from 'react';
import classes from './DrawerToggle.css';
const DrawerToggle = (props) => {
return (
<div className={classes.DrawerToggle} onClick={props.clicked}>
<div></div>
<div></div>
<div></div>
</div>
);
};
export default DrawerToggle;
/* src/components/Navigation/SideDrawer/DrawerToggle/DrawerToggle.css */
.DrawerToggle {
width: 40px;
height: 100%;
display: flex;
flex-flow: column;
justify-content: space-around;
align-items: center;
padding: 10px 0;
box-sizing: border-box;
cursor: pointer;
}
.DrawerToggle div {
width: 90%;
height: 3px;
background-color: white;
}
@media (min-width: 500px) {
.DrawerToggle {
display: none;
}
}
Let's dive into two important things, prop types and pure components (link with shouldComponentUpdate
). Let's also have a look at all these lifecycle methods we learned about. Do we use them in there? Why don't we use them at any point right now? Where would we use them?
We're not doing any validation in any other component (except BurgerIngredient
) and the reason is, we're not working on a project which is going to get used by other people, we're not working on a third-party library, and we're not even working in a developer team... a lot of the reasons why properties might be used incorrectly aren't relevant here.
Because in the BurgerBuilder
we have the Modal
and the OrderSummary
where we passed prop which can re-render them (without seeing them). We need to be sure to prevent it.
// src/containers/BurgerBuilder/BurgerBuilder.js
//...
render() {
//...
return (
<Aux>
<Modal
show={this.state.purchasing}
modalClosed={this.purchaseCancelHandler}
>
<OrderSummary
price={this.state.totalPrice}
ingredients={this.state.ingredients}
purchaseCanceled={this.purchaseCancelHandler}
purchaseContinued={this.purchaseContinueHandler}
/>
</Modal>
<Burger ingredients={this.state.ingredients} />
<BuildControls
price={this.state.totalPrice}
purchaseable={this.state.purchaseable}
ordered={this.purchaseHandler}
ingredientAdded={this.addIngredientHandler}
ingredientRemoved={this.removeIngredientHandler}
disabled={disabledInfo}
/>
</Aux>
);
}
//...
Let's change the Modal
component and add a shouldComponentUpdate
method to prevent the update if show
don't change.
// src/components/UI/Modal/Modal.js
import React, { Component } from 'react';
import classes from './Modal.css';
import Aux from '../../../hoc/Aux';
import Backdrop from '../Backdrop/Backdrop';
class Modal extends Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.show !== this.props.show;
}
render() {
return (
<Aux>
<Backdrop show={this.props.show} clicked={this.props.modalClosed} />
<div
style={{
transform: this.props.show ? 'translateY(0)' : 'translateY(-100vh)',
opacity: this.props.show ? 1 : 0,
}}
className={classes.Modal}
>
{this.props.children}
</div>
</Aux>
);
}
}
export default Modal;
We definitely improved our application because we make sure that we don't unnecessarily update OrderSummary
. OrderSummary
is included in the BurgerBuilder
but it is not updated because the wrapping element Modal
has a shouldComponentUpdate method where we control this.
We're going to use them, especially when we'll use HTTP requests.