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
reduxandreact-redux - Create an initial state
src/ducks/counter.js - Write a simple reducer in
src/ducks/counter.js - Create a Redux store in
src/store.js
./src/ducks/counter.js
const initialState = { currentValue: 0 };
export default function counter( state = initialState, action ) {
return state;
} ./src/store.js
import { createStore } from "redux";
import counter from "./ducks/counter";
export default createStore(counter);In this step, we'll make our application aware that redux exists and connect the Counter component.
- Open
src/App.js. - Import
Providerfromreact-redux. - Import
storefrom./src/store.js. - Wrap the
Appcomponent in theProvidercomponent.- Add a
storeprop that equals our importedstore.
- Add a
- Open
./src/Counter.js. - Import
connectfromreact-redux. - Connect the
Countercomponent to Redux.- Use a
mapStateToPropsfunction that takes in state.- Return
statefor now.
- Return
- Use a
./src/App.js
import React, { Component } from "react";
import { Provider } from "react-redux";
import store from "./store";
import "./App.css";
import Counter from "./Counter";
class App extends Component {
render() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
}
export default App; ./src/Counter.js
import React, { Component } from "react";
import { connect } from "react-redux";
class Counter extends Component {
render() {
return (
/* lots of jsx */
);
}
}
const mapStateToProps = state => state;
export default connect(mapStateToProps)(Counter);In this step, we'll set up Redux to actually execute actions. We'll start by creating action types, creating action creators, and implementing increment/decrement logic.
- Open
./src/ducks/counter.js. - Create
INCREMENTandDECREMENTaction types. - Write action creators corresponding to
INCREMENTandDECREMENTaction types.- Each of these action creators should accept an
amountargument.
- Each of these action creators should accept an
- Update the reducer to process these actions into state changes.
INCREMENTshould incrementcurrentValueby the givenamount.DECREMENTshould decrementcurrentValueby the givenamount.
./src/ducks/counter.js
const initialState = { currentValue: 0 };
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
export default 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 function increment(amount) {
return { amount, type: INCREMENT };
}
export function decrement(amount) {
return { amount, type: DECREMENT };
}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 creators. - Use
connect'smapDispatchToPropsto place the action creators onCounter's props. - Update the
.counter_buttonbuttons to callincrementordecrementwith the correctamount.
./src/Counter.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { decrement, increment } from "./ducks/counter";
class Counter extends Component {
render() {
const { currentValue, decrement, increment } = this.props;
return (
<div className="app">
<section className="counter">
<h1 className="counter__current-value">{currentValue}</h1>
<div className="counter__button-wrapper">
<button
className="counter__button"
onClick={() => increment(1)}
>
+1
</button>
<button
className="counter__button"
onClick={() => increment(5)}
>
+5
</button>
<button
className="counter__button"
onClick={() => decrement(1)}
>
-1
</button>
<button
className="counter__button"
onClick={() => 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.props, null, 2)}
</pre>
</section>
</div>
);
}
}
const mapStateToProps = state => state;
export default connect(mapStateToProps, { decrement, increment })(Counter);In this step, we'll implement undo/redo logic into our reducer.
- Open
./src/ducks/counter.js. - Create
UNDOandREDOaction types. - Write action creators for
UNDOandREDO. - Refactor
initialStateandcounterto handle undo/redo logic.
./src/ducks/counter.js
const initialState = {
currentValue: 0,
futureValues: [],
previousValues: []
};
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
const UNDO = "UNDO";
const REDO = "REDO";
export default 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 function increment( amount ) {
return { amount, type: INCREMENT };
}
export function decrement( amount ) {
return { amount, type: DECREMENT };
}
export function undo() {
return { type: UNDO };
}
export function redo() {
return { type: REDO };
}In this step, we'll import undo and redo action creators into our Counter.js and hook them up their respective buttons.
- Open
./src/Counter.js. - Import
undoandredoaction creators. - Add
undoandredotomapDispatchToProps. - Destrucuture
undoandredofromprops. - Hook up the
undoandredobuttons to their respective action creators.
./src/Counter.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { decrement, increment, redo, undo } from "./ducks/counter";
class Counter extends Component {
render() {
const {
currentValue,
decrement,
futureValues,
increment,
previousValues,
redo,
undo
} = this.props;
return (
<div className="app">
<section className="counter">
<h1 className="counter__current-value">{currentValue}</h1>
<div className="counter__button-wrapper">
<button
className="counter__button"
onClick={() => increment(1)}
>
+1
</button>
<button
className="counter__button"
onClick={() => increment(5)}
>
+5
</button>
<button
className="counter__button"
onClick={() => decrement(1)}
>
-1
</button>
<button
className="counter__button"
onClick={() => decrement(5)}
>
-5
</button>
<br />
<button
className="counter__button"
disabled={previousValues.length === 0}
onClick={undo}
>
Undo
</button>
<button
className="counter__button"
disabled={futureValues.length === 0}
onClick={redo}
>
Redo
</button>
</div>
</section>
<section className="state">
<pre>
{JSON.stringify(this.props, null, 2)}
</pre>
</section>
</div>
);
}
}
const mapStateToProps = state => state;
export default connect(mapStateToProps, { decrement, increment, redo, undo })(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.





