If you want to make a single-page app with React, you need to add routing to your app. React Router is the most popular routing library for React apps. To use it, you have to add the library via npm.
In this piece, I will create an app with React and React Router in the simplest way possible.
To create a new React app, use the create-react-app code generator made by the developers of React. Here are the README and full documentation.
The app that will be created is an app that displays data from the Dog API.
To create the app, run npx create-react-app and follow the instructions, you will get a new app. Then, you are ready to install React Router.
To install it, run npm i react-router-dom
. After that, install @material-ui/core and axios by running npm i @material-ui/core axios
. Material UI provides a Material Design look to our app and Axios is an HTTP client which works at client-side apps.
In index.js
, we have:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { breedsReducer, imagesReducer } from './reducers';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { combineReducers } from 'redux'
const dogApp = combineReducers({
breeds: breedsReducer,
images: imagesReducer
})
const store = createStore(dogApp)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
serviceWorker.unregister();
First, we add reducers to store state in a centrally available location. We make a file called reducer.js:
import { SET_BREEDS, SET_IMAGES } from './actions';
function breedsReducer(state = {}, action) {
switch (action.type) {
case SET_BREEDS:
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
function imagesReducer(state = [], action) {
switch (action.type) {
case SET_IMAGES:
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state
}
}
export { breedsReducer, imagesReducer };
In actions.js, we add these constants for our Redux actions:
const SET_BREEDS = 'SET_BREEDS';
const SET_IMAGES = 'SET_IMAGES';
export { SET_BREEDS, SET_IMAGES };
InactionCreators.js
, we add:
import { SET_BREEDS, SET_IMAGES } from './actions';
const setBreeds = (breeds) => {
return {
type: SET_BREEDS,
payload: breeds
}
};
const setImages = (images) => {
return {
type: SET_IMAGES,
payload: images
}
};
export { setBreeds, setImages };
In app.js, we change the default code to:
import React, { useState, useEffect } from "react";
import './App.css';
import { setBreeds } from './actionCreators';
import { connect } from 'react-redux';
import { Router, Route, Link } from "react-router-dom";
import BreedsPage from './BreedsPage';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { makeStyles } from '@material-ui/core/styles';
import { createBrowserHistory as createHistory } from 'history'
const history = createHistory();
const axios = require('axios');
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
}));
function App({ setBreeds }) {
const classes = useStyles();
const [initialized, setInitialized] = useState(false);
const [state, setState] = useState({
openDrawer: false
});
const titles = {
'/': 'Dog App',
'/breeds': 'Get Images By Breed - Dog App',
'/subbreeds': 'Get Images By Breed or Sub-Breed - Dog App'
}
history.listen((location, action) => {
document.title = titles[location.pathname];
setState({ openDrawer: false });
});
const toggleDrawer = (open) => event => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setState({ openDrawer: open });
};
const links = {
'Home': '/',
'Breeds': '/breeds',
'Sub-Breeds': '/subbreeds',
}
const getBreeds = () => {
setInitialized(true);
axios.get('https://dog.ceo/api/breeds/list/all')
.then((response) => {
setBreeds(response.data.message);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
});
}
useEffect(() => {
if (!initialized) {
getBreeds();
}
});
return (
<div className="App">
<Router history={history}>
<Drawer anchor="left" open={state.openDrawer} onClose= {toggleDrawer(false)}>
<List>
<ListItem button>
<h2><b>Dog App</b></h2>
</ListItem>
{Object.keys(links).map((text) => (
<ListItem button key={text}>
<Link to={links[text]}>
<ListItemText primary={text} />
</Link>
</ListItem>
))}
</List>
</Drawer>
<AppBar position="static">
<Toolbar>
<IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleDrawer(true)}>
<i className="material-icons">menu</i>
</IconButton>
<Typography variant="h6" className={classes.title}>Dog App</Typography>
</Toolbar>
</AppBar>
<Route path="/breeds/" component={BreedsPage} />
</Router>
</div>
);
}
const mapStateToProps = (state) => ({
breeds: state.breeds
})
const mapDispatchToProps = (dispatch) => ({
setBreeds: breeds => dispatch(setBreeds(breeds))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
The <Route path=”/breeds/” component={BreedsPage} />
is where the route is defined. It must be inside <Router history={history}></Router>
.
This is the routing part of our application. With this, we can go to the page when we type in http://localhost:3000/breeds.
This block sets the title and hides the app drawer on the left when the route changes:
history.listen((location, action) => {
document.title = titles[location.pathname];
setState({ openDrawer: false });
});
We now create the pages for our apps which will use React Router for routing. First, we create a page for displaying breeds, we call it BreedPage.js.
The code will look like this:
import React from 'react';
import './BreedsPage.css';
import { setImages } from './actionCreators';
import { connect } from 'react-redux';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { makeStyles } from '@material-ui/core/styles';
import ImagesBox from './ImagesBox';
const axios = require('axios');
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
width: '90vw',
},
}));
function BreedsPage({ breeds, setImages }) {
const classes = useStyles();
const [state, setState] = React.useState({
breed: '',
});
const [initialized, setInitialized] = React.useState(false);
const handleChange = name => event => {
setState({...state, [name]: event.target.value,});
if (name == 'breed') {
getImagesByBreed(event.target.value);
}
};
const getImagesByBreed = (breed) => {
axios.get(`https://dog.ceo/api/breed/${breed}/images`)
.then((response) => {
setImages(response.data.message);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
});
}
React.useEffect(() => {
if (!initialized) {
setInitialized(true);
setImages([]);
}
});
return (
<div className="App">
<h1>Get Images By Breed</h1>
<form>
<FormControl className={classes.formControl}>
<InputLabel>Breed</InputLabel>
<Select
value={state.breed}
onChange={handleChange('breed')}
>
{Object.keys(breeds || {}).map(b =>
<MenuItem value={b} key={b}>{b}</MenuItem>
)}
</Select>
</FormControl>
<ImagesBox></ImagesBox>
</form>
</div>
);
}
const mapStateToProps = state => {
return {
breeds: state.breeds,
images: state.images
}
}
const mapDispatchToProps = dispatch => ({
setImages: images => dispatch(setImages(images))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(BreedsPage);