Using Context and HashRouter in React
We will create React app which will use context and HashRouter. The will fetch data from API and supply all app with data. It is an easy way to create an app without the backend using free API as a data source.
Working project https://www.react-routes.ct8.pl/
In this level, you should know how to create react app and knowledge from reactjs.org main concept but it’s not necessary if you are familiar with javascript.
As an API we will use restcountries.eu version v2 which have country flag images for better performance.
The basic concept of our App:
This diagram shows us how our context will provide data across all components.
Let’s dive into it and break on parts:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './App.css';
import { HashRouter, Route } from "react-router-dom";
import { Home, About, Detail } from "./components/pages"
import * as serviceWorker from './serviceWorker';
import { BasicProvider } from './BasicContext';
ReactDOM.render(
<HashRouter>
<BasicProvider>
<Route exact path="/" component={Home} />
<Route path="/about" component={About}/>
</BasicProvider>
</HashRouter>
, document.getElementById('root'));
serviceWorker.unregister();
In our index.js we import our components and BasicProvider which wrap our Route to supply it in data. All are pass to render function.
BasicContext logic:
BasicContext.js
import React, { Component } from "react";
const BasicContext = React.createContext();
class BasicProvider extends Component{
constructor(props){
super(props);
this.state = {
username: 'user',
loading:false
};
}
componentWillMount(){
this.setState({ loading: true });
fetch("https://restcountries.eu/rest/v2/all")
.then(response => response.json())
.then(response => {
this.setState({
exampleItems: response,
loading: false,
initialData: response });
console.log(this.state);
});
}
render(){
const { loading } = this.state
return(
loading === false ?
<BasicContext.Provider value={ this.state }>
{this.props.children}
</BasicContext.Provider>
: <h1>Loading</h1>
)
}
}
const BasicConsumer = BasicContext.Consumer;
export { BasicProvider, BasicConsumer };
In BasicProvider we fetch the data from API and set it to state. Next, in the render function, we check if the data successfully fetched and assign this.state as a value to BasicContext which is defined as a context. Provider which wrap everything which is pass to this component. In our examples we wrap Routes. But the value we use deeper using consumer. Lastly, we create BasicConsumer which we will use to pass a value to the nested component.
Next step is to see how our home and about components are built:
pages.js
import React from "react";
import { MainMenu } from "./menu";
import CountryList from "./CountryList";
import Services from "./Services";
import { BasicConsumer } from "../BasicContext";
class PageTemplate extends React.Component{
constructor(props){
super(props);
}
render(){
return(
<div className="Page">
<MainMenu />
{this.props.children}
</div>
)
}
}
export const Home = () => (
<PageTemplate>
<BasicConsumer>
{({ initialData,exampleItems }) =>
<CountryList test={initialData} test2={exampleItems} />}
</BasicConsumer>
</PageTemplate>
)
export const About = () => (
<PageTemplate>
<BasicConsumer>
{({ initialData,exampleItems }) =>
<Services test={initialData} test2={exampleItems} />}</BasicConsumer>
</PageTemplate>
)
To display navigation in our app we create PageTemplate component which renders our menu component and everything which is pass to it. In Home and About component we use PageTemplate component to have navigation next we use BasicConsumer which keep our state from BasicContext.js. Using deconstructor we use from our state initialData and exampleItems value. Which will provide CountryList and Services component in data. This example shows how helpful can be context and can pass data deep in the nested component without worrying about props.
Next, lest’s see how our navigation looks like.
import React from "react";
import { NavLink } from "react-router-dom";
export const MainMenu = () =>(
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<ul className="navbar-nav">
<li className="nav-item active">
<NavLink className="nav-link" to="/">Home <span className="sr-only">(current)</span></NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/about">Facts</NavLink>
</li>
</ul>
</nav>
);
Here is logic for our navigation basic navLink with to parameter which will work with our Routes define in index.js.
Next, let’s see our CountryList component:
import React, {Component} from "react";
import Land from './Land';
import Pagination from './Pagination';
export default class CountryList extends Component{
constructor(props){
super(props)
this.state = {
initialData:this.props.test,
exampleItems: this.props.test2,
pageOfItems: [],
loading: false,
}
this.onChangePage = this.onChangePage.bind(this);
this.filterList = this.filterList.bind(this);
this.filterContinents = this.filterContinents.bind(this);
}
onChangePage(pageOfItems) {
// update state with new page of items
this.setState({ pageOfItems: pageOfItems });
console.log(this.state);
}
filterList(event) {
var updatedList = this.state.initialData;
updatedList = updatedList.filter(function(item) {
return item.name.toLowerCase().search(event.target.value.toLowerCase()) !== -1;
});
this.setState({ exampleItems: updatedList });
}
filterContinents(event) {
var updatedList = this.state.initialData;
if(event.target.value.toLowerCase() === "all region"){
this.setState({ exampleItems: updatedList });
}else{
updatedList = updatedList.filter(function(item) {
return item.region.toLowerCase().search(event.target.value.toLowerCase()) !== -1;
});
this.setState({ exampleItems: updatedList });
}
}
render(){
const { exampleItems, pageOfItems} = this.state
return (
<div className="container">
<div className="row">
<div className="col-sm-12">
<form>
<div className="form-group">
<label>Search for Country:</label>
<input type="email" className="form-control" placeholder="Search" onChange={this.filterList}/>
</div>
<div className="form-group">
<label>Search by contynent:</label>
<select className="form-control" onChange={this.filterContinents}><option>All region</option>
<option>Polar</option>
<option>Africa</option>
<option>Europe</option>
<option>Asia</option>
<option>Americas</option>
<option>Oceania</option>
</select>
</div>
</form>
</div>
</div>
<div className="row">
{
pageOfItems.map((item,index) => {
return <Land key={index} nation={item} delay={index}/>})
}
</div>
<Pagination
items={exampleItems}
onChangePage={this.onChangePage}
/>
</div>
)
}
}
CountryList component allowed us to display data with pagination and sort it in two way. By continent and by search field. In this example, I use the universal component create by react community with all logic to paginate data with bootstrap styles. To filter data I use two arrays of our data one is as a base to work on it and another does display data. In this scenario, clean data is always stored and doesn’t change.
Let’s see the Land component:
import React, {Component} from "react";
import Modal from './Modal';
export default class Land extends Component {
constructor(props){
super(props)
this.state = {
nation:this.props.nation,
showModal:false,
}
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState({ nation: nextProps.nation});
}
handleShow(){
this.setState({ showModal: true });
}
handleHide(){
this.setState({ showModal:false });
}
render(){
const{ nation,delay } = this.state
const time = delay * 100;
const modal = this.state.showModal ? (
<Modal>
<div className="modal" tabIndex="-1" role="dialog">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Country Details</h5>
<button onClick={this.handleHide} type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">
<p>{nation.name}</p>
<p>Capita: {nation.capital}</p>
<p>Region: {nation.region}</p>
<p>Subregion: {nation.subregion}</p>
<p>Population: {nation.population} people</p>
<p>TopLevelDomain: {nation.topLevelDomain.map(domain => domain).join(", ")}</p>
<p>Timezones: {nation.timezones[0]}</p>
<p>Languages: {nation.languages.map(lang => lang.name + ` ( ${lang.nativeName} )`).join(", ")}</p>
<p>Native name: {nation.nativeName}</p>
<p>Translations:</p>
<p>[es]: {nation.translations['es']}</p>
<p>[ja]: {nation.translations['ja']}</p>
<p>[fa]: {nation.translations['fa']}</p>
<p>Currencies: {nation.currencies.map(curr => `${curr.name} - ${curr.code} (${curr.symbol})`)}</p>
<p></p>
</div>
<div className="modal-footer">
<button onClick={this.handleHide} type="button" className="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</Modal>
) : null;
return (
<div className="col-md-4" style={{transitionDelay: `${time}ms`}} ><div className="card">
<div className="card-body">
<h5 className="card-title">{this.props.nation.name}</h5>
<h6 className="card-subtitle mb-2 text-muted">Capital: {this.props.nation.capital}</h6>
</div>
<img src={this.props.nation.flag} className="card-img-top" alt="..."/>
<ul className="list-group list-group-flush">
<li className="list-group-item">Region: {this.props.nation.region}</li>
<li className="list-group-item">Subregion: {this.props.nation.subregion}</li>
</ul>
<div className="card-body">
<button type="button" className="btn btn-outline-primary" onClick={this.handleShow}>Show more</button>
</div>
</div>
{modal}
</div>
)
}
}
This is a standard component to display data one interesting thing is modal which uses react portal to display modal window and hide it on the onclick event.
Let’s see how the modal component looks like:
import React from "react";
import ReactDOM from "react-dom";
const modalRoot = document.getElementById('modal-root');
export default class Modal extends React.Component{
constructor(props){
super(props);
this.el = document.createElement('div');
}
componentDidMount(){
modalRoot.appendChild(this.el);
}
render(){
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
Here we create div element which will show modal and assign as a value all that will be pass to this component. We must remember to create in an index.html div element with id modal-root as a reference to modal div.
Next, let’s see Services component:
import React from "react";
import Areas from './countryFacts/Areas';
import Population from './countryFacts/Population';
export default class Services extends React.Component{
constructor(props){
super(props);
this.state = {
initialData:this.props.test,
exampleItems: this.props.test2,
loading: false,
}
}
render(){
const { initialData } = this.state
return(
<div className="container">
<Areas initialData={initialData}/>
<Population initialData={initialData}/>
</div>
)
}
}
In this component, we have 2 nested component Areas and Population which will show detail information about our data.
import React from "react";
import { compareValues } from "../../utils/filter";
import { Link } from 'react-router-dom';
export default class Areas extends React.Component{
constructor(props){
super(props);
this.state = {
area:[],
initialData : this.props.initialData,
}
}
componentWillMount(){
var updatedList = this.state.initialData;
updatedList = updatedList.sort(compareValues('area','desc')).slice(0,16).map((item,index) =>{
return {
area:item.area,
name:item.name,
}
}
)
this.setState({ area:updatedList })
}
render(){
const { area } = this.state
return(
<div className="mt-5 mb-5">
<h2>Most areas Country on Earth:</h2>
<ul className="list-group">
{
area ? area.map((item,index) => {
return <li className="list-group-item" key={index}>
<p>{item.name}</p> area: {item.area.toLocaleString()}
</li>
}) : <h1>Loading</h1>}
</ul>
</div>
)
}
}
The interesting thing in this component is a function which compares and sort values depends on parameters set to it. The function looks like this:
utils/filter.js
export const compareValues = function compareValues(key, order='asc'){
return function(a,b){
if((!a.hasOwnProperty(key)) || !b.hasOwnProperty(key)){
return 0;
}
const varA = (typeof a[key] === 'string') ?
a[key].toUpperCase() : a[key];
const varB = (typeof b[key] === 'string') ?
b[key].toUpperCase() : b[key];
let comparison = 0;
if(varA > varB){
comparison = 1;
}else if (varA < varB){
comparison = -1;
}
return(
(order === 'desc') ? (comparison * -1) : comparison
)}
}
It reacts community utils which allowed sort object in define way. When we don’t have many tasks and not familiar with the package like sugar it can solve most development problem.
Last part of our small app is Population component:
import React from "react";
import { compareValues } from "../../utils/filter";
export default class Population extends React.Component{
constructor(props){
super(props);
this.state = {
population:[],
initialData: this.props.initialData
}
}
componentDidMount(){
var updatedList = this.state.initialData;
updatedList = updatedList.sort(compareValues('population','desc')).slice(0,16).map((item) =>{
return {
population:item.population,
name:item.name
}
}
)
this.setState({ population:updatedList })
}
render(){
const { population } = this.state
return(
<div className="mt-5 mb-5">
<h2>Most people in Country on Earth:</h2>
<ul className="list-group">
{
population.map((item,index) => {
return <li className="list-group-item" key={index}><b>{item.name}</b> population: {item.population.toLocaleString()}</li>
}
)}
</ul>
</div>
)
}
}
It’s quite similar to Areas component.
That’s how can look like ready to deploy project remember to add to package.json “homepage”:”.”. This will set the root structure in a proper way. Next npm build and transfer the file to a domain and we can see our project on the web. Project is in bitbucket repo https://bitbucket.org/tetsumote/country-app-2/src/master/ where are more adjust like transition and detail page which are not discussed in this article.
Thanks for attention.