In this project, we'll practice creating a higher order component (HOC). This app will have the same general functionality as the app we will create in the sister project with a render prop. It is important to remember that these are two different patterns to accomplish the same task. In this project we will be building a currency converter that will convert a foreign currency to USD.
In this step, we'll set up our file structure to keep things organized.
- Inside the
src
folder, create aComponents
folder. - Open
src/Components
. Create two new Files:- CurrencyConverter.js
- CurrencyDisplay.js
File Structure
-- src
-- Components
-- CurrencyConverter.js
-- CurrencyDisplay.js
-- App.css
-- App.js
-- index.css
-- index.js
In this step, we'll create the skeleton for our Higher Order Component (HOC).
- Import React in
CurrencyConverter.js
. - Create a function called
withCurrency
that will return a class component calledCurrency
- The
withCurrency
function should take one paramter we will callBaseComponent
Detailed Instructions
Remember, a Higher Order Component (HOC) is just a function that returns a new component. The naming convention of withNAMEofFUNCTION
is common for HOC's.
We first want to create an arrow function called withCurrency
that takes in one parameter, BaseComponent
. The withCurrency
function will return a new class-based component that we will call Currency
const withCurrency = (BaseComponent) => (
class Currency extends Component {...}
)
Give the returned Currency
component a render method and have it return empty parentheses for now. This is where will put some JSX in a bit. The BaseComponent
parameter will be used to hold the template of a component we will pass in once we invoke the function.
src/Components/CurrencyConverter.js
import React, {Component} from 'react'
const withCurrency = (BaseComponent) => (
class Currency extends Component {
render(){
return (
// soon to be jsx
)
}
}
)
In this step, we will create the boilerplate for our CurrencyConverter
. This will include a drop down and buttons to increment and decrement the amount to convert.
- Set intial state for this component. We will need three keys:
currencyChosen : false
,selectedCurrency: 'Select Currency'
andamount: 0
.
const currencyData = [
{ name: 'Japanese Yen', symbol: '¥', rate: 113.6, id: 0 },
{ name: 'British Pound', symbol: '£', rate: 0.77, id: 1 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32, id: 2 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41, id: 3 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01, id: 4 }
]
- We will use the above array,
currencyData
, to map over and dynamically create options inside of a soon to be createdselect
element. - Create a
<select>
element to hold the options created above along with a single default option with a value of 'Select Currency'. - Create two
<button>
elements, one should have+
as it's inner text and the other should be-
. - Below the
button
's display theBaseComponent
parameter like it is a React Component.
Detailed Instructions
- Set some intial state for
Currency
component. We will need acurrencyChosen
which will default tofalse
,selectedCurrency
which will default as 'Select Currency' (spelling and capitalization are important here) and finally anamount
with the default of0
.
const currencyData = [
{ name: 'Japanese Yen', symbol: '¥', rate: 113.6, id: 0 },
{ name: 'British Pound', symbol: '£', rate: 0.77, id: 1 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32, id: 2 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41, id: 3 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01, id: 4 }
]
- Copy the above
currencyData
array inside of thherender()
method but outside of thereturn
inside of theCurrency
component. - Using
.map()
, create an<option>
element for each item of thecurrencyData
array. Each<option>
element should have akey
set to a unique value on the currency object (use theid
), andvalue
set to theindex
the currency occupies in the array, and have the individual currency name as text. Call the new arraycurrencyOptions
. - Create a container div
<div>
. Inside of thediv
create a<select>
element. Set itsvalue
equal to theselectedCurrency
on state. - Inside of the
select
create a single<option>
element with a attribute ofvalue='Select Currency'
and 'Select Curreny' as the inner text. Below that, inside of{}
display thecurrencyOptions
. - Create a new
<div>
to hold buttons that will increment and decrement the currency amount. - Inside of the newly created
div
, create two<button>
elements, one should have+
as it's inner text and the other should be-
. The button with the+
should have a className ofadd
and the button with the-
should have a className ofminus
- Below the
button
's display theBaseComponent
parameter like it is a React Component. TheBaseComponent
will have two props; one calledcurrency
which will useselectedCurrency
from state as the index to select an option from thecurrencyData
array and the other prop will be calledamount
which will be the value of amount on state.
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
const withCurrency = (BaseComponent) =>
class Currency extends Component {
state = {
currencyChosen: false,
selectedCurrency: 'Select Currency',
amount: 0
}
render() {
const currencyData = [
{ name: 'Japanese Yen', symbol: '¥', rate: 113.6, id: 0 },
{ name: 'British Pound', symbol: '£', rate: 0.77, id: 1 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32, id: 2 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41, id: 3 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01, id: 4 }
]
const currencyOptions = currencyData.map((currency, index) => (
<option key={currency.id} value={index}>
{currency.name}
</option>
))
return (
<div>
<select value={this.state.selectedCurrency}>
<option value='Select Currency'>Select Currency</option>
{currencyOptions}
</select>
<div>
<button className='add'>+</button>
<button className='minus'>-</button>
</div>
<BaseComponent
currency={currencyData[this.state.selectedCurrency]}
amount={this.state.amount}
/>
</div>
)
}
}
In this step, we'll create three methods to help us handle user interactions. We will be using the auto-binding (public class field syntax for these methods).
- Using the public class field syntax, create a method that will increment the count of amount on state. Use
setState
via the callback function syntax. Call this methodhandleAmountIncrease
. - Using the public class field syntax, create a method that will decrement the count of amount on state. Use
setState
via the callback function syntax. Call this methodhandleAmountDecrease
. - Using the public class field syntax, create a method that will handle the users selection from the dropdown. You will need to store their selection in a variable we will call
userValue
. Call this methodhandleOptionSelect
.
Detailed Instructions
- Let's start off by creating a method that will handle the increase of the amount on state. We will be using the public class field syntax and using
setState
with a callback, rather than a object. The callback will take one parameter,prevState
. This parameter gives us access to state without modifying it directly (this is an example of closure!). The callback function needs to return an object that will be used to update state.
handleAmountIncrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount + 1
}
})
}
- Next we will create a method that will handle the decrease of the amount on state. We will be using the public class field syntax and using
setState
with a callback for this as well. The callback will take one parameter,prevState
. This parameter gives us access to state without modifying it directly.
handleAmountDecrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount - 1
}
})
}
- Finally we will create a method that will handle the user selection from the dropdown. We will be using the public class field syntax and using
setState
with a callback for this as well. This method will expect an event (evt
) as it's only parameter. We will assign the value fromevt.target.value
to a variable we will calluserValue
. Return a new object fromsetState
that updatesselectedCurrency
andcurencyChosen
on state. The new value ofselectedCurrency
will be theuserValue
variable. The new value ofcurrencyChosen
will be a boolean. Using a ternary, determine ifuserValue
is equal to 'Selected Currency' (capitalization matters here). If it does, set the value tofalse
, otherwise set totrue
.
handleOptionSelect = (evt) => {
const userValue = evt.target.value
this.setState(() => {
return {
selectedCurrency: userValue,
currencyChosen: userValue === 'Select Currency' ? false : true
}
})
}
- Last step is to use these methods in the appropriate spots.
- Using an
onClick
event listener, use thehandleAmountIncrease
method on the button with a+
as the inner text. - Using an
onClick
event listener, use thehandleAmountDecrease
method on the button with a-
as the inner text. - Using an
onChange
event listener, use thehandleOptionSelect
method on the select element.
- Using an
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
const withCurrency = (BaseComponent) =>
class Currency extends Component {
state = {
currencyChosen: false,
selectedCurrency: 'Select Currency',
amount: 0
}
handleAmountIncrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount + 1
}
})
}
handleAmountDecrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount - 1
}
})
}
handleOptionSelect = (evt) => {
const userValue = evt.target.value
this.setState(() => {
return {
selectedCurrency: userValue,
currencyChosen: userValue === 'Select Currency' ? false : true
}
})
}
render() {
const currencyData = [
{ name: 'Japanese Yen', symbol: '¥', rate: 113.6, id: 0 },
{ name: 'British Pound', symbol: '£', rate: 0.77, id: 1 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32, id: 2 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41, id: 3 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01, id: 4 }
]
const currencyOptions = currencyData.map((currency, index) => (
<option key={currency.id} value={index}>
{currency.name}
</option>
))
return (
<div>
<select
value={this.state.selectedCurrency}
onChange={this.handleOptionSelect}>
<option value='Select Currency'>Select Currency</option>
{currencyOptions}
</select>
<div>
<button className='add' onClick={this.handleAmountIncrease}>
+
</button>
<button className='minus' onClick={this.handleAmountDecrease}>
-
</button>
</div>
<BaseComponent
currency={currencyData[this.state.selectedCurrency]}
amount={this.state.amount}
/>
</div>
)
}
}
In this step, we'll invoke our function and build out a component to display the currency.
- At the bottom of the file, create a new variable called
ExchangedCurrency
which will hold the component returned from invokingwithCurrency
. - One thing to remember is that
withCurrency
required a parameter we calledBaseComponent
. Since we will be invoking thewithCurrency
function, we need to create a component to use. - Create a new component inside
CurrencyDisplay.js
. This component will be a functional component, meaning it has no state. We will need to haveprops
be one of our parameters. Think back to the previous step, what were the two props on BaseComponent? You can destructure them if you want. We will use data fromprops.currency
and the amount fromprops.amount
to display and convert our currency.
Detailed Instructions
- At the bottom of
CurrencyConverter.js
create a new constant calledExchangedCurrency
. This variable will hold the result of invokingwithCurrency
. BecausewithCurrency
requires aBaseComponent
parameter, we will need to create that but first we will need to export ourExchangedCurrency
variable. (Part 1) - Switch to our
CurrencyDisplay.js
file. This is where we'll display our converted currency. The component should be functional, take in one paramater calledprops
and return some JSX. The JSX will be a<p></p>
element. Thep
element should show the US Dollar amount, the name of the currency, the symbol and then the amount of the exchanged currency. Export the newly created component. - Back inside of
CurrencyConverter.js
, import ourCurrencyDisplay.js
component and pass it as argument to the invocation ofwithCurrency
at the bottom of the file. (Part 2)
src/Components/CurrencyConverter.js
Part 1
import React, { Component } from 'react'
const withCurrency = (BaseComponent) =>
class Currency extends Component {
// PREVIOUS STEPS
}
const ExchangedCurrency = withCurrency()
export default ExchangedCurrency
src/Components/CurrencyDisplay.js
import React from 'react'
const CurrencyDisplay = (props) => (
<p>
US Dollar ${props.amount.toFixed(2)} - {props.currency.name}{' '}
{props.currency.symbol}
{(props.amount * props.currency.rate).toFixed(2)}
</p>
)
export default CurrencyDisplay
src/Components/CurrencyConverter.js
Part 2
import React, { Component } from 'react'
import CurrencyDisplay from './CurrencyDisplay'
const withCurrency = (BaseComponent) =>
class Currency extends Component {
// PREVIOUS STEPS
}
const ExchangedCurrency = withCurrency(CurrencyDisplay)
export default ExchangedCurrency
In this step we need to import our newly created ExchangedCurrency
component into App.js
.
- Open
App.js
. Remove all of the existing code from Create React App and importExchangedCurrency
from./Components/CurrencyConverter.js
. - Render the component onto the screen but use a fragment rather than a div as it's parent container. Feel free to add an
h2
element above to signal that this is a Higher Order Component. In the next project you will create a Render Prop version of this same project and the heading will help you know which one is which.
src/App.js
import React, { Component } from 'react'
import './App.css'
import ExchangedCurrency from './Components/CurrencyConverter'
class App extends Component {
render() {
return (
<>
<h2>Higher Order Component</h2>
<ExchangedCurrency />
</>
)
}
}
export default App
You may have noticed by now that our project doesn't run yet and we are getting an error Cannot read name of undefined
. Think for a moment about what may be causing this problem?
This is happening because we are trying to display our CurrencyDisplay.js
component before we have anything on our currency
prop. In this step we will conditionally render text if the user hasn't selected an option from the dropdown.
- Inside of
CurrencyConverter.js
, look to the return statement and find where we are rendering theBaseComponent
. We will need to conditionally render this component only if a user has selected something from the dropdown. If the user has not, we will display the textPlease Select Currency
.
Detailed Instructions
Head down to the return of the Currency
component inside of withCurrency
. Here we will use a ternary to determine if our user has selected something from the dropdownn. Luckily on state we have a key of currencyChosen
which is a boolean. Use this to determine if we should display the BaseComponent or if we should display the text Please Select Currency
.
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
import CurrencyDisplay from './CurrencyDisplay'
const withCurrency = (BaseComponent) =>
class Currency extends Component {
state = {
currencyChosen: false,
selectedCurrency: 'Select Currency',
amount: 0
}
handleAmountIncrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount + 1
}
})
}
handleAmountDecrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount - 1
}
})
}
handleOptionSelect = (evt) => {
const userValue = evt.target.value
this.setState(() => {
return {
selectedCurrency: userValue,
currencyChosen: userValue === 'Select Currency' ? false : true
}
})
}
render() {
const currencyData = [
{ name: 'Japanese Yen', symbol: '¥', rate: 113.6, id: 0 },
{ name: 'British Pound', symbol: '£', rate: 0.77, id: 1 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32, id: 2 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41, id: 3 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01, id: 4 }
]
const currencyOptions = currencyData.map((currency, index) => (
<option key={currency.id} value={index}>
{currency.name}
</option>
))
return (
<div>
<select
value={this.state.selectedCurrency}
onChange={this.handleOptionSelect}>
<option value='Select Currency'>Select Currency</option>
{currencyOptions}
</select>
<div>
<button className='add' onClick={this.handleAmountIncrease}>
+
</button>
<button className='minus' onClick={this.handleAmountDecrease}>
-
</button>
</div>
{this.state.currencyChosen ? (
<BaseComponent
currency={currencyData[this.state.selectedCurrency]}
amount={this.state.amount}
/>
) : (
<p>Please Select Currency</p>
)}
</div>
)
}
}
const ExchangedCurrency = withCurrency(CurrencyDisplay)
export default ExchangedCurrency
This step is meant to stretch what you know and combine your knowledge. The first part of the black diamond is to make it so the user cannot go lower than 0. The second part of the challenge is to disable the buttons until an option from the dropdown has been selected.
The final challenge of the black diamond is to make it so every time a user selects a new currency it outputs a new currency display card rather than updating the same card each time.
If you see a problem or a typo, please fork, make the necessary changes, and create a pull request so we can review your changes and merge them into the master repo and branch.
© DevMountain LLC, 2017. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.