In this project, we'll create a small counter application using React and Redux. We'll also include extra functionality for undo/redo actions.
fork
andclone
this repository.cd
into the project root.- Run
npm install
to fetch the project dependencies. - Run
npm start
to spin up a development server.
In this step, we'll install some new dependencies, create a reducer, and create a Redux store.
- Install
redux
andreact-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
Provider
fromreact-redux
. - Import
store
from./src/store.js
. - Wrap the
App
component in theProvider
component.- Add a
store
prop that equals our importedstore
.
- Add a
- Open
./src/Counter.js
. - Import
connect
fromreact-redux
. - Connect the
Counter
component to Redux.- Use a
mapStateToProps
function that takes in state.- Return
state
for 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
INCREMENT
andDECREMENT
action types. - Write action creators corresponding to
INCREMENT
andDECREMENT
action types.- Each of these action creators should accept an
amount
argument.
- Each of these action creators should accept an
- Update the reducer to process these actions into state changes.
INCREMENT
should incrementcurrentValue
by the givenamount
.DECREMENT
should decrementcurrentValue
by 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
increment
anddecrement
action creators. - Use
connect
'smapDispatchToProps
to place the action creators onCounter
's props. - Update the
.counter_button
buttons to callincrement
ordecrement
with 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
UNDO
andREDO
action types. - Write action creators for
UNDO
andREDO
. - Refactor
initialState
andcounter
to 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
undo
andredo
action creators. - Add
undo
andredo
tomapDispatchToProps
. - Destrucuture
undo
andredo
fromprops
. - Hook up the
undo
andredo
buttons 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.