In this project, we'll be creating two of the same projects. One will be written as a higher order component (HOC) and the other will be written as 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 render prop component.
- Import React.
- Create a class component called
Currency
- The Currency component will have some state
Detailed Instructions
Remember render props are another pattern for cross cutting concerns and making your code reusable, similar to a HOC.
We first want to create a class based component we will call Currency
. We chose a class based component because this will have some state involved.
src/Components/CurrencyConverter.js
class Currency extends Component {
state = {}
render(){
return (
// SOON TO BE JSX
)
}
}
In this step, we will create the boilerplate for our currency converter. 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
,selected currency: '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 Country'. - Create two
<button>
elements, one should have+
as it's inner text and the other should be-
. - Below the
button
's, wrapthis.props.render()
in curly braces
Detailed Instructions
- Set some intial state for this component. We will need a
currencyChosen
which will default tofalse
,selected currency
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 },
{ name: 'British Pound', symbol: '£', rate: 0.77 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01 }
]
- 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
, andid
and have the individual currency name as text. call the new arraycurrencyOptions
. - Create a container div
<div>
. Inside of thediv
create a<select>
element. - 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, renderthis.props.render()
in curly braces. This is the function we will use to display our soon to be created component. Thethis.props.render()
will have two arguments; one which will useselectedCurrency
from state as the index to select an option from thecurrencyData
array and the other argument will be calledamount
which will be the value of amount on state.
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
class Currency extends Component {
state = {
currencyChosen: false,
selectedCurrency: 'Select Currency',
amount: 0
}
render() {
const currencyData = [
{ name: 'Japanese Yen', symbol: '¥', rate: 113.6 },
{ name: 'British Pound', symbol: '£', rate: 0.77 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01 }
]
const currencyOptions = currencyData.map((currency, index) => (
<option key={index} 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>
{this.props.render(
currencyData[this.state.selectedCurrency],
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
. - Export the function to be used in App.js
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.
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. The callback will take one parameter,prevState
. This parameter gives us access to state without modifying it directly. This method will need an event passed into it. We will assign the value fromevt.target.value
to a variable we will calluserValue
. ThissetState
won't need access to theprevState
parameter but we will still use the callback syntax. 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' (punctuation 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
- We will also export our component so we can use it on
App.js
. - Clear out all of the JSX from Create React app.
- Render the new
Currency
component. It will need one prop calledrender
. Remember, this prop can be called anything but it is common convention to call itrender
. This prop will contain an anonymous arrow function. Use a fragment as the container div.
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
class Currency extends Component {
state = {
currencyChosen: false,
selectedCurrency: 'Select Currency',
amount: 0
}
handleAmountIncrease = () => {
this.setState((prevState) => {
return {
amount: prevState.amount + 1
}
})
}
handleAmountDecrease = () => {
return (
this.state.amount > 0 &&
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 },
{ name: 'British Pound', symbol: '£', rate: 0.77 },
{ name: 'Canadian Dollar', symbol: 'CAD', rate: 1.32 },
{ name: 'Mexican Peso', symbol: 'Mex$', rate: 20.41 },
{ name: 'Swiss Franc', symbol: 'Fr.', rate: 1.01 }
]
const currencyOptions = currencyData.map((currency, index) => (
<option key={index} 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.props.render(
currencyData[this.state.selectedCurrency],
this.state.amount
)}
</div>
)
}
}
export default Currency
src/App.js
import React, { Component } from 'react'
import './App.css'
import CurrencyConverter from './Components/CurrencyConverter/CurrencyConverter'
class App extends Component {
render() {
return (
<>
<h2>Render Props</h2>
<CurrencyConverter render={() => {}} />
</>
)
}
}
export default App
In this step, we'll write some JSX to display the exchanged currency.
- Staying in
App.js
, pass in two parameters to ourrender prop
function;currencyData
andamount
. These will representcurrencyData[this.state.selectedCurrency]
andthis.state.amount
in a render function inCurrencyConverter.js
. - Return a
<p>
element that will hold the text displaying the amount in US dollars, the name of the currency we are exchanging to, the symbol and the exchanged amount.
Detailed Instructions
Pt 1
- Take a look at our render prop on the Currency Component. It is an anonymous function. We know that functions can return anything and in this case we will be returning some JSX. Before that though, we need to pass in two parameters that wil represent our data when we invoke our render prop function in
CurrencyConverter.js
. - Have our render prop function return a
p
tag. - The
p
element should show the US Dollar amount, the name of the currency, the symbol and then the amount of the exchanged currency. - If having this
p
element be represented as a new component, you can create a new component with the data passed down as a prop. The new component will be returned from the render prop rather than JSX. - Add in a
<h2>
with some text signaling that this is a render prop version of this project. This will help you when you are comparing render props to HOC's
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
import './App.css'
import CurrencyConverter from './Components/CurrencyConverter'
// import CurrencyDisplay from './Components/CurrencyDisplay' alternatively we can use the CurrencyDisplay component rather than inline JSX. We would need to pass an amount and currency prop
class App extends Component {
render() {
return (
<>
<h2>Render Props</h2>
<CurrencyConverter
render={(currencyData, amount) => (
<p>
US Dollar ${amount.toFixed(2)} - {currencyData.name}{' '}
{currencyData.symbol}
{(amount * currencyData.rate).toFixed(2)}
</p>
)}
/>
</>
)
}
}
export default App
If you want to creae a separate component to render the exchanged currency rather than JSX, take a look at the component below that we would then import into App.js
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
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 data before we have anything selected from the dropdown. 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 invokingthis.props.render
. We will need to conditionally render this function 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 currencyChose
which is a boolean. Use this to determine of we should display the result of invoking this.props.render
or if we should display the text Please Select Currency
.
src/Components/CurrencyConverter.js
import React, { Component } from 'react'
import CurrencyDisplay from './CurrencyDisplay'
class Currency extends Component {
state = {
currencyChosen: false,
selectedCurrency: 'Select Currency',
amount: 0
}
handleOptionSelect = (evt) => {
const userValue = evt.target.value
this.setState(() => {
return {
selectedCurrency: userValue,
currencyChosen: userValue === 'Select Currency' ? false : true
}
})
}
// OTHER STEPS
render() {
// OTHER STEPS
return (
<div>
{/*OTHER STEPS*/}
{this.state.currencyChosen ? (
this.props.render(
currencyData[this.state.selectedCurrency],
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.