Code Monkey home page Code Monkey logo

redux-counter's Introduction

Redux Counter - Mini

Setup

To begin, fork and clone this repository. Once it has finished downloading, cd into the project root and run npm i to fetch the project dependencies. After they are fetched, run npm start and a browser window will open at http://localhost:3000 displaying a (non-functioning) counter app. In another terminal window, run npm test to run the test suite.

The plan

Our goal with this mini project is to create a small counter application using React and Redux. Beyond the standard increment and decrement functionality, we also want to implement an undo/redo stack similar to what you see in most text editors.

A finished example can be found here

Step 1

Summary Right now nothing works! That's because Redux will be handling all of our state management and we haven't wired it up yet. To get started, we'll need to install some new dependencies, create a reducer, and create a Redux store.

Instructions

  • Install redux and react-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

Detailed Instructions

Begin by running npm i redux react-redux --save to install the required dependencies.

Open up src/ducks/counter.js and begin by creating a variable named initialState. initialState should be an object with one property, currentValue, set to 0. This variable determines how we want our application state to look when the application first loads.

We interact with Redux via functions commonly called reducers. Reducer functions are where we will be processing all changes to our application's state. Each time a reducer is invoked, Redux passes an object representing the current state and an "action" object that describes what prompted the state change. Because Redux doesn't have a representation of our state the first time it runs, we must provide it with our initialState. With this in mind, let's write our first reducer function, it should:

  • Be named counter
  • Take two parameters
    • state with a default value of initialState
    • action
  • Return state
  • Be exported by default

Right now it doesn't do much, but we'll come back to it once we have built some actions!

Next, we will need to create our Redux store in src/store.js. A Redux store is really just a big object that contains all of our application state data. Start by importing createStore from Redux and our counter reducer from src/ducks/counter.js. Invoke createStore passing in counter and export the result by default.

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

Step 2

Summary

Now that we have a Redux store, we need to make our application aware that it exists and connect relevant components.

Instructions

  • Make the application aware of our Redux store using the React-Redux Provider component.
  • Connect the App component to Redux.

Detailed Instructions

Begin in the root of application at src/index.js by importing the Provider component from React-Redux and store from src/store.js. Wrap the App component in a Provider component. Pass store as a prop to Provider. This will make our application aware of the Redux store and allow us to gain access to data.

Open up src/App.js so we can connect App to our application's state. To do this, we first need to import the aptly named connect function from react-redux. Then, underneath your App component definition, create a function mapStateToProps that takes a parameter state. This function will tell connect which pieces of application state we're interested in. Right now we want all of it, so just return state.

Using connect, we are going to "decorate" our component, which is a fancy way of saying that we are going to let it do things it wasn't able to before, such as access data in Redux. To do this we need to first create our decorator by invoking connect and passing in mapStateToProps. Once our decorator is created, we need to invoke it and pass in App, exporting the result by default. This is a little confusing at first, so check out the example below!

Decorator Example
function mapStateToProps( state ) {
	return state;
}
const decorator = connect( mapStateToProps );
const decoratedComponent = decorator( App );
export default decoratedComponent;

This is usually shortened to

function mapStateToProps( state ) {
	return state;
}
export default connect( mapStateToProps )( App );

Now that we're all wired up, we just need to make sure everything is working! Do this by console.loging this.props at the top of App's render method. You should see an output of { currentValue: 0 }. Whew, that was a lot of setup! Luckily we only have to do most of this once per application.

Code Solution
src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";

import "./index.css";

import store from "./store";
import App from "./App";

ReactDOM.render(
	<Provider store={ store }>
		<App />
	</Provider>
	, document.getElementById( 'root' )
);
src/App.js
import React, { Component } from "react";
import { connect } from "react-redux";

import "./App.css";

class App extends Component {
	render() {
		console.log( this.props );
		return (
			/* lots of jsx */
		);
	}
}

function mapStateToProps( state ) {
	return state;
}

export default connect( mapStateToProps )( App );

Step 3

Summary

Now that our application is talking to Redux, we need to set up Redux to actually do the things we want it to do. We'll start by creating action types, creating action creators, and implementing increment/decrement logic.

Instructions

  • Create INCREMENT and DECREMENT action types.
  • Write action creators corresponding to INCREMENT and DECREMENT.
    • Each of these action creators should accept an amount parameter.
  • Update the reducer to process these actions into state changes.

Detailed Instructions

We currently have state, but no way to do anything with it. To be able to access that data, we first need to create some action types. Action types describe to our reducer (counter) what has occurred when Redux receives an action. We'll start with two action types, each stored in its own variable. Create a variable named INCREMENT and set it equal to the string "INCREMENT", and a variable named DECREMENT set equal to the string "DECREMENT". We use all capital names here to indicate that these values are constants that will never be altered by the application.

Following action types comes the action creators. In Redux, actions are plain objects containing a type (describing what happened) and any data that might be necessary to the action. Our first action creator will be a function named increment that takes in a parameter of amount. increment will return an object with two properties: amount - set equal to the amount parameter, and type set equal to the INCREMENT action type. Create a decrement function that mimics increment, the only difference being that type should now be equal to DECREMENT. Export both of these functions.

The last change we'll be making in our counter.js file will be updating the reducer to handle these actions. It is a core concept of Redux and state management that state is never mutated, meaning you should never say state.currentValue++. This means that each time counter is called we need to return a new state object from the action and values from the current state without changing the current state.

With that in mind, let's get started. First we need to determine what the reducer should do by looking at the action's type, a switch statement is perfect for this. If the action type is INCREMENT, we will return a new a state object where the currentValue property is equal to state's currentValue property plus action.amount. If the action type is DECREMENT we will return a new state object where currentValue is equal to state's currentValue property minus action.amount. Lastly, move the return state to the switch statement's default case.

Code Solution
src/ducks/counter.js
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";

const initialState = { currentValue: 0 };

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

Step 4

Summary

Now that we can effectively manage application state, we need to wire up the App component so that it can dispatch actions to our reducer.

Instructions

  • Import the increment and decrement action creators to src/App.js.
  • Use connect's mapDispatchToProps to place the action creators on App's props.
  • Attach the action creators to the appropriate buttons.

Detailed Instructions

Inside of src/App.js, import the increment and decrement action creators. To use these within our component, we need to do two things:

  1. Place them onto the component's props
  2. Wrap them in React-Redux's dispatch function - dispatch is the function used within Redux to indicate that something has happened and state needs to change.

Luckily connect allows us to do just that! Pass a second argument to connect, this argument should be an object comprised of our action creators. Connect will automatically wrap each method in dispatch and then place it on our component's props.

Now that we have access to all of the necessary and data and functions inside of App, we can connect to the UI and provide interactivity. Begin by destructuring currentValue, decrement, and increment from this.props at the top of App's render method. Replace the static 0 inside of the h1 with currentValue. In the button with the text "+1", change the callback function to invoke increment with an argument of 1. Repeat this step for the "+5" button. Follow the same steps for the "-1" and "-5" buttons with the decrement function.

You should now be able to interact with all of the increment and decrement buttons and see their result update in the view.

Code Solution
src/App.js
import React, { Component } from "react";
import { connect } from "react-redux";

import "./App.css";

import { decrement, increment } from "./ducks/counter";

class App 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>
		);
	}
}

function mapStateToProps( state ) {
	return state;
}

export default connect( mapStateToProps, { decrement, increment } )( App );

Step 5 - Black Diamond

Be sure to set the BLACK_DIAMOND variable in src/ducks/counter.js to true to allow for testing on this step

Summary

In this step, we will implement undo/redo logic inside of the counter reducer.

Instructions

  • Create UNDO and REDO action types.
  • Write action creators for UNDO and REDO.
  • Refactor initialState and counter to handle undo/redo logic.

Detailed Instructions

Open up src/ducks/counter.js. Create two new actions - UNDO and REDO. Create the corresponding action creators undo and redo alongside your other action creators. Because we will already have all the data we need on state, the actions returned by these action creators only need to have a type property. Change the initialState variable by adding two properties - futureValues and previousValues, both set to empty arrays.

Because we changed how initialState looks, we need to update how we handle existing actions before adding handlers for the new ones. Adjust both the INCREMENT and DECREMENT cases so that they return an object that looks something like this:

{
	  currentValue: state.currentValue /* + or - */ action.amount
	, futureValues: []
	, previousValues: [ state.currentValue, ...state.previousValues ]
}

A few things to note:

  • We are handling the change to currentValue exactly the same as before.
  • We reset futureValues to an empty array, because if we have incremented or decremented since the last undo the redo stack is no longer valid.
  • We save the previous state's currentValue into the previousValues array without mutating the previous state's values.

Now that fixing our old code is out of the way, let's write the logic for UNDO and REDO actions. Start by writing a case for UNDO. As always in a reducer, this case will return an object representing our updated state. This object should:

  • Set currentValue equal to the previous state's previousValues[ 0 ]
  • Set futureValue equal to an array containing the previous state's currentValue as well as any other futureValues that we're already on state.
  • Set previousValues to a copy of the previous state's previousValues without the first index (because that value is now living at currentValue).
It should look something like this
{
	  currentValue: state.previousValues[ 0 ]
	, futureValues: [ state.currentValue, ...state.futureValues ]
	, previousValues: state.previousValues.slice( 1, state.previousValues.length )
}

Once that is complete, we can add the logic for REDO. REDO will be handled exactly the same as UNDO was, only the names have changed. Anything that was done previousValues above should now be done to futureValues and vice versa.

All that is left now is to connect these functions to the App component and tie them to the appropriate buttons. Inside of src/App.js, import the undo and redo functions from src/ducks/counter.js. Edit the object passed as a second argument to connect to include these functions.

Destructure undo, redo, futureValues, and previousValues from this.props. Pass undo as the click handler to the Undo button, and change the disabled prop to be equal to previousValues.length === 0 (so we don't accidentally undo when there are no previous values!). Pass redo as the click handler to the Redo button and change the disabled prop to be equal to futureValues.length === 0.

All done! You should now be able to increment and decrement, undo and redo, and see the values of state changing on the right side of the screen!

Code Solution
src/ducks/counter.js
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
const UNDO = "UNDO";
const REDO = "REDO";

const initialState = {
	  currentValue: 0
	, futureValues: []
	, previousValues: []
};

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, state.previousValues.length )
			};
		case REDO:
			return {
				  currentValue: state.futureValues[ 0 ]
				, futureValues: state.futureValues.slice( 1, state.futureValues.length )
				, 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 };
}
src/App.js
import React, { Component } from "react";
import { connect } from "react-redux";

import { decrement, increment, redo, undo } from "./ducks/counter";

import "./App.css";

class App 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>
		);
	}
}

function mapStateToProps( state ) {
	return state;
}

export default connect( mapStateToProps, { decrement, increment, redo, undo } )( App );

Contributions

Contributions

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.

Copyright

Copyright

© 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.

redux-counter's People

Contributors

r-walsh avatar devlemire avatar brianjhudson avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.