A bare bones react starter set of files
npm install
npm run build
run build
has been modified to watch for changes.
npm start
We're going to walk through creating a React application with Redux.
React has a mechanism for managing state, but it's most effective for controlling state at the component level. Though it can be used to manage state for the whole application, inter-component state can be very difficult to manage.
Lets say you have 2 components, A and B, that need to share and update some information. So you create a Parent component C that can pass the data and update functions as props to the child components.
|- C
|- A
|- B
Now we have some data that component D needs to share with C. So we'll need another parent component.
|- E
|- C
|- A
|- B
|- D
This solution can get out of hand very quickly as it's also very likely that many components don't need data and only exist to pass data to other components, but all that code needs to be written anyway. This is when we start to look for solutions for managing the state of our application as a whole.
Redux is a library for managing the state of your entire application. The core concept of Redux is that all state (or data) for your application is kept in one place called the Store. This way any component can get the data that it specifically needs. And any components subscribed to the store are informed when changes to the store have been made.
It is actually very simple.
Imagine your application's state is described as a plain object. For example, the state of a todo app might look like this:
{
todos: [
{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}
],
visibilityFilter: 'SHOW_COMPLETED'
}
This object is like a “model” except that there are no setters. Meaning there is no way to directly update a property's value, only read it. This is so that different parts of the code can’t change the state arbitrarily, causing hard-to-reproduce bugs.
We'll call this state-containing object the Store.
To change something in the Store, you need to dispatch an action. An action is a plain JavaScript object (notice how we still haven't introduced any magic?) that describes what happened. Here are a few example actions:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
Enforcing that every change is described as an action allows us have a clear understanding of what’s going on in the application. If something has changed, we know why it changed. Actions are like breadcrumbs of what has happened.
Finally, to tie the store and actions together, we write a function called a reducer. Again, nothing magical about it—it’s just a function that takes the previous state and an action as arguments, and returns the next state of the app. It would be hard to write such a function for a big app, so we write smaller functions managing parts of the state:
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map(
(todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}
And we write another reducer that manages the complete state of our app by calling those two reducers for the corresponding state keys:
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}
This is basically the whole idea of Redux, that you describe how your state is updated over time in response to action objects, and 90% of the code you write is just plain JavaScript, with no use of Redux itself, its APIs, or any magic.
Redux architecture revolves around a strict unidirectional (one-way) data flow.
This means that all data in an application follows the same lifecycle pattern, making the logic of your app more predictable and easier to understand.
Some event comes in (e.g. click, user typing, form submission)
Which dispatches an Action
The Reducer calculates a new state (based on previous state and the action)
Which updates the Store
Which updates the view (e.g. React Components)
Now the user can trigger a new event
By making sure that every change in the whole application follows this pattern, debugging any issue becomes very simple because you can just work backwards through the lifecycle.
It also encourages data normalization, meaning not ending up with multiple, independent copies of the same data that are unaware of one another.
It's much easier to show rather than tell.
And you learn a lot more, by typing it all out, rather than copying. Just saying.
For this project we'll be building a very very simple budget tracker where we can create line items and have them be calculated into a summary as we add them.
Start by cloning the repository from Github
Don't forget to run npm install
For this project we'll also be implementing a helpful tool creatively named "Redux DevTools" as a Chrome extension. It can be installed here. It will attach itself to the normal Chrome DevTools, and will only show on pages that implement it's use.
react200-budget-tracker
|- src/
|- js/
|- index.jsx
|- app.jsx
While we're working on this let's open 2 terminals to the react200-budget-tracker
directory. In one terminal run npm run build
this script has been modified to automatically watch for changes so keep it running while developing. In the other terminal run npm start
this will start the node server.
Now let's check the page at http://localhost:3000
It should like the example image above.
In this case the HTML and CSS work has already been done and moved into React, our job is to implement the logic using Redux. First we have to break up this application into components.
Let's start by adding new files and folders. Just make empty files for now, we'll fill them in as we go.
react200-budget-tracker
|- src/
|- js/
|- components/
|- ExpenseEntries/
|- ExpenseEntries.jsx
|- index.js
|- IncomeEntries/
|- IncomeEntries.jsx
|- index.js
|- Summary/
|- Summary.jsx
|- index.js
|- rootReducer.js
|- rootStore.js
|- index.jsx
|- app.jsx
Let's create the ExpenseEntries components first.
import React from 'react';
export default class ExpenseEntries extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className='card border-danger mb-3'>
<div className='card-header text-white bg-danger'>Expense Entries</div>
<div className='card-body'>
<form>
<div className='form-group'>
<label htmlFor='expense-description'>Description</label>
<input
type='text'
className='form-control'
id='expense-description'
/>
</div>
<div className='form-group'>
<label htmlFor='expense-amount'>Amount</label>
<div className='input-group'>
<span className='input-group-addon'>$</span>
<input
type='text'
className='form-control'
id='expense-amount'
/>
</div>
</div>
<button
type='button'
className='btn btn-danger col-12 mb-5'
>+ Add Expense
</button>
<table className='table table-sm table-hover'>
<thead>
<tr>
<th>Description</th>
<th style={ { width: 120 } } >Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rent</td>
<td>$1,500.00</td>
</tr>
</tbody>
</table>
</form>
</div>
</div>
);
}
}
For the IncomeEntries component we can copy the same code, and only change anywhere it says expenses
to income
.
And change the class names from danger
to success
.
Now let's create the Summary component
import React from 'react';
export default class Summary extends React.Component {
render() {
return (
<div className='card border-info mb-3'>
<div className='card-header text-white bg-info'>Summary</div>
<div className='card-body'>
<div className='container'>
<div className='row'>
<div className='col-6 text-center'>
<h6 className='h6 strong'>Total Income</h6>
<p>$4,000.00</p>
</div>
<div className='col-6 text-center'>
<h6 className='h6 strong'>Total Expense</h6>
<p>$1,500.00</p>
</div>
</div>
<div className='row justify-content-center'>
<div className='col-6 text-center'>
<h6 className='h6 strong'>Left after spending</h6>
<p>$1,500.00</p>
</div>
</div>
</div>
</div>
</div>
);
}
}
To make it easier them easier to import in the index.js
of each component simply import
and export default
the same component. This file will have more later, but for now it allows us to shorten our import path from './components/IncomeEntries/IncomeEntries'
to './components/IncomeEntries'
.
import ExpenseEntries from './ExpenseEntries';
export default ExpenseEntries;
Now that we have our components separated we can update the app.jsx
file to use them.
import React from 'react';
import IncomeEntries from './components/IncomeEntries';
import ExpenseEntries from './components/ExpenseEntries';
import Summary from './components/Summary';
export default class App extends React.Component {
render() {
return (
<div className='container'>
<div className='jumbotron' >
<h1 className='display-3 text-center'>Budget Tracker</h1>
</div>
<div className='row'>
<div className='col-12 col-md-6 mb-4'>
<IncomeEntries />
</div>
<div className='col-12 col-md-6 mb-4'>
<ExpenseEntries />
</div>
</div>
<div className='row justify-content-center'>
<div className='col-12 col-md-6'>
<Summary />
</div>
</div>
</div>
);
}
}
The application isn't going to look any different but we have taken the next step to building something scaleable.
If everything is working now would be a great time to make a git commit. Remember to make them often and as much as possible only when things are working.
Now here normally we would have to npm install redux rect-redux
, but I've included it already.
We need to create the store and attach any middleware. In this case the only middleware we'll use is the Redux DevTools. By attaching it as middleware the Redux DevTools will be able to see all the changes that pass through the Store.
Also note for now, since we haven't created any reducers we need an empty temporary one.
import { createStore } from 'redux';
function tempReducer () {
return null;
}
const rootStore = createStore(
tempReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default rootStore;
In order for React to make use of the store we'll need to use a special connect()
function later. And for that connect()
function to work we need to wrap our <App/>
in the <Provider>
component. So let's do that now.
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import App from './app';
import store from './rootStore';
render(
<Provider store={ store }>
<App />
</Provider>,
document.getElementById('root')
);
The Provider will provide the store to the connect()
function using some of the more complex layers of React called the context.
Again, at this point the application isn't going to look any different.
And again since everything is working, now would be a great time to make a git commit. Remember to make them often and as much as possible only when things are working.
Now that our store is working and connected to React, we need to create and connect our reducers. These will define how the Actions affect or change the store.
Since input to our application is only coming from the two Entries
components we only need to create reducers for those components.
This file should either live in the same directory as the component or it could be in it's own directory if it doesn't have a matching component.
In the file we also need to create a default state for the part of the Store this reducer is responsible for. Since this component has two input fields we'll need two properties with empty strings, and of course we'll start with an empty list (array) of line Items.
const defaultState = {
description: '',
amount: '',
lineItems: []
};
export default function ExpenseReducer (state = defaultState, action) {
// the `state = defaultState` above is new ES6 syntax
// for defining a default value on a parameter
return state;
}
Again for IncomeEntries/incomeReducer.js
we can use the exact same code just changing the name from expense to income. And making sure it's in the correct directory.
Now that we have some reducers and we need to combine them, because the store ultimately only accepts one reducer.
Let's update the rootReducer file
import { combineReducers } from 'redux';
import expenseReducer from './components/ExpenseEntries/expenseReducer';
import incomeReducer from './components/IncomeEntries/incomeReducer';
const rootReducer = combineReducers({
expense: expenseReducer,
income: incomeReducer
});
export default rootReducer;
And we need to update the store to use our new reducer.
import { createStore } from 'redux';
import rootReducer from './rootReducer';
const rootStore = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default rootStore;
With this our Store object officially has a shape that we can observe using the Redux DevTools. You can see it under the State tab.
Now we can see some changes in our DevTools, but still nothing in our UI.
Last reminder, If your app is in a working state (and it should be). Make a git commit.
Next we need a way to create the actions that the reducer needs to change the store.
Remember that Actions are just objects that describe a change, we'll have our action adhere to the Flux Standard Action pattern.
An FSA action should have at minimum a type and a payload. The type and payload are used by the reducer to identify and modify the state.
{
type: 'ADD_TODO',
payload: {
text: 'Go to swimming pool'
}
}
The goal of actions is to predefine all state changes, but the text
of that example could be any user input. So we need a way to dynamically create actions, hence Actions Creators.
Action Creators are just functions that return Actions, simple.
Create a file next to the expenseEntries component called expenseActions.js
.
In the actions file we're going to create our action types and the action creators. We'll export the action types in order to reuse them in the reducer.
// Action Creators
export function updateExpenseDescription(description) {
return {
type: 'UPDATE_EXPENSE_DESCRIPTION',
payload: { description }
};
}
export function updateExpenseAmount(amount) {
return {
type: 'UPDATE_EXPENSE_AMOUNT',
payload: { amount }
};
}
export function addExpense(description, amount) {
return {
type: 'ADD_EXPENSE',
payload: {
description,
amount: parseFloat(amount)
}
};
}
Now that we have an idea of what our actions are going to be we can update the reducers to use those actions.
We'll import the types from the actions file. And to the reducer functions we will add a switch-case statement that will return a different state based on the action that come in.
const defaultState = {
description: '',
amount: '',
lineItems: []
};
export default function ExpenseReducer(state = defaultState, action) {
const { type, payload } = action;
switch (type) {
// Here in the case of the update description action
case 'UPDATE_EXPENSE_DESCRIPTION': {
// we'll return an object
return {
// with all the previous state
...state,
// but overwriting the description
description: payload.description
};
}
case 'UPDATE_EXPENSE_AMOUNT': {
return {
...state,
amount: payload.amount
};
}
case 'ADD_EXPENSE': {
const { description, amount } = action.payload;
return {
description: '',
action: '',
lineItems: [
// here we have all the previous line items
...state.lineItems,
// plus a new object
{ description, amount }
]
};
}
default: {
return state;
}
}
}
Again, you can copy both files for the IncomeEntries component, just changing the names.
Now we can finally start connecting the individual components.
We'll use the connect()
function from the react-redux
library. This will allow us to translate data from the store to the props of the component.
import { connect } from 'react-redux';
import ExpenseEntries from './ExpenseEntries';
// This function takes the store and returns an object
// that's passed to the props of the component.
function mapStoreToProps(store) {
return {
description: store.expense.description,
amount: store.expense.amount,
lineItems: store.expense.lineItems
};
}
// This might look odd but, connect returns a function,
// that is then called with the component itself.
export default connect(mapStoreToProps)(ExpenseEntries);
Repeat for the IncomeEntries Component.
Finally we can update the components themselves to start using all these pieces.
In addition to pushing the store to props, connect()
also pushed the store's dispatch method as a prop. Dispatch is the function that we pass actions to, in order to change the store.
import React from 'react';
// We'll need to import all those action creators.
import {
updateExpenseDescription,
updateExpenseAmount,
addExpense
} from './expenseActions';
export default class ExpenseEntries extends React.Component {
constructor(props) {
super(props);
// Here we're binding these methods to the context
// of the components. This only has to be done,
// because these methods are called back by
// event emitters (which lose context).
this.handleDescriptionInput = this.handleDescriptionInput.bind(this);
this.handleAmountInput = this.handleAmountInput.bind(this);
this.handleAddExpense = this.handleAddExpense.bind(this);
}
handleDescriptionInput(event) {
// dispatch was provided by connect()
const { dispatch } = this.props;
const { value } = event.target;
dispatch(updateExpenseDescription(value));
}
handleAmountInput(event) {
const { dispatch } = this.props;
const { value } = event.target;
dispatch(updateExpenseAmount(value));
}
handleAddExpense() {
const { description, amount, dispatch } = this.props;
dispatch(addExpense(description, amount));
}
render() {
// These values were provided by connect()
const { description, amount, lineItems } = this.props;
return (
<div className='card border-danger mb-3'>
<div className='card-header text-white bg-danger'>Expense Entries</div>
<div className='card-body'>
<form>
<div className='form-group'>
<label htmlFor='expense-description'>Description</label>
<input
type='text'
className='form-control'
id='expense-description'
value={ description }
onChange={ this.handleDescriptionInput }
/>
</div>
<div className='form-group'>
<label htmlFor='expense-amount'>Amount</label>
<div className='input-group'>
<span className='input-group-addon'>$</span>
<input
type='text'
className='form-control'
id='expense-amount'
value={ amount }
onChange={ this.handleAmountInput }
/>
</div>
</div>
<button
type='button'
className='btn btn-danger col-12 mb-5'
onClick={ this.handleAddExpense }
>+ Add Expense
</button>
<table className='table table-sm table-hover'>
<thead>
<tr>
<th>Description</th>
<th style={ { width: 120 } } >Amount</th>
</tr>
</thead>
<tbody>
{
lineItems.map(lineItem => (
<tr>
<td>{ lineItem.description }</td>
<td>${ lineItem.amount.toFixed(2) }</td>
</tr>
))
}
</tbody>
</table>
</form>
</div>
</div>
);
}
}
There are quite a few changes in this file, take your time to go through each line to understand how the data is going to flow using all that you've learned so far.
To recap the LifeCycle of the React-Redux implementation:
Element's onChange fires the handleChange method
HandleChange dispatches an action-created action
A Reducer catches the action and updates the store
connect() function updates a component by it's props
The component rerenders and now reflects the new state
You'll note that we never created a place in the store for the Summary component. This is not usually the case with patterns like MVC. That's because all the data needed to calculate the summary is in the line items in income
and expense
. In other words we can derive the information from existing data.
Let's start by updating the Summary component's index.js.
import { connect } from 'react-redux';
import Summary from './Summary';
function mapStoreToProps(store) {
return {
expenseItems: store.expense.lineItems,
incomeItems: store.income.lineItems
};
}
export default connect(mapStoreToProps)(Summary);
Now let's use the lineItems as props to calculate the final tallies.
import React from 'react';
function calculateSum(lineItems) {
return lineItems.reduce((acc, lineItem) => acc + lineItem.amount, 0);
}
function formatCurrency(amount) {
if (amount >= 0) {
const dollars = Math.floor(amount);
const cents = Math.floor((amount - dollars) * 100).toString().padEnd(2, '0');
return `$${dollars.toLocaleString()}.${cents}`;
}
const dollars = Math.ceil(amount);
const cents = Math.floor((amount - dollars) * 100 * -1).toString().padEnd(2, '0');
return `-$${(dollars * -1).toLocaleString()}.${cents}`;
}
class Summary extends React.Component {
render() {
const { incomeItems, expenseItems } = this.props;
const incomeTotal = calculateSum(incomeItems) / 100;
const expenseTotal = calculateSum(expenseItems) / 100;
const difference = Math.round(incomeTotal - expenseTotal) / 100;
return (
<div className='card border-info mb-3'>
<div className='card-header text-white bg-info'>Summary</div>
<div className='card-body'>
<div className='container'>
<div className='row'>
<div className='col-6 text-center'>
<h6 className='h6 strong'>Total Income</h6>
<p>{ formatCurrency(incomeTotal) }</p>
</div>
<div className='col-6 text-center'>
<h6 className='h6 strong'>Total Expenses</h6>
<p>{ formatCurrency(expenseTotal) }</p>
</div>
</div>
<div className='row justify-content-center'>
<div className='col-6 text-center'>
<h6 className='h6 strong'>Left after spending</h6>
<p>{ formatCurrency(difference) }</p>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default Summary;
- Deploy the app to Heroku
- Submit the live URL in the LMS
- Add
prop-types
to all components - Update the Entries component to delete or edit a line item.
- Refactor the income and expense components into one component that can handle both.
To submit this project for instructor evaluation, please do the following:
- Push this project to GitHub
- Create a Heroku application
- Create a CircleCI project
- Configure automatic Heroku deployment
- Trigger a successful deployment
Once you have done the above, Submit your project