Basic React Redux

Using React Redux

Basic React Redux

by John Vincent


Posted on October 22, 2017



This stuff ends up sprayed everywhere, so let’s create a reference document.

React Redux

Redux

npm react-redux

npm redux

npm redux-thunk

redux-thunk

npm http-proxy-middleware

npm isomorphic-fetch

Fetch

Webpack Dev Server Proxy

Webpack Dev Server

React Redux Docs

React Training

React In-Depth

Dan Abramov

Prestudy

This article requires some know-how. Please see:

React/Redux Node/Express Ecosystem

Hot Cold App

Basic React

Hot Cold Application

Previous articles describe the setup and the ecosystem. This article discusses some key features.

client/src/index.jsx

In particular, notice the store.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import createStore from './store';

// eslint-disable-next-line import/no-named-as-default
import Game from './components/Game';

const initialState = {};
const store = createStore(initialState);

document.addEventListener('DOMContentLoaded', () => {
	ReactDOM.render(
		<Provider store={store}>
			<Game />
		</Provider>,
		document.getElementById('root'),
	);
});

client/src/store.js

Notice the reducers and the middleware.

import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

import makeRootReducer from './reducers/';

export default (initialState = {}) => {
	const middleware = [thunk];
	const enhancers = [];
	const store = createStore(
		makeRootReducer,
		initialState,
		compose(
			applyMiddleware(...middleware),
			...enhancers,
		),
	);
	return store;
};

Combine Reducers

client/src/reducers/index.js

Notice the reducers are combined.

import { combineReducers } from 'redux';

import help from './help.reducer';
import board from './board.reducer';
import fetchTopScore from './fetchTopScore.reducer';

const rootReducer = combineReducers({
	helpReducer: help,
	boardReducer: board,
	fetchTopScoreReducer: fetchTopScore,
});

export default rootReducer;

Combine Actions

client/src/actions/index.js

export * from './board.actions';
export * from './help.actions';
export * from './sendGameScore.actions';
export * from './fetchTopScore.actions';

Help Action and Reducer

Notice how the action defines the method and its signature.

client/src/actions/help.actions.js

import {
	HELP,
	DISMISS_HELP,
} from '../constants/action.types';

export const handleHelp = () => ({
	type: HELP,
});

export const handleDismissHelp = () => ({
	type: DISMISS_HELP,
});

Notice how the reducer handles the state of the help property.

client/src/reducers/help.reducer.js

import {
	HELP,
	DISMISS_HELP,
} from '../constants/action.types';

const initialState = {
	help: false,
};

function help(state = initialState, action) {
	switch (action.type) {
	case HELP: {
		return { help: true };
	}
	case DISMISS_HELP: {
		return { help: false };
	}
	default:
		return state;
	}
}

export default help;

Board Action and Reducer

client/src/actions/board.actions.js

import {
	NEW_GAME,
	USER_GUESSED_NUMBER,
} from '../constants/action.types';

export const handleNewGame = () => ({
	type: NEW_GAME,
});

export const userGuessedNumber = guess => ({
	type: USER_GUESSED_NUMBER,
	guess,
});

Notice the various patterns for altering state.

import {
	USER_GUESSED_NUMBER,
	NEW_GAME,
} from '../constants/action.types';

import Utils from '../utils';

// eslint-disable-next-line no-mixed-operators
const randomInteger = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

const initialState = {
	guessed: [],
	comment: 'Make your Guess!',
	random: 0,
	completed: false,
};

function board(state = initialState, action) {
	switch (action.type) {
	case USER_GUESSED_NUMBER: {
		const comment = Utils.handleComment(state.random, action.guess);
		const completed = state.random === action.guess;
		if (action.guess > 0 && action.guess < 101) {
			return Object.assign({}, state, {
				guessed: [...state.guessed, action.guess],
				comment,
				completed,
			});
		}
		return Object.assign({}, state, {
			comment,
		});
	}
	case NEW_GAME: {
		return Object.assign({}, initialState, {
			random: randomInteger(1, 100),
		});
	}

	default: {
		return state;
	}
	}
}

export default board;

Fetch Top Score Action and Reducer

client/src/actions/fetchTopScore.actions.js

Notice:

  • fetchScore will issue the asynchronous request to /api/score/get
  • the error handling
  • fetchScoreSuccess and fetchScoreError are both defined as actions.
import {
	FETCH_SCORE_SUCCESS,
	FETCH_SCORE_ERROR,
} from '../constants/action.types';

export const fetchScoreSuccess = score => ({
	type: FETCH_SCORE_SUCCESS,
	score,
});

export const fetchScoreError = error => ({
	type: FETCH_SCORE_ERROR,
	error,
});

export const fetchScore = () => (dispatch) => {
	const url = '/api/score/get';
	return fetch(url)
		.then((response) => {
			if (!response.ok) {
				const error = new Error(response.statusText);
				error.response = response;
				throw error;
			}
			return response;
		})
		.then(response => response.json())
		.then(data => dispatch(fetchScoreSuccess(data.score)))
		.catch(error => dispatch(fetchScoreError(error)));
};

client/src/reducers/fetchTopScore.reducer.js

Notice:

  • initial state = 999
  • error state = 99
  • success sets the state from the server.
import {
	FETCH_SCORE_SUCCESS,
	FETCH_SCORE_ERROR,
} from '../constants/action.types';

const initialState = {
	best: 999,
};

function fetchTopScore(state = initialState, action) {
	switch (action.type) {
	case FETCH_SCORE_SUCCESS: {
		return Object.assign({}, state, {
			best: action.score,
		});
	}

	case FETCH_SCORE_ERROR: {
		return Object.assign({}, state, {
			best: 99,
		});
	}
	default: {
		return state;
	}
	}
}

export default fetchTopScore;

Send Score Action

client/src/actions/sendGameScoreScore.actions.js

Notice:

  • there is no reducer.
  • post Json data using body: JSON.stringify({ score })
  • no real error handling as the error is not important.
/* eslint-disable import/prefer-default-export */
export const sendScore = score => () => {
	// console.log(`sendScore, score = ${score}`);
	const data = {
		headers: {
			Accept: 'application/json',
			'Content-Type': 'application/json; charset=utf-8',
		},
		method: 'POST',
		body: JSON.stringify({ score }),
	};
	// console.log(data);
	const url = '/api/score/send';
	return fetch(url, data)
		.then((response) => {
			if (!response.ok) {
				const error = new Error(response.statusText);
				error.response = response;
				throw error;
			}
			return response;
		})
		/* eslint-disable no-console */
		.then((success) => { console.log(success); })
		.catch(error => console.log(error));
};