This dojo contains a selection of ReactJs concepts that will help you to start your first React Web App. We will not be diving to deep in to the ReactJs library or the eco system of supporting libraries; that will be left to you.
You can find the official documentation at: https://reactjs.org/docs/hello-world.html
- Install node v6 or newer & yarn
- Install git either git-for-windows or git-for-mac. If you need help take a look at https://www.atlassian.com/git/tutorials/install-git
- Install create-react-app, by running
yarn global add create-react-appin a command or bash window - Install vscode
- You may consider installing the Prettier extension for vscode to help with code formatting.
- You may also consider installing Reactjs code snippets once you are comfortable with the basics.
Follow these steps to create a blank project and run it locally. Keep it running for the rest of this dojo:
- Open a new git bash window (or cmd if you prefer)
- Change directory to where you'd like to create your new project folder (i.e.
cd /c/development/), and run the following commands:create-react-app react-dojo-frontendcd react-dojo-frontendcode .- Open a terminal window in vscode using
ctrl + 'and type:yarn start
After a moment or two your default browser should open and you should see your new React site. If not open it and enter URL shown in your terminal window. (i.e. http://localhost:3000).
You should also have your new project open in vscode, ready for editing. The page will reload if you make edits and you will also see any lint errors in the terminal window
README.md
node_modules/
package.json
public/
index.html
favicon.ico
src/
App.css
App.js
App.test.js
index.css
index.js
logo.svgFor the project to build these files must exist with exact filenames:
public/index.html- This is the main page template and provides the element to which React adds everything (<div id="root"><div>)src/index.js- This is the Javascript entry point and where React is told to use therootdiv mentioned above
You are free to delete or rename any of the other files.
You can create subdirectories inside src to help organise the components that build up the application. We will do this as we progress through the Dojo.
For our dojo frontend we are going to use Bootstrap 3 as our CSS framework. This is going to be provided by the React-Bootstrap library as it provides some easy to use components which follow the React way of working nicely. You'll need to add this package to the app:
- Open another terminal in vscode using
ctrl + shift + '. We'll use two terminals so the other can continue to serve our app while we work. - Run
yarn add react-bootstrap@0.31.5 bootstrap@3 - Import Bootstrap CSS and optionally Bootstrap theme CSS in the beginning of
src/index.js- you can open the file by usingctrl + pand typingindex.js:import 'bootstrap/dist/css/bootstrap.css';import 'bootstrap/dist/css/bootstrap-theme.css';
(Note: After adding new packages you may need to restart the development server running in the first terminal with ctrl + c and running yarn start again)
Now let's see if it's working by editing App.js:
- Add
import { Navbar, Jumbotron } from "react-bootstrap";to the list of imports - Replace
headerwithNavbarand remove theclassNameattribute (remember the end tag) - Replace
pwithJumbotronand move it outside theNavbartags - Move the
afrom betweenNavbarto betweenJumbotron
If you've left the site running, you should see all of your changes show up in real time as you save.
Having everything in the App.js is nice at the moment and makes it easy to see all the components that make up the page. As the application grows however this file will become much larger and harder to maintain. The React way of dealing with this (and a sensible concept to follow) is to break pages and apps up into small components that do one thing and do it well.
So let's pull our navigation out into its own component.
- Create a new folder under
srccalledcomponents - Create a new file under
componentscalledNavigation.js(Files with React components in should use PascalCase) - Add the following scaffolding to
Navigation.js
importReact,{Component}from'react';classNavigationextendsComponent{render(){return(<div></div>);}}exportdefaultNavigation;Move the import for
logofromApp.jstoNavigation.jsand change the path to'../logo.svg'Copy the import for
NavbarfromApp.jstoNavigation.jsCopy the
Navbartag fromApp.jsand paste it in between the<div>tags inNavigation.jsAdd the following import to
App.js:importNavigationfrom"./components/Navigation";
Remove the
Navbartag inApp.jsand replace it with:<Navigation/>
When your browser refreshes you should see... No change at all. If this is the case you have done everything right and have just created your first reusable React component.
If you see an error in the browser, check the terminal window running the development server for errors. The message(s) should help you pinpoint the issue easily.
The HTML-like syntax in the render of the Navigation component above is not HTML, it's JSX. JSX is just syntactic sugar for how React composes the nested components. It's important to remember that JSX is NOT HTML. If you look closely you can see the differences as you move through this Dojo (for example className in JSX vs class in regular HTML).
The other important thing to note is that JSX can only have one root element. As in the Naviagtion component above, there is a single <div> element inside which you copied the navigation element from App.js. For example:
Bad JSX
return(<div>Hello</div><div>World</div>);Good JSX
return(<div><div>Hello</div><div>World</div></div>);More information about JSX can be found in the official docs.
Routing allows for navigation between components (page components) in our React app. So we will start by adding the following two components:
- Home
- About
For routing to work we need to add two additional packages to our project:
- Open the terminal used above to add Bootstrap (or open a new one using
ctrl + shift + '). - Run
yarn add react-router-dom react-router-bootstrap
(Note: After adding new packages you may need to restart the development server running in the first terminal with ctrl + c and running yarn start again)
Now we need to add two new components, one for home and one for about:
- Under the
componentsfolder add a file calledHome.js - Add the following scaffolding to
Home.js
importReact,{Component}from'react';classHomeextendsComponent{render(){return(<div></div>);}}exportdefaultHome;- Add another file under components called
About.jswith the above scaffolding with a class name ofAboutand export ofAbout(You should start to see a pattern in the basic scaffolding for a component by now) - Copy the
Jumbotronimport fromApp.jstoHome.js - Copy the
Jumbotronelement fromApp.jsin between thedivelement inHome.js - Add some text (maybe "This is the about page") between the
divelement inAbout.js
With the components in place we need to make the changes to our App.js file to tell React which component to show and when.
In App.js:
- Add the following imports
import{BrowserRouter,Route,Switch}from"react-router-dom";importHomefrom"./components/Home";importAboutfrom"./components/About";- Wrap the
divelement with aBrowserRouterelement (Remember the endingBrowserRouterelement) - Completely replace the
Jumbotronelement with the following
<Switch><Routeexactpath="/" component={Home}/><Routeexactpath="/about" component={About}/></Switch>- Remove the now unused
Jumbotronimport
The final step is to add some links to our Navigation component to allow us to navigate between the home and about components.
In Navigation.js:
- Add
NavandNavItemto thereact-bootstrapimport - Add a new import for
LinkContainerfromreact-router-bootstrap - Remove the
imgelement in theNavBarelement - In the
NavBarelement add the following
<Nav><LinkContainerexactto="/"><NavItem>Home</NavItem></LinkContainer><LinkContainerto="/about"><NavItem>About</NavItem></LinkContainer></Nav>- Remove the now unused logo import in
Navigation.js
You should now have two links in your nav bar that when clicked change the component displayed. Click away and enjoy the wonder of your first React app...
Our basic app is nice however it feels very bare. Let's add some content to it from an API. For this Dojo we are going to use the public Random user API from https://randomuser.me/
- Create a new component called
UserDetails.jswith the basic scaffolding as we did forHomeandAbout - Add a
constructorfunction, just above therenderfunction as follows:
constructor(){super();this.state={user: {name: {title: "",first: "",last: ""},thumbnail: ""}}}- Add a
componentDidMountfunction (between theconstructorandrenderfunctions) as follows:
componentDidMount(){fetch("https://randomuser.me/api/?results=1").then(apiResults=>{returnapiResults.json();}).then(apiResultsAsJson=>{letrandomUser=apiResultsAsJson.results[0];returnrandomUser;}).then(randomUser=>{letuserData=this.state.user;return{ randomUser, userData };}).then(randomUserAndNewUserData=>{let{ randomUser, userData }=randomUserAndNewUserData;userData.thumbnail=randomUser.picture.large;return{ randomUser, userData };}).then(randomUserAndNewUserData=>{let{ randomUser, userData }=randomUserAndNewUserData;userData.name=randomUser.name;returnuserData;}).then(newUserData=>{this.setState({user: newUserData});});}You can find out about componentDidMount and other life cycle events in the React docs
If you are not furmiliar with the Promise syntax above there is a nice blog post on scotch.io that might help.
- Replace the
renderfunction with the following, remembering to add the imports for the newly used Bootstrap components
render(){letusersFullname=this.getUsersFullname(this.state.user);return(<divclassName="col-sm-6 col-sm-offset-3"><Panelstyle={this.getPanelHeaderStyle()}footer={this.getPanelFooter()}header={usersFullname}bsStyle="info"><Imagesrc={this.state.user.thumbnail}alt={usersFullname}circlethumbnail/></Panel></div>);}Let's add the missing supporting functions between componentDidMount and render functions.
getUsersFullname=user=>{let{ first, last }=user.name;return(first.charAt(0).toUpperCase()+first.slice(1)+" "+last.charAt(0).toUpperCase()+last.slice(1));};getPanelHeaderStyle=()=>{return{fontSize: "30px"};};getPanelFooter=()=>{return<div> </div>;};Now it's time to tell the app about our new component:
- In
Home.jsadd an import for theUserDetailscomponent - Under the
Jumbotronelement add
<UserDetails/>Now if the everything went well when the app reloads in the browser you will see a new element that displays a random user's name and profile picture. Moving between the 'About' and 'Home' components will trigger a fetch and render of a new random user. Try it!!
In one sense, "state" means the current visual representation of the app on screen… In the React sense, "state" is an object that represents the parts of the app that can change. Each component can maintain its own state, which lives in an object called this.state. - Dave Ceddia
In the UserDetails component above we introduced state by setting the initial values in the constructor function with the following:
this.state={user: {name: {title: "",first: "",last: ""},thumbnail: ""}}A components state can only be set like this in its constructor function, at any other point if it needs to change the this.setState() function is used.
The main reason for this is that React is trying to inforce immutability in a non immutable language - you can read more about JavaScript and immutability here. One upside to this is that all changes to state are done by one function which makes it easy to find and debug when a component isn't rendering the right details.
Props in React are a way for a parent component to pass information/properties down to child components. These properties can be static state values, settings and also functions that the child component can call. They look like normal HTML attributes.
An abstract example of this in the context of our app could be, when the UserDetails component fetches a new random user the Home component changes the message in the Jumbotron. This could done as follows
- A property called "currentRandomUsersFullname" is added to the state of the
Homecomponent - A function called
changeRandomUsersFullnamethat updates the "currentRandomUsersFullname" state value is added to theHomecomponent - The
Jumbotroncontent is updated to display the value ofthis.state.currentRandomUsersFullname - The
changeRandomeUsersFullnamefunction is passed to theUserDetailscomponent as a property calledonDetailsChange - Every time a random user is fetched in the
UserDetailscomponent a call is made tothis.props.onDetailsChangepassing it the full name of the user
Bonus points - Give the above a go and see if you can get it to work.
Let's add some functionality that users of our app can interact with.
At the moment the only way to get a new random user is to reload the page or navigate between the Home and About components. It would be nicer if the user could click a button instead.
In UserDetails.js:
- Refactor the contents of the
componentDidMountfunction out into a function calledfetchRandomUser - Add a call to
this.fetchRandomUsertocomponentDidMount(At this point if you test the app you should not see any difference) - Add
Buttonto the import formreact-bootstrap - Replace the contents of
getPanelFooterwith:
return(<Buttonid="btnNewRandomUser"bsStyle="success"onClick={()=>{this.fetchRandomUser()}}>
New random user
</Button>);At this point if you click the button you will see that the users name and profile image changes.
If you open the developer tools in the browser (F12) and look at the network tab, you'll notice that there are only two calls made when the button is clicked. One to the API for the user details and another for the user's profile image. This is because everything else is happening in the browser (client-side) and the page is not being reloaded.
Let's add the ability to only show male or female names and profile pictures.
In UserDetails.js:
- Add a
searchobject with agenderproperty set to"male"to the initial state - Change the call to
fetchRandomUserincomponentDidMountto pass an object with agenderproperty set tothis.state.search.gender - Change the function signature for
fetchRandomUserto accept a parameter and call itsearchOptions - Change the url passed to the
fetchfunction to"https://randomuser.me/api/?results=1&gender=" + searchOptions.gender - Add a the following function above the
getPanelFooterfunction:
handleGenderChange=changeEvent=>{this.setState({search: {gender: changeEvent.target.value}});};- Change the
getPanelFooterfunction to the following, remembering to add the imports for the newly used Bootstrap components:
getPanelFooter=()=>{return(<formstyle={{fontSize: "0.6em"}}><Buttonid="btnNewRandomUser"bsStyle="success"onClick={()=>{this.fetchRandomUser({gender: this.state.search.gender});}}>
New random user
</Button><br/><br/><FormGroupcontrolId=""><ControlLabel>Gender:</ControlLabel>{" "}<Radioid="rdiMale"name="gender"value="male"inlineonChange={this.handleGenderChange}checked={this.state.search.gender==="male"}>
Male
</Radio><Radioid="rdiFemale"name="gender"value="female"inlineonChange={this.handleGenderChange}checked={this.state.search.gender==="female"}>
Female
</Radio></FormGroup></form>);};Now when the app reloads there will be two radio buttons below the New random user button. Changing which is selected and clicking New random user will result in only names and profile pictures for that gender being shown.
- Change the default gender selection when the app loads from male to female
- Add the ability to only display English and French names (Have a look at the docs)
- Add a loading spinner using state, Promises, props and a component
As you have seen components are the core building blocks of React apps. We have not only created our own components, in Navigation and UserDetails we have used others components like LinkContainer and Button. React is really just a library for creating components at its core.
Each component needs to return or render some JSX (the HTML-like looking code we have been creating above) for React to render to the real DOM. Without this there would be nothing shown on the screen.
When creating components there are two different ways:
- Functional (presentational, stateless, dumb)
constUsername=(user)=>{return<div>{user.identity}</div>}- Class-Based (containers, smart, stateful)
classUserDetailsextendsComponent{render(){return<div>Some user details...</div>}}It's considered best practice to use Functional components as often as possible as they are lightweight, fast and composable. We have used Class-Based components so far however a number of them could we switched to be Functional components.
Can you spot which ones? (Hint: They don't have any state)
Try converting one of them, if not all.
While navigating the project you may have noticed the App.test.js file that was created by create-react-app. The convention for React apps is to name the test files after the components they test with test after the name and before .js
You can run them using:
yarn testRunning them now results in a failure. This is because when the UserDetails component mounts it makes a call to the randomuser.me API (in componentDidMount).
We need to make some changes to allow our tests to run without calling out to the real API. This is done using a concept called mocking, we will not dig in to this here as there are plenty of good explanations online.
Lets start making the required changes.
- Add a package called
jest-fetch-mockusingyarn. This package is for use while developing for make sure you use the--devoption when adding.
In App.test.js
- Add the following code just under the import lines to replace the global
fetchfunction with the one from thejest-fetch-mocklibrary.
global.fetch=require("jest-fetch-mock");- Replace the existing call to
it("renders without crashing")function with:
describe("App component",()=>{it("renders without crashing",()=>{constdiv=document.createElement("div");ReactDOM.render(<App/>,div);});});- Add the following mock response to as the first line in the
renders without crashingtest
fetch.mockResponse(JSON.stringify({results: [{gender: "male",name: {title: "monsieur",first: "victor",last: "riviere"},location: {street: "2113 place des 44 enfants d'izieu",city: "savigny",state: "ticino",postcode: 5614},email: "victor.riviere@example.com",login: {username: "organiccat975",password: "base",salt: "q0IJerIY",md5: "6183c15acc40044097f248acb1d3da9f",sha1: "ce0bc4f0c22a390ad1b5e4c0ac6214745967105c",sha256:
"6616ffb0fa7bf5dc13bb9aedc16b36d5f84bf49818e784ea58f342835b7876b2"},dob: "1955-06-08 23:49:32",registered: "2002-11-14 04:24:53",phone: "(704)-124-3808",cell: "(966)-638-1407",id: {name: "AVS",value: "756.RFRI.GUKM.93"},picture: {large: "https://randomuser.me/api/portraits/men/59.jpg",medium: "https://randomuser.me/api/portraits/med/men/59.jpg",thumbnail: "https://randomuser.me/api/portraits/thumb/men/59.jpg"},nat: "CH"}],info: {seed: "fff763e9a0d04a5f",results: 1,page: 1,version: "1.1"}}));Now run the tests again and if everything is good they will pass. Yay! Go Us!!! :)
As you can see from the existing test, our test suites begin with a call to a global function describe which has two parameters: a string and a function. The string is a name or title for a suite of tests ("App component"). The function is a block of code that implements the suite.
Tests are defined by calling the global function it, which, like describe takes a string and a function. The string is the title of the test and the function is the test. A test contains one or more expectations that prove the state of the code under test. An expectation is just an assertion that is either true or false (In this case the fact that the app didn't crash results in a true assertion).
Since describe and it are functions, they can contain any executable code necessary to implement the test. JavaScript scoping rules apply, so variables declared in a describe are available to any it block inside.
So now that we have a passing test that proves our app doesn't crash on start up, it's probably a good time to add some more. First though let's tidy up our code a little to make it easier to add more tests.
- Add a new file called
testConstants.jsunder thesrcfolder. - In
testConstants.jsadd:
module.exports={mocks: {responseFromRandomUserApi: {success: {results: [{gender: "male",name: {title: "monsieur",first: "victor",last: "riviere"},location: {street: "2113 place des 44 enfants d'izieu",city: "savigny",state: "ticino",postcode: 5614},email: "victor.riviere@example.com",login: {username: "organiccat975",password: "base",salt: "q0IJerIY",md5: "6183c15acc40044097f248acb1d3da9f",sha1: "ce0bc4f0c22a390ad1b5e4c0ac6214745967105c",sha256:
"6616ffb0fa7bf5dc13bb9aedc16b36d5f84bf49818e784ea58f342835b7876b2"},dob: "1955-06-08 23:49:32",registered: "2002-11-14 04:24:53",phone: "(704)-124-3808",cell: "(966)-638-1407",id: {name: "AVS",value: "756.RFRI.GUKM.93"},picture: {large: "https://randomuser.me/api/portraits/men/59.jpg",medium: "https://randomuser.me/api/portraits/med/men/59.jpg",thumbnail: "https://randomuser.me/api/portraits/thumb/men/59.jpg"},nat: "CH"}],info: {seed: "fff763e9a0d04a5f",results: 1,page: 1,version: "1.1"}}}},expects: {requestsToRandomUserApi: {maleUrl: "https://randomuser.me/api/?results=1&gender=male",femaleUrl: "https://randomuser.me/api/?results=1&gender=female"}}};- In
App.test.js- Import
testConstants.js - Remove the call to
fetch.mockResponse- Make sure to remove all of it's arguments as well - Between the call to
describeanditadd the following:
- Import
beforeEach(()=>{fetch.resetMocks();genericMockedFetch=fetch.mockResponse(JSON.stringify(testConstants.mocks.responseFromRandomUserApi.success));});These changes move the mock response from randomuser.me in to a file and an object that can be reused as we progress. Should the expected response from the API change at any point we have one place to make the change for all tests.
Now let's add some tests that cover the UserDetails component.
- Add the following new packages to help us out:
- enzyme ^3.2.0
- enzyme-adapter-react-16 ^1.1.0
- jest-enzyme ^4.0.1
- Add a new file in the src folder called
setupTests.jsand add the following to configure the enzyme package to work with React 16:
import"jest-enzyme";import{configure}from"enzyme";importAdapterfrom"enzyme-adapter-react-16";configure({adapter: newAdapter()});- Add a new file in the components folder that will contain the
UserDetailstests (remember the naming convention) - Add the following to the new file:
importReactfrom"react";import{mount}from"enzyme";importtestConstantsfrom"../testConstants";importUserDetailsfrom"./UserDetails";global.fetch=require("jest-fetch-mock");letgenericMockedFetch;letcomponentUnderTest;describe("UserDetails component",()=>{beforeEach(()=>{fetch.resetMocks();genericMockedFetch=fetch.mockResponse(JSON.stringify(testConstants.mocks.responseFromRandomUserApi.success));componentUnderTest=mount(<UserDetails/>);});describe("Initial state",()=>{it("Should make initial call to randomuser.me with gender set to male",()=>{expect(genericMockedFetch).toHaveBeenCalledTimes(1);expect(genericMockedFetch).toHaveBeenLastCalledWith(testConstants.expects.requestsToRandomUserApi.maleUrl);});});});There are a couple of new concepts introduced above which are:
beforeEach- ThebeforeEachfunction is used to specifiy a block of code that is run before eachitfunction within thedescribeblock. This is useful for setting up some state or resetting state like we do withfetch.resetMocks()expect- Theexpectfunction is used to assert that the code executed as required. Should any call toexpectresult in an error or false result then the test will fall.
There is one more concept that we need to cover and that's testing the Async calls.
In the UserDetails test file add the following support functions just after the componentUserTest declaration:
// https://github.com/facebook/jest/issues/2157#issuecomment-279171856functionflushPromises(){returnnewPromise(resolve=>setImmediate(resolve));}functiongetFirstSuccessResultFromMockApiResponse(){returntestConstants.mocks.responseFromRandomUserApi.success.results[0];}With these in place let's add a new describe function and two test to the "Initial state" block.
describe("State.User",()=>{it("Should have the name object set",()=>{returnflushPromises().then(()=>{expect(componentUnderTest.state().user.name.title).toBe(getFirstSuccessResultFromMockApiResponse().name.title);expect(componentUnderTest.state().user.name.first).toBe(getFirstSuccessResultFromMockApiResponse().name.first);expect(componentUnderTest.state().user.name.last).toBe(getFirstSuccessResultFromMockApiResponse().name.last);});});it("Should have the thumbnail set",()=>{returnflushPromises().then(()=>{expect(componentUnderTest.state().user.thumbnail).toBe(getFirstSuccessResultFromMockApiResponse().picture.large);});});});Because our user object is populated upon the return of a result from the randomuser.me API we need to make sure Jest waits for the promise chain to complete with the use of flushPromises. Then we can assert the state object has the values we expect.
Let's add a test to make sure that when the gender is changed and the "New random user" button is pressed the correct request is made to the the API. As this test is not to do with the initial state of the component add it outside the "Initial state" describe function but inside the "UserDetails component" describe function:
it("Should make call to randomuser.me with gender set to female",()=>{componentUnderTest.find("input#rdiFemale").simulate("change");componentUnderTest.find("button#btnNewRandomUser").simulate("click");returnflushPromises().then(()=>{expect(genericMockedFetch).toHaveBeenLastCalledWith(testConstants.expects.requestsToRandomUserApi.femaleUrl);});});Again this test introduces two new concepts in the form of .find and .simulate from the enzyme package.
The find function finds every node in the React DOM that matches the passed selector, in this case "input#rdiFemale". The selector used here is a plain CSS selector. We then call the simulate function passing it the name of the event we want to simulate, in this case "change". The result React will update the state of the component just as if we did the same in a browser. We then find the btnNewRandomUser button and click it. This initiates a call to our mock fetch object which we can then check to make sure the correct url was requested using expect.
Now it's over to you!
- Add a test to make sure that when gender is set to male the correct url is requested
- Add a test to check the users name is set as the heading in the panel