In this project, we'll create a small counter application using React and Redux. We'll also include extra functionality for undo/redo actions.
forkandclonethis repository.cdinto the project root.- Run
npm installto fetch the project dependencies. - Run
npm startto spin up a development server.
In this step, we'll install some new dependencies, create a reducer, and create a Redux store.
- Install
redux - Open
./src/store.js - Import
createStorefrom Redux. - Create an initial state with a
currentValueproperty for our counter. - Write a simple reducer.
- Create and export a Redux store.
./src/store.js
import { createStore } from "redux";
const initialState = { currentValue: 0 };
function counter( state = initialState, action ) {
return state;
}
export default createStore(counter);In this step, we'll give our Counter component access to the store.
- Open
./src/Counter.js. - Import
storefrom./src/store.js. - Setup state for
Counter.- State should have a property called
store.- Use the
getStatemethod to copy the Redux state to thestoreproperty.
- Use the
- State should have a property called
- Destructure
currentValuefrom state in yourrendermethod. - Update the
h1to use thecurrentValue. - Update the
JSON.stringifymethod to display thestoreproperty on state.
./src/Counter.js
import React, { Component } from "react";
import store from "./src/store";
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
store: store.getState();
}
}
render() {
const {
currentValue
} = this.state.store;
return (
<div className="app">
<section className="counter">
<h1 className="counter__current-value">{currentValue}</h1>
/* lots of jsx */
</section>
<section className="state">
<pre>{JSON.stringify(this.state.store, null, 2)}</pre>
</section>
</div>
);
}
}
export default Counter;In this step, we'll set up Redux to actually execute actions. We'll start by creating action types and implementing increment/decrement logic.
- Open
./src/store.js. - Create and export
INCREMENTandDECREMENTaction types. - Update the reducer to process these actions into state changes.
INCREMENTshould incrementcurrentValueby the givenamount.DECREMENTshould decrementcurrentValueby the givenamount.
./src/store.js
import { createStore } from 'redux';
const initialState = { currentValue: 0 };
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { currentValue: state.currentValue + action.amount };
case DECREMENT:
return { currentValue: state.currentValue - action.amount };
default:
return state;
}
}
export default createStore(counter);In this step, we'll wire up the Counter component so that it can dispatch actions to our reducer.
- Open
./src/Counter.js. - Import the
INCREMENTandDECREMENTaction types. - Create an
incrementand adecrementmethod.- The methods should accept an amount parameter.
- The component method should use the Redux
dispatchmethod to send an action to the reducer.- The action should include the action type you imported.
- The action should include the amount.
- Update the
.counter_buttonbuttons to callincrementordecrementwith the correctamount. - In
componentDidMount, use the Reduxsubscribemethod to update local state.- The
subscribemethod will use thegetStatemethod to update.
- The
./src/Counter.js
import React, { Component } from "react";
import store, { INCREMENT, DECREMENT } from "./store.js";
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
store: store.getState()
};
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
}
componentDidMount() {
store.subscribe(() => {
this.setState({
store: store.getState()
});
});
}
increment(amount) {
store.dispatch({ amount, type: INCREMENT });
}
decrement(amount) {
store.dispatch({ amount, type: DECREMENT });
}
render() {
const {
currentValue
} = this.state.store;
return (
<div className="app">
<section className="counter">
<h1 className="counter__current-value">{currentValue}</h1>
<div className="counter__button-wrapper">
<button className="counter__button" onClick={() => this.increment(1)}>
+1
</button>
<button className="counter__button" onClick={() => this.increment(5)}>
+5
</button>
<button className="counter__button" onClick={() => this.decrement(1)}>
-1
</button>
<button className="counter__button" onClick={() => this.decrement(5)}>
-5
</button>
<br />
<button
className="counter__button"
disabled={true}
onClick={() => null}
>
Undo
</button>
<button
className="counter__button"
disabled={true}
onClick={() => null}
>
Redo
</button>
</div>
</section>
<section className="state">
<pre>
{JSON.stringify(this.state.store, null, 2)}
</pre>
</section>
</div>
);
}
}
export default Counter;In this step, we'll implement undo/redo logic into our reducer.
- Open
./src/store.js. - Create and export
UNDOandREDOaction types. - Refactor
initialStateandcounterto handle undo/redo logic.
./src/store.js
import { createStore } from 'redux';
const initialState = {
currentValue: 0,
futureValues: [],
previousValues: []
};
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const UNDO = 'UNDO';
export const REDO = 'REDO';
function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return {
currentValue: state.currentValue + action.amount,
futureValues: [],
previousValues: [state.currentValue, ...state.previousValues]
};
case DECREMENT:
return {
currentValue: state.currentValue - action.amount,
futureValues: [],
previousValues: [state.currentValue, ...state.previousValues]
};
case UNDO:
return {
currentValue: state.previousValues[0],
futureValues: [state.currentValue, ...state.futureValues],
previousValues: state.previousValues.slice(1)
};
case REDO:
return {
currentValue: state.futureValues[0],
futureValues: state.futureValues.slice(1),
previousValues: [state.currentValue, ...state.previousValues]
};
default:
return state;
}
}
export default createStore(counter);In this step, we'll import UNDO and REDO action types into our Counter.js and write methods to dispatch them.
- Open
./src/Counter.js. - Import
UNDOandREDOaction types. - Create an
undoand aredomethod.- The component method should use the Redux
dispatchmethod to send an action to the reducer.- The action should include the action type you imported.
- The component method should use the Redux
- Hook up the
undoandredobuttons to their respective methods. - Destructure
previousValuesandfutureValuesfrom the store in therendermethod. - Update the
disabledattributes of the buttons to usepreviousValuesandfutureValuesrespectively.
./src/Counter.js
import React, { Component } from "react";
import store, { INCREMENT, DECREMENT, UNDO, REDO } from "./store.js";
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
store: store.getState()
};
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
this.undo = this.undo.bind(this);
this.redo = this.redo.bind(this);
}
componentDidMount() {
store.subscribe(() => {
this.setState({
store: store.getState()
});
});
}
increment(amount) {
store.dispatch({ amount, type: INCREMENT });
}
decrement(amount) {
store.dispatch({ amount, type: DECREMENT });
}
undo() {
store.dispatch({ type: UNDO });
}
redo() {
store.dispatch({ type: REDO });
}
render() {
const {
currentValue,
futureValues,
previousValues
} = this.state.store;
return (
<div className="app">
<section className="counter">
<h1 className="counter__current-value">{currentValue}</h1>
<div className="counter__button-wrapper">
<button className="counter__button" onClick={() => this.increment(1)}>
+1
</button>
<button className="counter__button" onClick={() => this.increment(5)}>
+5
</button>
<button className="counter__button" onClick={() => this.decrement(1)}>
-1
</button>
<button className="counter__button" onClick={() => this.decrement(5)}>
-5
</button>
<br />
<button
className="counter__button"
disabled={previousValues.length === 0}
onClick={this.undo}
>
Undo
</button>
<button
className="counter__button"
disabled={futureValues.length === 0}
onClick={this.redo}
>
Redo
</button>
</div>
</section>
<section className="state">
<pre>{JSON.stringify(this.state.store, null, 2)}</pre>
</section>
</div>
);
}
}
export default Counter;If you see a problem or a typo, please fork, make the necessary changes, and create a pull request so we can review your changes and merge them into the master repo and branch.
© DevMountain LLC, 2017. Unauthorized use and/or duplication of this material without express and written permission from DevMountain, LLC is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to DevMountain with appropriate and specific direction to the original content.





