DEV Community

John Au-Yeung
John Au-Yeung

Posted on

How To Use Redux With React

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

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);

Top comments (5)

Collapse
 
garrisond profile image
Ihor Dotsenko

Use useDispatch and useSelector instead of connect.

It's a pity that you suggest to use connect in 2020 🥺

Collapse
 
dance2die profile image
Sung M. Kim • Edited

Redux hooks have ups and downs compared to connect.

connect lets you decouple your component (making it resuable) but a lot of times that abstraction might not be needed. Then you'd use useDispatch/useSelector. But hooks are really darn easy though, and more readable even though it's usable only within function components.

So the bottomline is knowing both ways would help understand trade-offs and when to use each one.

Collapse
 
aumayeung profile image
John Au-Yeung

Yea. The hooks have to be in the component. This is inherent to using hooks instead of connect.

Collapse
 
garrisond profile image
Ihor Dotsenko
Collapse
 
aumayeung profile image
John Au-Yeung

Thanks for pointing that out.

I have to update that.