To practice creating React components, you are going to build four simple widgets. You’ll be building a clock widget, an interactive folder widget, a weather widget, and a simple search input component.
By the end of this project, you will:
Generate a new React application called “Widgets” with create-react-app by running npx create-react-app widgets --template @appacademy/simple
. Note how you are using a custom template to generate your React application.
Once your project has been initialized, in the index.js
file you’ll see that ReactDOM
is rendering a <React.StrictMode>
component. StrictMode simply means that additional checks and warnings will be made in development mode. It’s a helpful tool that highlights potential problems.
// index.js
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Let’s rename the rendered App
component to be a component named Root
. Make sure to update where you have imported App
and to update the App.js
file name to Root.js
.
The Root
component should be a function component because it won’t use internal state or any lifecycle methods. For now, have your Root
component return an empty <div>
. You will fill this in with your widget components as you create them. At this point, your Root.js
file should look something like this:
The clock component should display the current date and time, updating every second. Start by creating a new file Clock.js
in your src
folder importing React
into the file. Define your Clock
class to extend from React.Component
and remember to export the class. You will import your Clock
component into your Root.js
file and incorporate it into the return value of your Root
. This is the pattern you will follow for all the widgets.
Now it’s time to create a render method! Have your clock render a “Clock” title in an <h1>
element and check that this renders correctly on the page.
In the constructor, set the initial state for the time of your clock using new Date()
like so:
Write a method, tick
that uses setState
to update the time
to a new Date()
. Remember to define this method using an arrow function or else you’d need to bind the function in the constructor.
Now you can define a componentDidMount() method to initialize the ticking of your clock. As a reminder, the componentDidMount()
method is one of the lifecycle methods. When a component is mounted, the render()
method will first return the component’s JSX elements. Then componentDidMount()
will be called. You can often house your logic to fetch information that updates state in this lifecycle method.
For the componentDidMount()
method in your Clock
component, you’ll use JavaScript’s setInterval()
method to call your this.tick()
method every second.
You’ll also want to store that interval as a property of the Clock
class that you can cancel with clearInterval()
in componentWillUnmount(), which gets called just before the component is removed. Don’t store this in the component’s state
since it doesn’t affect the UI. Instead, just store it directly on this
, like so:
In your render method, display the current hours
, minutes
, and seconds
. Check out all of the Date object methods you can use to display the date and time in a human-readable string.
You’ll notice that you have an index.css
file already imported into your entry index.js
file. Create and include a reset.css
file before the line to import your index.css
file.
Feel free to use the following CSS reset file template:
/* reset.css */
a, article, body, button, div, fieldset, footer, form, h1, h2, header, html, i, img, input, label, li, main, nav, p, section, small, span, strong, textarea, time, ul {
background: transparent;
border: 0;
box-sizing: inherit;
color: inherit;
font: inherit;
margin: 0;
outline: 0;
padding: 0;
text-align: inherit;
text-decoration: inherit;
vertical-align: inherit;
}
ul {
list-style: none;
}
img {
display: block;
height: auto;
width: 100%;
}
button, input[type="email"], input[type="password"], input[type="submit"], input[type="text"], textarea {
/*
Get rid of native styling. Read more here:
http://css-tricks.com/almanac/properties/a/appearance/
*/
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
}
button, input[type="submit"] {
cursor: pointer;
}
Now go to Google Fonts and select a nice font for your clock. In the public/index.html
file, update your page to have a title
of “Widgets”. Now take the font embed code and paste it into the <head>
of your page.
Your index.html
file should look something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link href="https://fonts.googleapis.com/css2?family=Orbitron" rel="stylesheet">
<title>Widgets</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
To use the font, set the font-family
of your element to the font name in your index.css
file.
Set the time and date headers to be on one side and the actual time and date to the other. Refer to the live demo to see what your end goal is. You can achieve this easily with a flexbox. Take a look at the justify-content
property. Which one do you want to use? Try all of them to understand what they do.
Add a background. Use the background
or background-color
property to change the background. Feel free to do this for every widget.
You should now have a clock that displays the current time and date. You used setInterval()
to make sure that the clock updates every second, and clearInterval()
to clear the timer that setInterval()
set. Once you have sufficiently styled your clock, move on to the next widget.
You’re going to add a folder widget that the user can interact with. The folder tabs should each be labeled with their own title. The selected tab should be in a bold font. Below the tab, display the contents of the selected tab. The folder content should update when the user selects different tabs.
Make a Folder
component. Root
should pass the Folder
component a folders
prop. The prop should be an array of JavaScript objects that each have title
and content
as properties:
Folder component
Folders prop
const folders = [
{title: 'one', content: 'I am the first'},
{title: 'two', content: 'Second folder here'},
{title: 'three', content: 'Third folder here'}
];
Keep track of the selected tab’s index in your Folder
component’s state. Set the Folder
component’s default currentTab
state to zero.
In the render method, return an <h1>
element with the title of “Folder”. You’ll begin by rendering one folder’s content, using the currentTab
state to select which folder content to render.
Render a <div>
element with two child elements: a header to render folder titles (you’ll make a <Header>
subcomponent) and a <div>
element to render the selected tab’s content. Define a folder
variable by indexing into your folders
prop with your currentTab
state. This way you can reference your selected folder’s content with clean code!
At this point, your component’s render()
method should look something like this:
render() {
const folder = this.props.folders[this.state.currentTab];
return (
<div>
<h1>Folder</h1>
<div className='tabs'>
{/* TODO: render folder titles */}
<div className='tab-content'>
{folder.content}
</div>
</div>
</div>
);
}
Take a moment to observe the syntax for making a comment inside of JSX. If you use VS Code’s keyboard shortcut (cmd + /
) to comment, you will not make a valid comment. You need to use block comment syntax wrapped in curly braces in order to write comments in JSX!
Remember that JSX interpolation is just syntactic sugar and that it only supports expressions, so you also can’t use if
/else
inside { }
. However, [ternary conditionals] are valid inside JSX interpolation.
Now create a selectTab()
method that takes in a selected folder index. You’ll use this method to update the currentTab
state with the input index. For now, have the method console log the index input.
Let’s move forward with rendering the folder titles!
Let’s create a Headers
subcomponent to render your folder titles! Within your Folder.js
file, create a subcomponent above your Folder
class. This subcomponent will take care of rendering an unordered list of list items containing clickable tabs.
Plan what information you want to pass as props from your Folder
component into your Headers
. You’ll want to render each tab’s title, so you’ll probably want to thread a titles
prop from your Folder
component. Map over your array of folders
to define a titles
array of folder titles. Now thread your titles
array as a prop to the Headers
subcomponent. As a reminder, “threading props” simply refers to passing props from one component to another.
You also want to pass the currentTab
state so that the Headers
component can know which tab to render with different CSS as selected or active.
Lastly, you’ll want your Headers
component to be able to use the selectTab()
method you have defined in order to update the tab’s currentTab
state.
Your Folder
component should render the Headers
subcomponent below:
Now let’s dive into what your Headers
component should render. Begin by returning an unordered list:
Instead of taking in a props
argument and referring to all your props like props.folders
or props.currentTab
, you can destructure the props you have received like so:
const Headers = ({ titles, currentTab, selectTab }) => {
return (
<ul className='tab-header'>
</ul>
);
}
Now map your folder titles
to list item elements that render each folder’s title
. You’ll need to pass a unique key
property to each <li>
or React will grumble to all your console-reading users about its unfair working conditions. “How is one supposed to efficiently diff the DOM when one doesn’t even know which list items match up with which!?”
const Headers = ({ titles, currentTab, selectTab }) => {
return (
<ul className='tab-header'>
{titles.map((title, idx) => {
return (
<li key={idx}>
{title}
</li>
);
})}
</ul>
);
}
To clean up your return, you can extract your list elements as a tabs
variable:
const Headers = ({ titles, currentTab, selectTab }) => {
const tabs = titles.map((title, idx) => {
return (
<li key={idx}>
{title}
</li>
);
});
return (
<ul className='tab-header'>
{tabs}
</ul>
);
};
Now add an onClick
handler to each list item to update the currentTab
state in the Folder
component. You’ll also want to set the id
of the <li>
element to each title’s index. You can then reference the index through e.target.id
to use in the selectTab()
function.
You might ask why not just preset an argument with an arrow function callback directly in the onClick
. It is actually bad practice to do so! Feel free to read more here. In this case, it’s better to handle the event and invoke the selectTab()
function within the click handler.
/* BAD PRACTICE */
return (
<li key={idx} id={idx} onClick={() => selectTab(idx)}>
{title}
</li>
);
/* GOOD PRACTICE */
return (
<li key={idx} id={idx} onClick={handleClick}>
{title}
</li>
);
Define a handleClick()
function in your Headers
component. Reference the folder’s index through e.target.id
and parse the id
into an integer to invoke the selectTab()
function:
At this point, test your click handler. Click your folder titles and open your developer tools console. You should see the logging of clicked folder indices. After you have confirmed your click handler is working, update your selectTab()
function to set the currentTab
state using its input.
Before you move forward to focusing on a specific tab, add some styling to make your Folder
widget look like folders with tabs! Add a border around each tab and use border-radius
to add nicely curved corners to the top of your tabs.
Use a flexbox to ensure that the tabs all take up the same amount of space. Add display: flex
to your CSS for your folder tabs. Center the folder content, both horizontally and vertically.
Add a hover effect to change the background color of the tab that’s being moused over. Change the cursor
to be a pointer
when you’re mousing over the tabs to make it clear that the tabs are interactive.
Now let’s be able to focus on a specific tab! At this point, you should have a widget that displays the content of all your folder tabs.
In your Headers
subcomponent, you’ll want to assign an active
class to your selected tab. The selected tab’s label should be bold and the folder content should update when a different tab is selected. Within the mapping of your header titles
, you can compare the idx
of each title to the folder’s currentTab
state to decide whether a list item should have the CSS class name of active
.
For example, you can use a ternary operator to assign a headerClass
variable like this:
Feel free to restyle your Folder
component by adding the CSS below into your index.css
file. Play around with changing the .tab-header > li.active
class styling to manipulate the styling of your selected tab!
/* Folder */
.tab-header {
margin: 0 20px;
display: flex;
justify-content: space-between;
}
.tab-header > li {
width: 33%;
border-top: 2px solid black;
border-left: 1px solid black;
border-right: 1px solid black;
border-bottom: 2px solid black;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
padding: 5px;
text-align: center;
cursor: pointer;
background-color: lightpink;
}
.tab-header > li:first-child {
border-left: 2px solid black;
}
.tab-header > li:last-child {
border-right: 2px solid black;
}
.tab-header > li:hover {
background-color: lightblue;
color: white;
}
.tab-header > li.active {
color: white;
font-weight: bold;
background-color: lightblue;
border-bottom: 0px;
}
.tabs {
width: 240px;
}
.tab-content {
font-weight: bold;
color: white;
height: 192px;
margin: 0 20px;
border-left: 2px solid black;
border-bottom: 2px solid black;
border-right: 2px solid black;
display: flex;
align-items: center;
justify-content: center;
background-color: lightblue;
}
In this phase, you will create a weather widget to display the current weather based on the user’s location. You will be using the navigator.geolocation
API to get the user’s current location, and the OpenWeatherMap API to get the current weather.
Make a Weather
component, which again, will be incorporated into your Root
component. Now set your component’s default state with a null
weather object in your constructor, like so:
Review the OpenWeatherMap API documentation. You’ll use this API to get the weather based on your current location (it is recommended to fetch the weather by geographic coordinates). Upon a successful fetch, you’ll update your component’s state.
In order to get the API to accept your HTTP requests, you’ll need an API key. Read up on how to use the API key and sign up for one here. After signing up, click on the API keys tab to get your key. You may need to open their welcome email before the API key will work.
In the real world, you should be very careful about placing API keys in frontend JavaScript or anywhere else they are publicly available and can be scraped (this includes public Git repositories). Stolen keys can cost you. You have been warned.
Now let’s get your current location! When the component mounts, call navigator.geolocation.getCurrentPosition()
to get it. Read through the navigator documentation to figure out how to use this method properly. (Make sure you have location services enabled in your browser, or this won’t work.)
From reading the documentation, you know that there are two methods to access a browser’s location data: - getCurrentPosition()
- watchPosition()
Let’s look at the documentation for the getCurrentPosition()
method to find out more about its expected parameters. You should see a Syntax portion on the documentation with the method breakdown below:
You’ll also see that there is a Parameters section below that outlines a mandatory success
callback function, an optional error
callback, and an optional options
object. In documentation, square brackets around a parameter indicates that it is an optional parameter.
Now let’s test the getCurrentPosition()
method in your developer tools console. Console log a result as the method’s success
callback like so:
You should have received a request to share your location with the browser! Upon allowing the browser to know your location, you should console log a GeolocationPosition
object when invoking the method again in the console:
Begin by invoking the getCurrentPosition()
method in your Weather
component’s componentDidMount()
method. Upon successfully retrieving your browser’s location, you’ll invoke a success callback to query the weather API.
Let’s create your success callback! Create a pollWeather()
method to take in your received location
result from navigator.geolocation.getCurrentPosition()
. You’ll use the latitude
and longitude
of your location to make a fetch call to the weather API. Think of how to extract the latitude
and longitude
properties from your GeolocationPosition
object. Also think of how you might structure your fetch URL to include the query parameter for your geographic coordinates.
Navigate to the By geographic coordinates
section in the OpenWeatherMap API documentation. You’ll see an example of an API query string using latitude and longitude coordinates (api.openweathermap.org/data/2.5/weather?lat=35&lon=139
). You’ll also see an example JSON response below.
You can define a toQueryString()
helper method to format your query parameters into a fetch call URL. To think of scaling your “Widgets” project, you can move this helper function into a utils.js
file so that it can be used for other APIs you might incorporate! Have the function take in a params
object. You’ll then iterate through the object to sanitize each query value with encodeURIComponent(). You can then return a query string like lat=35&lon=139
to build an example API query string above.
In your pollWeather()
method, use the Fetch API to make a fetch call to the OpenWeatherMap API. Remember to parse your response as JSON before updating the weather
state. Upon a successful fetch, update your component’s weather
state with the weather
property of your JSON response! Use your component’s state to render the current city and temperature on the page.
By default, the OpenWeatherMap API will return the temperature in Standard units (Kelvin). Convert to Fahrenheit OR peruse the API docs for a way to request the weather in Imperial units (Fahrenheit)! Give the weather box a nice border and make sure the elements inside are spaced evenly.
Great work! Now you have three widgets. One that displays the time, another that allows you navigate folder tabs, and another that displays the weather. You used the navigator.geolocation
API to get your current location, which you then passed to your fetch request to get the weather from the OpenWeatherMap API.
Make an Autocomplete
component that filters a list of names by the user’s input. Match only names that start with the search input. When a user clicks on a name, the input field should autocomplete to that name. Create a new file Auto.js
and define your Autocomplete
class there. Incorporate it into Root
.
Because your autocomplete widget should be reusable, you shouldn’t hard code a list of names into the component. Instead of hard coding the names, set up your Autocomplete
component to accept names
as a prop. Then set the component’s initial state for inputVal
as an empty string.
Build your widget in the render
method. It should contain an input field and an unordered list. Render an <li>
inside the <ul>
for every name that begins with the value in the input box. Remember to pass your unique key
property to each <li>
!
When a user types something into the input, use an onChange
event handler to update the widget’s state. Create a handleInput()
event handler method to update the state of inputVal
with the typed input value.
Also add an onClick
handler to the unordered list. The role of this click handler is to update the widget’s search string (the inputVal
state) upon a user’s click of the <li>
element you’ve created for each name. You will need to turn your <input>
into a controlled component for this to work. Would you access the event’s currentTarget or target? Remember to use setState()
to update the widget’s search string.
Now you’ll want to find the names that match your user’s search input. Define a matches()
method to generate an array of name matches
based on the inputVal
state. Since you’re taking in user input, think of how you could use regular expressions to match the character combinations between your user’s input string and the list of searchable names. If the input is empty, return the original, full list of names so that your user can see all the searchable names!
Now let’s generate the name matches! Iterate through each name. You’ll use the length of inputVal
to slice a segment of each name. Compare the name segment with the input value. Take into consideration that some users might type “barney” instead of searching for “Barney”.
For example, compare the name segment to the input value in order to match a search input of “bar” to the “bar” segment of “Barney”. Then you could add the name, “Barney”, to your matches
array. On the next iteration, the “bar” input would also match to “Barbara” so that you could add “Barbara” to the matches
array.
If you have no matches, you can add a “No matches” string to your matches
array so that when matches
is returned and rendered, your user will be notified upon searching for a name without matches.
Give your component a border and make sure all the <li>
elements are nicely padded inside the box. Change the cursor
property to display a pointer when hovering over one of the <li>
elements. Center all your widgets using flexboxes. Which justify-content
property would you use for this?
Great job! The autocomplete widget uses an event handler to update the state of the component when letters are typed into the input field. Once the autocomplete widget is sufficiently styled, move on to the bonus phase to make your widgets even better.
Right now, in the autocomplete widget, the matched names instantly appear on the screen and the filtered names instantly disappear. This is abrupt and ugly. You want the names to fade out or in when they are entering or leaving the page. How can you achieve that with React? With React Transition Group!
First you need to import the CSSTransition
module into your project. In the console, run npm install react-transition-group@^4.0.0 --save
.
Then you need to import the module in the file. At the top of Auto.js
, write import CSSTransition from 'react-transition-group';
.
In your render
method, you will need to wrap the group of elements that will be entering and leaving the screen with the <TransitionGroup>
element. In the case of the autocomplete widget, wrap the results rendered as <li>
, within the <ul>
. You are not wrapping each individual <li>
, but rather the entire group.
Now you’ll need to wrap each individual <li>
with a <CSSTransition>
element. Move the list item’s key
to the <CSSTransition>
element.
<CSSTransition>
has three necessary attributes. Read what they are below and make sure to include them:
classNames
: This is the name that’s used to create all of the transition classes. For now, let’s set this to "result"
, but you can pick any name you like.
timeout
: Specifies how long (in ms) the transition should last. This prop takes in an object with two keys (timeout={{ exit: exitNumber, enter: enterNumber }}
). * enter
: Length of the transition when the element enters. This needs to be a number, so you’ll have to interpolate the JavaScript number, otherwise it’ll be read as a string. (i.e {500}
instead of 500
). * exit
: Same as above, except for when an element is leaving the page.
Finally the CSS. Create a new CSS file and paste in the code below. Be sure to import your new CSS file into your entry index.js
file so the transitions are applied.
The CSS below assumes you’ve given the classNames
attribute to result
. If you gave it a different name, just replace every result
with the name you gave.
/* AutoComplete */
.result-enter {
opacity: 0.01;
transform: translateY(500%);
}
.result-enter.result-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 500ms, transform 500ms;
}
.result-exit {
opacity: 1;
transform: translateY(0);
}
.result-exit.result-exit-active {
opacity: 0.01;
transform: translateY(500%);
transition: opacity 500ms, transform 500ms;
}
Go play with the widget! You’ll notice that when names appear, they fade in from the bottom. When they leave, they fade out and fall to the bottom. Let’s break down the CSS file:
.result-enter
: Specifies the initial state of an element that is entering the page. Since I want the names to start invisible and at the bottom, I’ve given it the opacity
and transform
properties the appropriate values.
.result-enter.result-enter-active
: Specifies the final state of an element that has entered the screen. Looking at the CSS, you can see that I expect the element to be completely opaque and in it’s original y-position when it is done entering. This is where you also specify the transition
property.
.result-exit
: Specifies the initial state of an element that is leaving the page. In almost all cases, the values of this class with match the values in the result-enter.result-enter-active
class.
.result-exit.result-exit-active
: Specifies the final state of an element that has left the screen. This is where you also specify the transition
property.
Play around with the CSS file. What kind of interesting transitions can you create?
Check out your new transition in the browser. Open up your developer tools and type something in the “Autocomplete” search input. Your transitions are working, but wait - you have a warning in the console!
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of CSSTransitionGroupChild which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://fb.me/react-strict-mode-find-node
This is an example of how StrictMode is a helpful tool that highlights potential problems. In this case, StrictMode
is giving you helpful information about the deprecation of findDOMNode
, which is used under the hood. You are also given a clickable link to the official React documentation!
According to the documentation, findDOMNode
is used “to search the tree for a DOM node given a class instance.” Now is your chance to practice going through the official React documentation and learning from reading a merged PR in the official react-transition-group repository! Take a moment to read through the merged PR to see real-life discussion about implementing the nodeRef
feature as an alternative to having React use findDOMNode
under the hood.
In your constructor method, create a ref with React.createRef()
and use the ref to assign a nodeRef
prop to the <CSSTransition>
that wraps your result items. Doing this will allow React to reference the <CSSTransition>
component, without using the deprecated findDOMNode
method to search through the tree for the component. Since React is no longer using findDOMNode
under the hood, using a nodeRef
will remove the warning in the developer tools console.
Congratulations! You have just read through official documentation. In the future, you may contribute to an open-source or community managed project, just like how the use for the merged PR did! Don’t be discouraged by reading live discussion in GitHub issues and pull requests. You’ll continue building your foundation of React knowledge and before you know it, you might even be contributing to projects yourself!