Redux
&
Reduxsauce
Redux!
What is it?
"A predictable state container for JavaScript apps"
Redux is a library to keep data organized and consistent across an app
In other words,
But how does it work?
Illustrative Story Time!
Depositing $$$
1. "I would like to deposit my wads of cash, please"
2. "Great, I can definitely help you!"
3. "Balance: 1 Billion Dollars"
=> We make a request to the teller
=> The teller accepts our cash, counts it, loads it, etc
=> Our bank account reflects any new changes
$$$ in the bank
=
Data in the app
3 Main Components of Redux
Action - The request made to the teller
Reducer - The teller who interprets our request & follows the proper procedure
Store - Bank vault where all money is stored
Things to note:
- A law-abiding customer cannot go straight to the vault and take money => Data in the Store cannot be directly changed
- A customer must make a request that the teller understands => Only specific actions can be properly interpreted
- In this story, there is only one single bank in the entire world => There is only one store in an app
Redux manages app state, but...
what is State???
- A Plain-Old-Javascript-Object (POJO) that holds our app's data
{
user: {
email: "pj@smartlogic.io",
username: "thepeej",
groupIds: [1, 5, 48]
},
firstTimeLoaded: false,
}
{ balance: 1000000000 }
Our story:
Slightly more realistic:
Action
The customer's request
- A POJO that expresses user's intent
- Has a required attribute of "type"
{ type: "WITHDRAW_TWENTY" }
{
type: "DEPOSIT_MONEY",
payload: 1000000000
}
Reducer
The bank teller
- A pure JS function
- Accepts two parameters - State & Action
- Sets initial state
- Always returns a new State object
function bankTransactions(state = { balance: 100 }, action) {
switch (action.type) {
case 'DEPOSIT_MONEY' :
return { ... state, balance: state.balance + action.payload}
case 'WITHDRAW_TWENTY' :
return {... state, balance: state.balance - 20}
default :
return state
}
}
Problem!
A reducer only returns a new state!
let state
function bankTransactions(state = {balance: 100}, action) {
switch (action.type) {
case 'DEPOSIT_MONEY' :
return {...state, balance: state.balance + action.payload}
case 'WITHDRAW_TWENTY' :
return {... state, balance: state.balance - 20}
default :
return state
}
}
bankTransactions(state, {type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
bankTransactions(state, {type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
bankTransactions(state, {type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
Store
The Bank Vault
Ties everything together
- Declares and encapsulates application state
- getState(): Provides access to application state
- dispatch(): "Saves" state changes from action/reducer output
- There should be only one Store in an application
Dispatch
- The only way to save state changes
let state
bankTransactions(state, {type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
bankTransactions(state, {type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
bankTransactions(state, {type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
Problem:
function dispatch(action) {
state = bankTransactions(state, action)
}
Solution:
dispatch({type: "WITHDRAW_TWENTY"})
// => { balance: 80 }
dispatch({type: "WITHDRAW_TWENTY"})
// => { balance: 60 }
dispatch({type: "WITHDRAW_TWENTY"})
// => { balance: 40 }
createStore
function createStore(reducer) {
}
let state
function getState() {
return state
}
return {
dispatch,
getState
}
function dispatch(action) {
state = reducer(state, action)
}
dispatch({})
- Requires a reducer to be passed in
( subscribe() & listeners are omitted in example )
createStore
let store = createStore(bankTransactions)
store.getState()
// => { balance: 100 }
store.dispatch({type: 'DEPOSIT_CASH', payload: 40})
store.getState()
// => { balance: 120 }
store.dispatch({type: 'WITHDRAW_TWENTY'})
store.getState()
// => { balance: 80 }
How do we use it?
Reduxsauce
Now onto
(This is a terrible logo)
Why use it?
- Keeps Redux code well organized
- Makes code easier to read and maintain
- Allows for cleaner code expandability
- Provides easier testing of Reducers
By what means?
- createReducer()
- createActions()
createReducer
import { createReducer } from 'reduxsauce'
// the initial state of this reducer
const INITIAL_STATE = { balance: 100 }
const depositCash = (state, action) => {
return {... state, balance: state.balance + action.payload}
}
const withdrawTwenty = (state, action) => {
return {... state, balance: state.balance - 20}
}
// map our action types to our reducer functions
const HANDLERS = {
['DEPOSIT_CASH']: depositCash,
['WITHDRAW_TWENTY']: withdrawTwenty
}
export default createReducer(INITIAL_STATE, HANDLERS)
import { createReducer } from 'reduxsauce'
const INITIAL_STATE = { balance: 100 }
const depositCash = (state, action) => {
return {... state, balance: state.balance + action.payload}
}
export const withdrawTwenty = (state, action) => {
return {... state, balance: state.balance - 20}
}
const HANDLERS = {
['DEPOSIT_CASH']: depositCash,
['WITHDRAW_TWENTY']: withdrawTwenty
}
export default createReducer(INITIAL_STATE, HANDLERS)
Initial state set outside of function definition
State manipulation organized into separate functions
Concise routing of action types to business logic
function bankTransactions(state = { balance: 100 }, action) {
switch (action.type) {
case 'DEPOSIT_MONEY' :
return { ... state, balance: state.balance + action.payload}
case 'WITHDRAW_TWENTY' :
return {... state, balance: state.balance - 20}
default :
return state
}
}
Original Reducer
Eliminates use of switch/case statement which can become messy
createActions
import { createActions } from 'reduxsauce'
const { Types, Creators } = createActions({
withdrawTwenty: null,
depositCash: ['payload']
})
Keys of object passed in will become keys/values of the Types after being converted to SCREAMING_SNAKE_CASE
Types
// => { DEPOSIT_CASH: "DEPOSIT_CASH", WITHDRAW_TWENTY: "WITHDRAW_TWENTY" }
function depositMoney(amount) {
return { type: 'DEPOSIT_MONEY', payload: amount }
}
depositMoney(50)
// => { type: "DEPOSIT_MONEY", payload: 50 }
Now, let's talk about
action creators
A function that returns ('creates') an action
createActions
import { createActions } from 'reduxsauce'
const { Types, Creators } = createActions({
withdrawTwenty: null,
depositMoney: ['payload']
})
Creators.withdrawTwenty()
// => {type: "WITHDRAW_TWENTY"}
Creators.depositMoney()
// => {type: "DEPOSIT_MONEY"}
The keys of the object are also used as the keys (as-is) of Creators. The value is a function which returns an action (aka an "action creator")
(Remember, an action is a POJO with at least a type attribute.)
Creators.depositMoney(30)
// => {type: "DEPOSIT_MONEY", payload: 30}
Creators.depositMoney(30, 50)
// => {type: "DEPOSIT_MONEY", payload: 30}
Creators.withdrawTwenty(30)
// => {type: "WITHDRAW_TWENTY"}
strings become additional action attribute
Putting it all together
import { createReducer, createActions } from 'reduxsauce'
const { Types, Creators } = createActions({
withdrawTwenty: null,
depositCash: ['payload']
})
export default Creators
const INITIAL_STATE = { balance: 100 }
const depositCash = (state = INITIAL_STATE, action) => {
return {... state, balance: state.balance + action.payload}
}
const withdrawTwenty = (state = INITIAL_STATE, action) => {
return {... state, balance: state.balance - 20}
}
const HANDLERS = {
[Types.DEPOSIT_CASH]: depositCash,
[Types.WITHDRAW_TWENTY]: withdrawTwenty
}
export const reducer = createReducer(INITIAL_STATE, HANDLERS)
-
Creators are exported for use within mapDispatchToProps()
-
Reducer is exported for use within createStore()