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
Fork
this repository.clone
this repo onto your computer.cd
into this repository and runnpm i
.- if you did that successfully you will see a
node_modules
folder in the same directory as thesrc
andpublic
folder.
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
components
folder inside thesrc
folder.
- Create
Product.js
inside thesrc/components
folder. - inside the
Product.js
file make a functional component with same name as the file. - Use the values off of the
props
object 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
img
element for the product'simageUrl
. - Use a
h4
element for the product'stitle
. - Use a
p
element for the product'sdescription
. - Use a
p
element for the product'sprice
. - Use a
button
element that says "Add to Cart":- Add an
onClick
handler that callsaddToCart
off 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
Product
component. - Scroll down to the
products section
:- Replace the current map's
JSX
with rendering aProduct component
. - Remember to pass down an
item
andaddToCart prop
.
- Replace the current map's
- Bind the correct context of
this
toaddToCart
insrc/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
props
object 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.js
inside thesrc/components
folder. -
inside the
CartItem.js
file make a functional component with same name as the file. -
Use the values off of the
props
object 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
img
element for theproduct
'simageUrl
. - Use a
h4
element for theproduct
'stitle
. - Use a
p
element for theproduct
'sdescription
. - Use a
p
element 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
JSX
with 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-types
library by runningnpm install prop-types
.
- Open
src/components/Product.js
. - import the
prop-types
library. - Define the component
propTypes
after the ending curly brace of the functional component definition. - Provide the appropriate propTypes for the
item
prop being passed into theProduct
component.
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-types
library. - Define the component
propTypes
after the ending curly brace of the functional component definition. - Provide the appropriate propTypes for the
item
prop being passed into theCartItem
component.
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
Text
functional component
insrc/components
. - The
Text
component 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
propTypes
to theText
component. - Replace all instances of
h4
andp
tags 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.js
calleddeleteFromCart
. - 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
setState
with 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
CartItem
component.
- Open
src/components/CartItem
. - Add a button that says
remove from cart
that 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.js
calledhandleToggleView
.- 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
Product
component. - on one of the divs inside the
Product.js
you will need to turn theclassName
into 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
onChange
event 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.