Categories
JavaScript React

How To Use Hooks in React

Spread the love

The latest version of React (v16.8 or later) introduced Hooks, which allow us to set state more simply than before. It also makes function components smart, having the equivalent functionality and lifecycle as class-based components.

State management in a single-page app is also important. Without centralized state management, a lot of data has to be passed directly between components, which becomes unmaintainable and confusing very quickly.

Flux architecture remedies this by making apps store the state in a centralized location and the states are stored as immutable objects to prevent accidental modification.

Redux is one of the most popular libraries for state management and, with React Redux, it can connect your Redux store directly to your components, as you will see below.

In this piece, I will create an app with React and React Redux in the simplest way possible.

Create a New React App

Use the create-react-app code generator, created by the developers of React.

Here is the README and full documentation for create-react-app.

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. This will create 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 in 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 states.

As the combineReducers function is 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 create 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 };

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 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 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 work with function-based components. The benefit is 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 we do 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 has run successfully so that the getBreeds function will not repeatedly run 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 with 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 app, which will be used by React Router for routing.

First, we create a page for displaying breeds, we will 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);

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 *