Categories
Material UI

How To Use Material UI to Bring Material Design to React

Spread the love

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:

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *