For this project, we will be using the same project from the previous day lecture, and we will be refactoring our code to take advantage of some of the tools that React has to offer. If you want to use your previous project you can do so and just follow the readme on this page (no need to clone this repo), but it is expected that you finished parts 1 and 2 of Day 1. If you didn't get that far or just want to start fresh we have provided code that will put you at the correct starting point, just follow the instructions below for cloning the project.
In this project we will be using reusable functional components to follow the
DRY principle, which is, don't repeat yourself. We will pass props into our
reusable components. At the end of this project, you should have a better
understanding of the following concepts:
- Props
- PropTypes
- Functional Components
- Reusable Components
Forkthis repository.clonethis repo onto your computer.cdinto this repository and runnpm i.- if you did that successfully you will see a
node_modulesfolder in the same directory as thesrcandpublicfolder.
In this part we will create two functional components so that we don't have to
repeat our JSX for a product in the products section and cart section.
We'll make use of props to pass in a product to render.
- Create a
componentsfolder inside thesrcfolder.
- Create
Product.jsinside thesrc/componentsfolder. - inside the
Product.jsfile make a functional component with same name as the file. - Use the values off of the
propsobject passed into the function to render a product.- All the properties about the product will be on props.item.
property, it may be a good idea to destructure those values. - (At this moment we are not passing down props but we will in the next step):
- Use an
imgelement for the product'simageUrl. - Use a
h4element for the product'stitle. - Use a
pelement for the product'sdescription. - Use a
pelement for the product'sprice. - Use a
buttonelement that says "Add to Cart":- Add an
onClickhandler that callsaddToCartoff ofprops. - Remember to pass in the product as an
argument.
- Add an
- Use an
- All the properties about the product will be on props.item.
The JSX for src/components/Product.js is almost identical to the map in
src/App.js
Detailed Instructions
Let's begin by creating a new file called Product.js inside of the
src/components folder and create a functional component called Product
inside of it. Make sure it includes props in its parameters.
import React from 'react';
export default function Product(props) {}Now that we have our functional component we are going to make some
assumptions here. This component should expect to receive two props. One
prop called item which will be a product object and another prop called
addToCart which will be the addToCart method from src/App.js. With these
assumptions, let's start by destructuring them off of the props argument.
import React from 'react';
export default function Product(props) {
const { item, addToCart } = props;
}With those assumptions out of the way, the JSX we need is almost exactly the
same as the JSX we are already using in src/App.js. Let's move that JSX
over into the component to start.
import React from 'react';
export default function Product(props) {
const { item, addToCart } = props;
return (
<div key={item.id} className="product">
<img src={item.imageUrl} />
<div className="product-info">
<h4>{item.title}</h4>
<p>{item.description}</p>
<p>{item.price}</p>
<button onClick={() => this.addToCart(item)}>Add to Cart</button>
</div>
</div>
);
}Now we can start taking away the pieces of code that don't make sense in this
file. For starters, we no longer need a key on our most parent div because
we aren't executing a map inside of src/components/Product.js. We can also
strip away the this from this.addToCart(item) since that method is now being
passed down as a prop.
import React from 'react';
export default function Product(props) {
const { item, addToCart } = props;
return (
<div class="product">
<img src={item.imageUrl} />
<div className="product-info">
<h4>{item.title}</h4>
<p>{item.description}</p>
<p>{item.price}</p>
<button onClick={() => addToCart(item)}>Add to Cart</button>
</div>
</div>
);
} src/components/Product.js
import React from 'react';
export default function Product(props) {
const { item, addToCart } = props;
return (
<div class="product">
<img src={item.imageUrl} />
<div className="product-info">
<h4>{item.title}</h4>
<p>{item.description}</p>
<p>{item.price}</p>
<button onClick={() => addToCart(item)}>Add to Cart</button>
</div>
</div>
);
}- Open
src/App.js. - Import the
Productcomponent. - Scroll down to the
products section:- Replace the current map's
JSXwith rendering aProduct component. - Remember to pass down an
itemandaddToCart prop.
- Replace the current map's
- Bind the correct context of
thistoaddToCartinsrc/App.js.
Detailed Instructions
Let's begin by opening src/App.js and import the Product component.
import React, { Component } from "react";
import Product from "./components/Product";
import "./App.css";
export default class App extends Component {
...Now that we have access to the Product component, we can replace the JSX in
the map for our products section. Remember that the Product component is
expecting an item and addToCart prop. Also, we will still need to use a
key prop here since we are inside a map.
<section className="products">
<h1>Products</h1>
<h2>Hats</h2>
{this.state.hats.map(item => (
<Product key={item.id} item={item} addToCart={this.addToCart} />
))}
<h2>Beach Gear</h2>
{this.state.beachGear.map(item => (
<Product key={item.id} item={item} addToCart={this.addToCart} />
))}
</section>Lastly, we'll need to fix the context of this for the addToCart method. We
can either bind it in the constructor method, use an arrow function, or turn
the addToCart method into an arrow function.
addToCart = item => {
this.setState({
cart: [...this.state.cart, item],
});
}; src/App.js
import React, { Component } from 'react';
import Product from './components/Product';
import './App.css';
export default class App extends Component {
constructor() {
super();
this.state = {
cart: [],
hats: [
{
id: 1,
title: "Fisherman's Hat",
description:
'Headgear commonly used by fishermen. Increases fishing skill marginally.',
price: 12.99,
imageUrl: 'https://via.placeholder.com/150x150',
},
{
id: 2,
title: 'Metal Hat',
description: 'Uncomfortable, but sturdy.',
price: 8.99,
imageUrl: 'https://via.placeholder.com/150x150',
},
],
beachGear: [
{
id: 3,
title: 'Tent',
description: 'Portable shelter.',
price: 32.99,
imageUrl: 'https://via.placeholder.com/150x150',
},
],
};
}
addToCart = item => {
this.setState({
cart: [...this.state.cart, item],
});
};
checkout = () => {
this.setState({ cart: [] });
alert('Purchase is complete!');
};
render() {
return (
<div className="App">
<section className="products">
<h1>Products</h1>
<h2>Hats</h2>
{this.state.hats.map(item => (
<Product key={item.id} item={item} addToCart={this.addToCart} />
))}
<h2>Beach Gear</h2>
{this.state.beachGear.map(item => (
<Product key={item.id} item={item} addToCart={this.addToCart} />
))}
</section>
<section className="cart">
<h1>Cart</h1>
<h2>
Total: $
{this.state.cart.reduce(
(totalPrice, product) => (totalPrice += product.price),
0
)}
</h2>
<button onClick={this.checkout}>Checkout</button>
{this.state.cart.map(item => (
<div key={item.id} className="product">
<img src={item.imageUrl} />
<div className="product-info">
<h4>{item.title}</h4>
<p>{item.description}</p>
<p>{item.price}</p>
</div>
</div>
))}
</section>
</div>
);
}
}-
Use the values off of the
propsobject passed into the function to render a product.- All the properties about the product will be on props.item.
property, it may be a good idea to destructure those values. - (At this moment we are not passing down props but we will in the next step):
- All the properties about the product will be on props.item.
-
Create
CartItem.jsinside thesrc/componentsfolder. -
inside the
CartItem.jsfile make a functional component with same name as the file. -
Use the values off of the
propsobject passed into the function to render a product.- All the properties about the product will be on props.item.
property, it may be a good idea to destructure those values. - (At this moment we are not passing down props but we will in the next step):
- Use an
imgelement for theproduct'simageUrl. - Use a
h4element for theproduct'stitle. - Use a
pelement for theproduct'sdescription. - Use a
pelement for theproduct'sprice.
- Use an
- All the properties about the product will be on props.item.
-
Open
src/App.js. -
Import the
CartItem component. -
Scroll down to the
cart section:- Replace the current map's
JSXwith rendering aCartItem component. - Remember to pass down an
item prop.
- Replace the current map's
Detailed Instructions
Let's begin by creating a new file called CartItem.js inside of the
src/components folder and create a functional component called CartItem
inside of it.
import React from 'react';
export default function CartItem(props) {}Just like we did earlier, we'll destructure item off of props and then
render the JSX from the cart section's map in src/App.js. We'll then
strip away the key prop since we are not mapping inside of CartItem.
import React from 'react';
export default function Product(props) {
const { item } = props;
return (
<div class="product">
<img src={item.imageUrl} />
<div className="product-info">
<h4>{item.title}</h4>
<p>{item.description}</p>
<p>{item.price}</p>
</div>
</div>
);
}Now that our CartItem component is ready, let's open src/App.js and import
it.
import React, { Component } from "react";
import Product from "./components/Product";
import CartItem from "./components/CartItem";
import "./App.css";
export default class App extends Component {
...Now that we have access to the CartItem component, we can replace the JSX in
the map for our cart section. Remember that the CartItem component is
expecting an item prop. Also, we will still need to use a key prop here
since we are inside a map.
<section className="cart">
<h1>Cart</h1>
<h2>
Total: $
{this.state.cart.reduce(
(totalPrice, product) => (totalPrice += product.price),
0
)}
</h2>
<button onClick={this.checkout}>Checkout</button>
{this.state.cart.map(item => (
<CartItem key={item.id} item={item} />
))}
</section> src/CartItem.js
import React from 'react';
export default function CartItem(props) {
const { item } = props;
return (
<div class="product">
<img src={item.imageUrl} />
<div className="product-info">
<h4>{item.title}</h4>
<p>{item.description}</p>
<p>{item.price}</p>
</div>
</div>
);
} src/App.js
import React, { Component } from 'react';
import Product from './components/Product';
import CartItem from './components/CartItem';
import './App.css';
export default class App extends Component {
constructor() {
super();
this.state = {
cart: [],
hats: [
{
id: 1,
title: "Fisherman's Hat",
description:
'Headgear commonly used by fishermen. Increases fishing skill marginally.',
price: 12.99,
imageUrl: 'https://via.placeholder.com/150x150',
},
{
id: 2,
title: 'Metal Hat',
description: 'Uncomfortable, but sturdy.',
price: 8.99,
imageUrl: 'https://via.placeholder.com/150x150',
},
],
beachGear: [
{
id: 3,
title: 'Tent',
description: 'Portable shelter.',
price: 32.99,
imageUrl: 'https://via.placeholder.com/150x150',
},
],
};
}
addToCart = item => {
this.setState({
cart: [...this.state.cart, item],
});
};
checkout = () => {
this.setState({ cart: [] });
alert('Purchase is complete!');
};
render() {
return (
<div className="App">
<section className="products">
<h1>Products</h1>
<h2>Hats</h2>
{this.state.hats.map(item => (
<Product key={item.id} item={item} addToCart={this.addToCart} />
))}
<h2>Beach Gear</h2>
{this.state.beachGear.map(item => (
<Product key={item.id} item={item} addToCart={this.addToCart} />
))}
</section>
<section className="cart">
<h1>Cart</h1>
<h2>
Total: $
{this.state.cart.reduce(
(totalPrice, product) => (totalPrice += product.price),
0
)}
</h2>
<button onClick={this.checkout}>Checkout</button>
{this.state.cart.map(item => (
<CartItem key={item.id} item={item} />
))}
</section>
</div>
);
}
}In this part we will start using the prop-types library to provide better
documentation and an enhanced debugging experience to the CartItem and
Product components. We will also create a new Text component that will
replace our our <h4> and <p> tags based off of the props that it receives.
- Install the
prop-typeslibrary by runningnpm install prop-types.
- Open
src/components/Product.js. - import the
prop-typeslibrary. - Define the component
propTypesafter the ending curly brace of the functional component definition. - Provide the appropriate propTypes for the
itemprop being passed into theProductcomponent.
Detailed Instructions
Lets begin by opening src/components/Product.js and importing prop-types at
the top of the file.
import React from 'react';
import PropTypes from 'prop-types';We will then scroll to the bottom of the file and define the propTypes object
for this component after the closing curly brace of the functional component.
This component is receiving two props: item which is an Object that has
multiple product properties on it, and addToCart which is a function. We can
specify the required properties for the item object using the PropTypes.shape
method and defining each required property of the item object and the
corresponding data type for each of those properties. Each item has the
following properties with their associated data type: id-Number,
title-String, description-String, price-Number, imageUrl-String. Then we
want to add the addToCart function, it too is required. We can mark each
property on the object as required so that we will be warned if any of those
properties are missing.
Product.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
imageUrl: PropTypes.string.isRequired,
}),
addToCart: PropTypes.func.isRequired,
};- Open
src/components/CartItem.js. - import the
prop-typeslibrary. - Define the component
propTypesafter the ending curly brace of the functional component definition. - Provide the appropriate propTypes for the
itemprop being passed into theCartItemcomponent.
Detailed Instructions
Now we will basically repeat the process of defining the propTypes in the
Product component in the CartItem component. Lets begin by opening
src/CartItem.js and importing prop-types at the top of the file.
import React from 'react';
import PropTypes from 'prop-types';We will then scroll to the bottom of the file and define the propTypes object
for this component after the closing curly brace of the functional component.
This component is receiving a single prop: item which is an Object that has
multiple product properties on it. We can specify the required properties for
this object using the PropTypes.shape method and defining each required
property of the item object and the corresponding data type for each of those
properties. Since the items in the cart have the same data as the list of
available products, the propTypes for this component will look almost
identical to the Product component. Each item has the following properties
with their associated data type: id-Number, title-String,
description-String, price-Number, imageUrl-String. We can mark each
property on the object as required so that we will be warned if any of those
properties are missing.
CartItem.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
imageUrl: PropTypes.string.isRequired,
}),
};- Create a new
Textfunctional componentinsrc/components. - The
Textcomponent should receive twoprops.isHeader- A boolean based off of whether the text is a header or not.text- The string value of the text to be rendered.
- Add the appropriate
propTypesto theTextcomponent. - Replace all instances of
h4andptags with our new text component with the appropriateprops.
Detailed Instructions
Lets start by creating a new Text.js file inside of our src/components
folder. Create a functional component inside our newly created file. This
Text component should receive two props: isHeader(boolean) and
text(string or number). We can then destructure those props after the function
declaration as a clean way to use those props as variables.
import React from 'react';
export default function Text(props) {
const { text, isHeader } = props;
}We can then add some logic to render either an h4 or a p tag based off of
the value of the isHeader prop passed in. A great way to do this is by using
the ternary operator to determine which JSX to render. We can add the return
statement right next to the ternary so that the component renders the resulting
value from the ternary.
import React from 'react';
import propTypes from 'prop-types';
export default function Text(props) {
const { text, isHeader } = props;
return isHeader ? <h4>{text}</h4> : <p>{text}</p>;
}Since this component is receiving props, we should add propTypes to improve
the documentation and debugging experience of this component. As a reminder,
this component is receiving two props: isHeader(boolean), and text(string).
Both props are required. Dont forget to import the prop-types library. Since
the text prop could be a string in the case of a title or description, or a
number in the case of a price, we will need to use the PropTypes.oneOfType
method. This method takes an array of acceptable propTypes for that specific
prop.
import React from 'react';
import PropTypes from 'prop-types';
export default function Text(props) {
const { text, isHeader } = props;
return isHeader ? <h4>{text}</h4> : <p>{text}</p>;
}
Text.propTypes = {
text: PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.number.isRequired,
]),
isHeader: PropTypes.bool.isRequired,
};We can now use our new Text component inside the CartItem and Product
components. Open src/components/CartItem.js, import the Text component after
the other import.
import React from 'react';
import Text from './Text';We can now replace any h4 or p tag with our newly imported Text component.
For any h4 tag, we want to set the isHeader prop to true, otherwise we want
the isHeader prop to be set to false. We also want to set the information in
between the h4 or p tags to the text prop.
import React from 'react';
import Text from './Text';
import PropTypes from 'prop-types';
export default function CartItem(props) {
const { item } = props;
return (
<div className="product">
<img src={item.imageUrl} />
<div className="product-info">
<Text isHeader={true} text={item.title} />
<Text isHeader={false} text={item.description} />
<Text isHeader={false} text={item.price} />
</div>
</div>
);
}
CartItem.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
imageUrl: PropTypes.string.isRequired,
}),
};We will now repeat this process in the Product component.
import React from 'react';
import PropTypes from 'prop-types';
import Text from './Text';
export default function Product(props) {
const { item, addToCart } = props;
return (
<div className="product">
<img src={item.imageUrl} />
<div className="product-info">
<Text isHeader={true} text={item.title} />
<Text isHeader={false} text={item.description} />
<Text isHeader={false} text={item.price} />
<button onClick={() => addToCart(item)}>Add to Cart</button>
</div>
</div>
);
}
Product.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
imageUrl: PropTypes.string.isRequired,
}),
addToCart: PropTypes.func.isRequired,
}; src/components/Product.js
import React from 'react';
import PropTypes from 'prop-types';
import Text from './Text';
export default function Product(props) {
const { item, addToCart } = props;
return (
<div className="product">
<img src={item.imageUrl} />
<div className="product-info">
<Text isHeader={true} text={item.title} />
<Text isHeader={false} text={item.description} />
<Text isHeader={false} text={item.price} />
<button onClick={() => addToCart(item)}>Add to Cart</button>
</div>
</div>
);
}
Product.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
imageUrl: PropTypes.string.isRequired,
}),
addToCart: PropTypes.func.isRequired,
}; src/components/CartItem.js
import React from 'react';
import Text from './Text';
import PropTypes from 'prop-types';
export default function CartItem(props) {
const { item } = props;
return (
<div className="product">
<img src={item.imageUrl} />
<div className="product-info">
<Text isHeader={true} text={item.title} />
<Text isHeader={false} text={item.description} />
<Text isHeader={false} text={item.price} />
</div>
</div>
);
}
CartItem.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
imageUrl: PropTypes.string.isRequired,
}),
}; src/components/Text.js
import React from 'react';
import PropTypes from 'prop-types';
export default function Text(props) {
const { text, isHeader } = props;
return isHeader ? <h4>{text}</h4> : <p>{text}</p>;
}
Text.propTypes = {
text: PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.number.isRequired,
]),
isHeader: PropTypes.bool.isRequired,
};In this part we will add in the remove from cart function to the CartItem
component. We will also need to add onto the prop types object to account for
this. We will also add in the functionality to change the view of each card.
- Open
src/App.js. - If you have not already then you will need to create a method on
App.jscalleddeleteFromCart. - This Method will take in an ID of the item you want to remove from the cart.
- Have it remove the item from state then
setStatewith the new array. - Because we are moving this method to another component make sure you bind it.
- Then pass it down as a prop to the
CartItemcomponent.
- Open
src/components/CartItem. - Add a button that says
remove from cartthat when clicked runs the deleteFromCart prop. - Also make sure to add it to your propType object at the bottom of the file.
- Open
src/App.js. - If you have not already then you will need to create a method on
App.jscalledhandleToggleView.- this function will change the way the product cards are displayed by toggling a boolean on state.
- then you will need to pass that value down as a prop into each
Productcomponent. - on one of the divs inside the
Product.jsyou will need to turn theclassNameinto a ternary.- have it toggle between two different class names to based on the value passed down by props.
- Don't forget to also add the new prop to the
Prop.Types
In this part we will add a search bar which can filter the list of products. We will also add a navbar at the top of the app that can toggle between product and cart view.
- Open
src/App.js - In the products section under the header in App.js, create an input that will
be our search bar.
- Store its value on state.
- Create an
onChangeevent listener that will update state with the user input.
- Change the code where we map over products to display so that we are filtering based on the user input string if the user has typed anything.
- Add a navbar with a button at the top of the
src/App.js. - The user should be able to toggle between Product View and Cart View, by changing a boolean value on state.
- Use conditional rendering (with the ternary operator), so that only one view is displayed at a time.


