React under the Hood
A look behind the curtain of React Starters like:
React is often said to be easy to learn, but impossible to set up in an dev environment. Once you start reading about it, you will be faced by an exhausting amount of choices that you have to make, before you can move on to actual coding. Starter Packages, like the ones named above, give a quick access to the React world. Let's take a look into that black box now.
- 01 Pure React
- 02 JSX and Babel
- 03 Webpack
- 04 React Components
- 05 Adding React Icons
- 06 Working with Props
- 07 Working with State
01 Pure React
Create a file /dist/index.js with the following React code:
const { createElement } = React
const { render } = ReactDOM
const title = createElement(
'h1',
{id: 'title', className: 'header'},
'Hello World'
)
render(
title,
document.getElementById('react-container')
)
The <title /> component uses the createElement function from React to create a h1 header with the css class header, an id title and a text string Hello World.
The ReactDom render function will then render it into the div container with the id react-container.
Now we need to create a html page called /dist/index.html that contains the container with named id:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<!-- Force latest available IE rendering engine and Chrome Frame (if installed) -->
<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>
<!-- Mobile Screen Resizing -->
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<!-- HTML5 Shim for IE 6-8 -->
<!--[if lt IE 9]>
<script src='http://html5shiv.googlecode.com/svn/trunk/html5.js'></script>
<![endif]-->
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js'></script>
<meta charset='UTF-8'>
<title>Hello World with React</title>
</head>
<body>
<!--[if lt IE 8]>
<section class='container'>
Did you know that your web browser is a bit old? Some of the content on this site might not work right
as a result. <a href='http://whatbrowser.org'>Upgrade your browser</a> for a faster, better, and safer
web experience.
</section>
<![endif]-->
<div id='react-container'></div>
<script src='./index.js'></script>
</body>
</html>
We add React and ReactDOM directly via CDN and link our index.js inside the body tag.
Now we need to put our React app onto a webserver - for testing, we will use the npm package httpster to serve our files:
npm install httpster -g
Now start the webserver with the port and directory flag:
httpster -p 3000 -d /e/react-under-the-hood/dist
Our app can now be accessed with a webbrowser on http://localhost:3000
We can easily style our title component by creating a style component:
const style = {
backgroundColor: 'purple',
color: 'teal',
fontFamily: 'verdana'
}
And assigning the style to our component:
const title = createElement(
'h1',
{id: 'title', className: 'header', style: style},
'Hello World'
)
02 JSX and Babel
React offers a way to write our mark-up directly inside the Javascript component - called JSX. The title component written in JSX looks like this:
render(
<h1 id = 'title'
className = 'header'
style = {style}>
Hello World
</h1>,
document.getElementById('react-container')
)
Since our webbrowser don't understand JSX, we will have to transpile it to pure Javascript using Babel - this can be quickly done with the babel-cli transpiler. Let us first initialize our node project by npm init -y then install the babel-cli both globally as well as a development dependency inside our project:
Transpilation
npm install -g babel-cli
npm install --save-dev babel-cli
now create a folder called src inside the root dir and move the index.js file into it - since we want to use Babel to transpile all JSX files from the source directory and copy them to the distribution directory, where they can be picked up and served by our webserver.
Now we need to configure Babel to transpile JSX and all latest and proposed versions of ECMA Script, by adding a file .babelrc inside the root director:
{
'presets': ['latest', 'react', 'stage-0']
}
Now we need to install those presets as dev-dependencies be advised: we later throw out babel-preset-latest babel-preset-stage-0 and replace it with babel-preset-env to work with webpack 3! :
npm install --save-dev babel-preset-react babel-preset-latest babel-preset-stage-0
We can now use the cli tool to transpile our JSX source file and create the browser-readable bundle.js file from it:
babel ./src/index.js --out-file ./dist/bundle.js
Now open index.html inside the /dist directory and change the index.js to bundle.js. Reloading our webserver will now show our app again. To make our life easier we will add the httpster call as our npm start script inside the package.json file - then start your webserver with npm start
'scripts': {
'start': 'httpster -p 3000 -d ./dist'
}
We are now able to write our React code in JSX as well as to use ES2015 or ES2017 syntax inside our source files. Babel will transpile them into browser-friendly code inside /dist/bundle.js. But now we don't want to do this by hand, every time we made a small edit on our page - we need an automation solution for this process.
03 Webpack
Webpak is a module bundler, that enables us to create static files from our React code. We can use it to automate processes like the Babel transpiling and use it to serve our app in an hot-reloading dev-server environment.
First we need to add a Webpack configuration file inside the root directory - webpack.config.js:
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: path.resolve(__dirname, './src/index.js'),
devServer: {
contentBase: path.resolve(__dirname, './dist'),
port: 3000,
inline: true
},
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
}]
},
output: {
path: path.resolve(__dirname, './dist/assets/'),
filename: 'bundle.js',
publicPath: 'assets'
},
};
Now we want to install the latest version of Webpack together with the babel-loader & presets, as well as the Webpack Dev-Server to host our files:
npm install --save-dev webpack babel-loader babel-core babel-preset-env webpack-dev-server
We can create an npm script to start webpack from inside the repository (a global installation is not recommended). The start scripts hosts our webapp, according to the devServer configuration inside webpack.config.js. The build script takes all js files (node_modules excluded), babel-transpiles them with the babel-loader, and puts them bundled into the ./dist/assets directory. And the watch script will watch the directories for changes and starts up the loader automatically, when we saved an edit.
'scripts': {
'start': 'webpack-dev-server --open',
'watch': 'webpack --watch',
'build': 'webpack --config webpack.config.js'
}
We can now run our build process with npm run build / npm run watch and start our devServer with npm start.
Let us now use Webpack to load our react dependencies - instead of linking them into our HTML page. To do this we first have to install React to the project:
npm install --save react react-dom
Loading JSON
And to demonstrate the function of module loading, we want to use some JSON data, being loaded into our react app by Webpack:
npm install --save-dev json-loader
Lets add the JSON loader to our Webpack config file:
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
},
{
test: /\.json$/,
exclude: /(node_modules)/,
use: {
loader: 'json-loader'
}
}
]
},
And create a exciting JSON file ./src/title.json :
{
'data1': 'first data',
'data2': 'second data'
}
And create a JSX module that uses this data in ./src/lib.js :
import React from 'react'
import text from './titles.json'
export const data1 = (
<h1 id='title'
className='header'
style={{backgroundColor: 'teal', color: 'purple'}}>
{text.data1}
</h1>
)
export const data2 = (
<h1 id='title'
className='header'
style={{backgroundColor: 'purple', color: 'teal'}}>
{text.data2}
</h1>
)
We are now using the module import statement to import React from the installed React dependency, as well as our own JSON file. This is a function that is not yet integrated in JavaScript but is available thanks to Webpack and Babel. Now we can rewrite our ./src/index.js file to receive the module that we just created:
import React from 'react'
import { render } from 'react-dom'
import {data1, data2} from './lib'
render(
<div>
{data1}
{data2}
</div>,
document.getElementById('react-container')
)
Notice that we need to import react-dom here, since render is not part of react.
Adding SASS
The same principle can be applied to add styles to our react app app - lets try to add some SASS to our app with the Kraken-Sass boilerplate:
First we want to install the Webpack loaders for the job of preprocess the source SASS into proper CSS:
npm install --save-dev style-loader css-loader sass-loader
You will get a warning, that sass-loader requires another dependency called node-sass, which is a library that provides binding for Node.js to LibSass, the C version of the popular stylesheet preprocessor, Sass. This, on the other hand, requires - under Windows - the installation of the Windows Build Tools:
npm install --g --production windows-build-tools
Go and get yourself a cup of coffee - as this is going to take a while ¯\(ツ)/¯
Once this is through, continue with node-sass:
npm install --save-dev node-sass
Then add the SASS loaders to our Webpack config:
{
test: /\.scss$/,
exclude: /(node_modules)/,
use: [{
loader: 'style-loader' // creates style nodes from JS strings
}, {
loader: 'css-loader' // translates CSS into CommonJS
}, {
loader: 'sass-loader' // compiles Sass to CSS
}]
}
Download the master.zip from kraken-sass and unzip the kraken.scss file (together with the lib folder - that contains all the scss components) to ./src/assets/sass.
Now we can import the kraken-sass styles into our ./src/index.js component:
import React from 'react'
import { render } from 'react-dom'
import {data1, data2} from './lib'
import './assets/sass/kraken.scss'
render(
<div>
<h1>Webpack Styling</h1>
<h4>With Kraken-Sass Boilerplate</h4>
<button className='btn btn-blue btn-block'> {data1} </button>
<button className='btn btn-blue btn-block'> {data2} </button>
</div>,
document.getElementById('react-container')
)
As we can see by now - react allows us to create a collection of separate JSX components and CSS modules that offer isolation to our app logic and component styles. Each piece is a building block, that is then imported into our ./src/index.js react interface and bundled & transpiled by Webpack/Babel into a browser-conform website. Lets clean up our folder structure to show the separation between main pages (index.js) and components and modules that can be re-used in every page (make sure to also change the relative links inside each file):
04 React Components
Let us now build a small component that list [how many countries] there are in the world, how many we have visited and how much we want to visit in total. We can also add a little bit of math to it and calculate the completion percentage of our endeavor. When you look at code examples on Github, you will find a couple of different ways to write such a component. The first, and oldest one uses the createClass syntax and will no longer work in react v16 - React 15.5.0: React.createClass officially deprecated.
import React from 'react'
import '../assets/sass/kraken.scss'
// cannot be rendered inside react 16 - you need to downgrade your react and reactDom version to react < 15.5
export const CountriesVisited = React.createClass({
percentToDecimal(decimal) {
return ((decimal * 100) + '%')
},
calcGoalProgress(total, goal) {
return this.percentToDecimal(total/goal)
},
render() {
return (
<div className='countries-visited'>
<hr/>
<h3>The React.createClass Syntax is no longer supported inside React v16!</h3>
<div className='total-contries'>
<span>{this.props.total} </span>
<span>total countries</span>
</div>
<div className='visited'>
<span>{this.props.visited} </span>
<span>visited countries</span>
</div>
<div className='wish-list'>
<span>{this.props.liked} </span>
<span>countries on wishlist</span>
</div>
<div>
<span>
{this.calcGoalProgress(
this.props.total,
this.props.goal
)}
</span>
</div>
</div>
)
}
})
Here we are working with props (properties) that are passed down from the parent component in ./src/index.js. That means, if we want to add this component, we also have to inject those properties. If you add the following to the render function inside the parent component (see further below, on how to implement it):
<CountriesVisited total={196}
visited={86}
liked={186}
goal={96}/>
and given that you are using react < v16, our component would be rendered inside our main component, just as our buttons did in the example before.
Just in case that you stumble over a code bit somewhere that looks like this... Now lets bring it up to speed and rewrite the component with ES16 syntax!
ES6 Class Syntax
export class MyComponent extends Component {
render() {
return (
<div>{props.title}</div>
)
}
}
import { Component } from 'react'
import '../assets/sass/kraken.scss'
import '../assets/sass/ui.scss'
export class CountriesVisitedES6 extends Component {
percentToDecimal (decimal) {
return ((decimal * 100) + '%')
}
calcTravelProgress (visited, goal) {
return this.percentToDecimal (visited/goal)
}
render() {
return (
<div>
<hr/>
<div className='grid-full space-bottom text-center'>
<span>{this.props.total} </span>
<span>total countries </span>
<Globe className='text-tall' />
</div>
<div className='grid-half text-center space-bottom'>
<span>{this.props.visited} </span>
<span>visited countries </span>
<Landing className='text-tall' />
</div>
<div className='grid-half space-bottom text-center'>
<span>{this.props.liked} </span>
<span>countries on wishlist </span>
<Heart className='text-tall' />
</div>
<div className='grid-full space-bottom text-center'>
<span>{this.calcTravelProgress (
this.props.visited,
this.props.goal
)}
</span>
<span> Completion </span>
<Checked className='text-tall' />
</div>
<p className='text-small text-muted'>This Data is calculated inside an ES6 Class Component</p>
</div>
)
}
}
One thing to point out, is that, written in this ES6 class syntax, we no longer need to wrap our component in parenthesis and the different methods inside the component don't have to be separated by commas anymore. But we can go even one step further and turned it into a Stateless functional component.
Stateless Functions
Stateless functional component - just as their name implies - are components that are created by a function. They do not have access to state - you cannot use this to access variables. They follow the following structure:
const MyComponent = (props) => (
<div>{props.title}</div>
)
They take in property information from their parent component and return (unrendered) JSX Elements to them. That means, that we also do not have to import react anymore. But local methods - like our calculations - have to be removed from the component and put into their own functions:
import '../assets/sass/kraken.scss'
import '../assets/sass/ui.scss'
const percentToDecimal = (decimal) => {
return ((decimal * 100) + '%')
}
const calcTravelProgress = (visited, goal) => {
return percentToDecimal (visited/goal)
}
export const CountriesVisitedStateless = (props) => (
<div>
<div className='grid-full space-bottom text-center'>
<span>{props.total} </span>
<span>total countries</span>
</div>
<div className='grid-half text-center space-bottom'>
<span>{props.visited} </span>
<span>visited countries</span>
</div>
<div className='grid-half space-bottom text-center'>
<span>{props.liked} </span>
<span>countries on wishlist</span>
</div>
<div className='grid-full space-bottom text-center'>
<span>{calcTravelProgress (
props.visited,
props.goal
)}
</span>
<span> Completion</span>
</div>
</div>
)
To destructure this a little bit more, we can declaratively state only the object keys that we actually want to use from props - this way we don't have to add the props. in front anymore:
import '../assets/sass/kraken.scss'
import '../assets/sass/ui.scss'
const percentToDecimal = (decimal) => {
return ((decimal * 100) + '%')
}
const calcTravelProgress = (visited, goal) => {
return percentToDecimal (visited/goal)
}
export const CountriesVisitedStateless = ({ total, visited, liked, goal }) => (
<div>
<hr/>
<div className='grid-full space-bottom text-center'>
<span>{total} </span>
<span> total </span>
<Globe className='text-tall' />
</div>
<div className='grid-half text-center space-bottom'>
<span>{visited} </span>
<span> visited </span>
<Landing className='text-tall' />
</div>
<div className='grid-half space-bottom text-center'>
<span className='text-tall'>{liked} </span>
<span> wishlist </span>
<Heart className='text-tall' />
</div>
<div className='grid-full space-bottom text-center'>
<span>{calcTravelProgress (
visited,
goal
)}
</span>
<Checked className='text-tall' /><br/><br/>
</div>
<p className='text-small text-muted'>This Data is calculated inside a stateless Component</p>
</div>
)
05 Adding React Icons
The React-Icons module allows you to include popular icons in your React projects. The module can be installed by npm
React-Icons can be imported to our component:
import Globe from 'react-icons/lib/go/globe'
import Landing from 'react-icons/lib/md/flight-land'
import Heart from 'react-icons/lib/go/heart'
import Checked from 'react-icons/lib/ti/input-checked'
And simply be added to as a child component:
<Globe />
<Landing />
<Heart />
<Checked />
06 Working with Props
Lets call a new component CountryList inside ./src/index.js and give it some props - in form of an array of objects:
<CountryList countries= {
[
{
country: 'Japan',
date: new Date ('10/19/2010'),
visited: true,
liked: true
},
{
country: 'Taiwan',
date: new Date ('12/12/2006'),
visited: true,
liked: true
},
{
country: 'China',
date: new Date ('10/20/2010'),
visited: true,
liked: true
}
]
}/>
Now create this component in ./src/components/country-list.js :
import Landing from 'react-icons/lib/md/flight-land'
import Heart from 'react-icons/lib/go/heart'
import { CountryRow } from './country-row'
export const CountryList = ({countries}) => (
<table>
<thead>
<tr>
<th>Date</th>
<th>Country</th>
<th>Visited</th>
<th>Liked</th>
</tr>
</thead>
<tbody>
{countries.map((country, i) =>
<CountryRow key={i}
country={country.country}
date={country.date}
visited={country.visited}
liked={country.liked}/>
)}
</tbody>
</table>
)
We created another nested child component inside to create the actual table body of our country list. All the props that were given to us inside index.js have now be handed further down to the CountryRow component. And we have a map function wrapped around it to go through all values inside the countries array - and create a row for every entry. Which is an excellent time to introduce The Spread Operator - three dots that changed the world - to clean up our code:
export const CountryList = ({countries}) => (
<div className='grid-full space-bottom space-top'>
<br/><br/>
<table className='grid-full space-bottom space-top float-center'>
<thead>
<tr>
<th>Date</th>
<th>Country</th>
<th>Visited</th>
<th>Liked</th>
</tr>
</thead>
<tbody>
{countries.map((country, i) =>
<CountryRow key={i} {...country} />
)}
</tbody>
</table>
</div>
)
And the row component, that we already imported in the list component (above), can now use the props: country, date, visited, liked from the countries array to populate the rows of our table inside ./src/components/countru-row.js :
import Landing from 'react-icons/lib/md/flight-land'
import Heart from 'react-icons/lib/go/heart'
export const CountryRow = ({country, date, visited, liked}) => (
<tr>
<td>
{ date.getMonth() +1 } / { date.getDate() } / { date.getFullYear() }
</td>
<td>
{ country }
</td>
<td>
{ (visited) ? <Landing /> : null }
</td>
<td>
{ (liked) ? <Heart /> : null }
</td>
</tr>
)
We added icons again to show, whether we visited and liked a country. The syntax of the Inline If-Else Statement (visited) ? <Landing /> : null reads like: if visited is true, display the Landing react-icon, otherwise, don't.
Default Props
Default props populate our child component with properties, when they are not provided by the parent component. Lets add them to our country-visited component, to see how this looks like for the createClass syntax, the ES6 Class syntax and inside a stateless component:
createClass (see .src/components/countries-visited-createClass.js)
The default props are just added like our custom methods before inside the component, by the getDefaultProps function:
export const CountriesVisited = React.createClass({
getDefaultProps() {
return {
total : 196,
visited : 50,
liked : 100,
goal : 99
}
}, [...] })
ES6 Class (see .src/components/countries-visited-ES6.js)
In case of an ES6 class we have to add default props to the class instance (written below the component itself):
CountriesVisitedES6.defaultProps = {
total : 196,
visited : 50,
liked : 100,
goal : 99
}
stateless (see .src/components/countries-visited-stateless.js)
the way above can also be used for stateless components - just copy&paste. But you can also assign default values to the props that you give the component - they will only be used if no props are provided by the parent component:
export const CountriesVisitedStateless = ({ total=196, visited=50, liked=100, goal=99 }) => ([...])
PropType Validation
As your app grows, you can catch a lot of bugs with typechecking. To run typechecking on the props for a component, you can assign the special propTypes property:
createClass (see .src/components/countries-visited-createClass.js)
import { createClass, PropTypes } from 'react'
export const CountriesVisited = React.createClass({
propTypes() {
total : propTypes.number,
visited : propTypes.number,
liked : propTypes.number,
goal : propTypes.number
}, [...] })
ES6 Class (see .src/components/countries-visited-ES6.js)
In case of an ES6 class we have to add propsTypes to the class instance (written below the component itself). For React >15.5 we also need to install npm install --save prop-types separately and import it at the top of the file !
import { Component } from 'react'
import PropTypes from 'prop-types';
[...]
CountriesVisitedES6.defaultProps = {
total : propTypes.number.isRequired,
visited : propTypes.number.isRequired,
liked : propTypes.number,
goal : propTypes.number.isRequired
}
You can test it by 'feeding' your component, e.g. a Boolean instead of a Number - you will now get an error message inside your console (the same would happen, if you remove a prop that is tagged as .isRequired):
stateless (see .src/components/countries-visited-stateless.js)
the way above can also be used for stateless components - just copy&paste:
import PropTypes from 'prop-types';
[...]
CountriesVisitedES6.defaultProps = {
total : propTypes.number.isRequired,
visited : propTypes.number.isRequired,
liked : propTypes.number,
goal : propTypes.number.isRequired
}
We can now add Type Validation to our two list / row components:
stateless (see .src/components/country-row.js)
import PropTypes from 'prop-types';
[...]
CountryRow.propTypes = {
country: PropTypes.string.isRequired,
date: PropTypes.instanceOf(Date).isRequired,
visited: PropTypes.bool,
liked: PropTypes.bool,
}
stateless (see .src/components/country-list.js)
import PropTypes from 'prop-types';
[...]
CountryList.propTypes = {
countries: PropTypes.array.isRequired,
}
Beside those default Type Checks, we can also employ Custom Validations - e.g. is countries an array - if yes - does the array countries have at least 1 entry:
CountryList.propTypes = {
countries: function(props) {
if(!Array.isArray(props.countries)) {
return new Error (
'Country List has to be an Array!'
)
} else if (!props.countries.length) {
return new Error (
'Country List must have at least one record!'
)
} else {
return null
}
}
}
07 Working with State
So far we used props to pass down data into our components. The other way to do this is by handling the state of a component. Here - when you are not using state managements like Redux - it is very important that we limit the amount of components that handle our components state. To do this, we want to create another component in ./src/components/app-createClass.js called <App />:
import {createClass} from 'react'
import { CountriesVisitedES6 } from './countries-visited-es6'
import { CountryList } from './country-list'
export const App = createClass({
getInitialState() {
return {
countries: [
{
country: 'Japan',
date: new Date ('10/19/2010'),
visited: true,
liked: true
},
{
country: 'Taiwan',
date: new Date ('12/12/2006'),
visited: true,
liked: true
},
{
country: 'China',
date: new Date ('10/20/2010'),
visited: true,
liked: true
}
]
}
},
render() {
return (
<div className='app'>
<CountryList countries={this.state.countries} />
<CountriesVisitedES6 total={196}
visited={86}
liked={186} />
</div>
)
}
})
We can remove the CountryList and CountriesVisitedES6 components from ./src/index.js amd import App now, as their new parent component. The [countries] array is now made available t our CountryList component by the getInitialState function in <App />.
We can also add a filter function that allows us to filter all countries, that are either visited or liked to
countCountries(filter) {
return this.state.countries.filter(function(country) {
if(filter {
return country[filter]
} else {
return country
}
})
}).length
},
And again, we can use the the inline If/Else syntax to clean up our countCountries function:
countCountries(filter) {
return this.state.countries.filter(
(country) => (filter) ? country[filter] : country
).length
}
This filter takes the countries array, takes all countries and returns them - unless the function call uses a filter string. In this case only the countries are returned that fulfill the filter condition.
The function call and filter condition can then be applied in our render function:
render() {
return (
<div className='app'>
<CountryList countries={this.state.countries} />
<CountriesVisitedES6 total={this.countCountries()}
visited={this.countCountries('visited')}
liked={this.countCountries('liked')} />
</div>
)
}
For total we don't use a filter - we want to display the number of all countries. But for visited and liked we only want to count countries that have the corresponding variable set to true. This way we now managed to get rid of the hard-coded numbers. And instead started to simply count those countries inside the state of our component.
Now lets re-write our App component using ES6 classes in ./src/components/app-es6.js:
import {Component} from 'react'
import { CountriesVisitedES6 } from './countries-visited-es6'
import { CountryList } from './country-list'
export class App extends Component {
constructor(props) {
super(props)
this.state = {
countries: [
{
country: 'Japan',
date: new Date ('10/19/2010'),
visited: true,
liked: true
},
{
country: 'Taiwan',
date: new Date ('12/12/2006'),
visited: true,
liked: true
},
{
country: 'China',
date: new Date ('10/20/2010'),
visited: true,
liked: true
},
{
country: 'Austria',
date: new Date ('10/20/2010'),
visited: true,
liked: false
}
]
}
}
countCountries(filter) {
return this.state.countries.filter(
(country) => (filter) ? country[filter] : country
).length
}
render() {
return (
<div className='app'>
<CountryList countries={this.state.countries} />
<CountriesVisitedES6 total={this.countCountries()}
visited={this.countCountries('visited')}
liked={this.countCountries('liked')} />
</div>
)
}
}