Material UI is one of the most popular Material Design libraries for React. It has the basic Material Design UI elements, such as inputs, cards, grids, tables, navigation, toolbar, dropdowns, fonts, etc. Here’s the full list.
It is available as a Node package. To install it, run npm i @material-ui/core
. Then, you can import them into your component files when you need them.
In this piece, I will make an app with React and Material UI that uses the Dog API.
To create a new React app, use the create-react-app
code generator, made by the developers of React. Here are the README and the full documentation.
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 the 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();
The file above is where the reducers are mapped to the states. With the combineReducers
function called, the store is created, which is then passed into the app, where the mapStateToProps
will make the state available to the component as props
.
The mapDispatchToProps
allows you to set state in your component via a function in the props, as you will see below.
We add reducers to store state in a centrally available location. The states of our app are set here.
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 };ima
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 };
In actionCreators.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 HomePage from './HomePage';
import BreedsPage from './BreedsPage';
import SubBreedsPage from './SubBreedsPage';
import AppBar from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/AppBar';
import Toolbar from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/Toolbar';
import Typography from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/Typography';
import Button from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/Button';
import IconButton from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/IconButton';
import Drawer from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/Drawer';
import List from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/List';
import ListItem from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/ListItem';
import ListItemText from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/ListItemText';
import { makeStyles } from '[@material](http://twitter.com/material "Twitter profile for @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'](https://dog.ceo/api/breeds/list/all%27))
.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="/" exact component={HomePage} />
<Route path="/breeds/" component={BreedsPage} />
<Route path="/subbreeds/" component={SubBreedsPage} />
</Router>
</div>
);
}
const mapStateToProps = (state) => ({
breeds: state.breeds
})
const mapDispatchToProps = (dispatch) => ({
setBreeds: breeds => dispatch(setBreeds(breeds))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
Notice that, in the code above, we imported all the widgets that we need into the component file and then we include it in the return
statement.
For styling, we import the makeStyles
function and put an object in it. We style it by providing CSS-like attributes and values.
The top-level keys are the classes. They will be created when the imported useStyles()
is called. Then, we can use it by referencing classes.className
.
For example, classes.menuButton
would be used for applying the menuButton
class to the button above.
The code above has Hooks.
const [initialized, setInitialized] = useState(false);
const [state, setState] = useState({
openDrawer: false
});
These functions are equivalent to setState
functions in class-based components.
The first element in the array (initialized
) is equivalent to this.state.initialized
and setInitialized
is equivalent to a function that calls this.setState({initialized: initializedValue});
in a class-based component.
Hooks only works with function-based components. This has the benefit of writing fewer lines of code to achieve the same effect of setting state.
Also note that we have this in the above component:
useEffect(() => {
if (!initialized) {
getBreeds();
}
});
As we don’t have componentDidMount
like in class-based components, we have to check if the component is loaded with our own flag.
The getBreeds
function sets the initialized
flag to true
once it’s run successfully, so that the getBreeds
function will not run repeatedly forever. useEffect
is a function that is run during every render, so be careful not to put necessary code in there.
Note the connect
function at the end of the file above. This where the state connects to the component. setBreeds
is a function which returns a plain object with the action type and the payload.
This allows the reducer to set the state, according to the type
field, which in this case would be SET_BREEDS
or SET_IMAGES
. The state will be set, returned, and the new state will be available via props.breeds
for breeds
.
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](http://twitter.com/material "Twitter profile for @material")-ui/core/InputLabel';
import MenuItem from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/MenuItem';
import FormControl from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/FormControl';
import Select from '[@material](http://twitter.com/material "Twitter profile for @material")-ui/core/Select';
import { makeStyles } from '[@material](http://twitter.com/material "Twitter profile for @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`](https://dog.ceo/api/breed/$%7Bbreed%7D/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);
In the end, you get the screen below when you go to [http://localhost:](http://localhost/)3000/breeds
: