Skip to content

Instantly share code, notes, and snippets.

@0bie
Last active January 23, 2018 23:53
Show Gist options
  • Save 0bie/3d58ae5b4505246c9cae3f5a483fa8c5 to your computer and use it in GitHub Desktop.
Save 0bie/3d58ae5b4505246c9cae3f5a483fa8c5 to your computer and use it in GitHub Desktop.
Redux Beginner Notes

Intro:

Resources:

Notes:

  • Redux defines itself as a predictable state container for JS applications.

  • It attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.

  • It makes you think of your app as an initial state being modified by a sequential list of actions.

  • In summary Redux provides the following:

    • A place to put your application state.
    • A mechanism to dispatch actions to modifiers of your application state, A.K.A reducers.
    • A mechanism to subscribe to state updates.
  • The Redux instance is called a store and can be created like this:

import { createStore } from 'redux';
const store = createStore(() => {});
  • createStore expects a function that will allow it to reduce your state (reducer).

  • That instance will hold the state of the application and the state will be mutated by reducer function(s).

  • By passing reducer function(s) into createStore() Redux will call that function each time an action occurs.

Actions

  • Actions represent everything a user can do in an app.
  • They are plain JS objects with a type property; type is one of ActionTypes (string constants defined by your app) E.g
{
  type: 'ADD_TODO',
  text: 'Use Redux'
}

{
  type: 'REMOVE_TODO',
  stampId: 42
}

{
  type: 'LOAD_ARTICLE',
  response: {...}
}

Action creators

  • Action creators are functions used to create and return actions E.g:
// Boilerplate
const actionCreator = () => {
  return {
    type: 'AN_ACTION'
  }
}

function actionCreator() {
    return {
        type: 'AN_ACTION'
    }
}

// actionCreators.js
export function actionCreator() {
  return {
    type: 'AN_ACTION'
  }
}

export function addTodo(text) {
  return {
    type: 'ADD_TOO',
    text
  }
}

// AddTodo.js
import { addTodo } from './actionCreators';

// somewhere in an event handler
dispatch(addTodo('Use Redux'))
  • In order for an action to be useful it needs to be dispatched (using a dispatch function), the store will then be aware of its occurrence and act accordingly.

  • actionCreator -> Action (the steps so far)

Reducers

const total = [0, 1, 2, 3].reduce((accumulator, currentValue) => {
  return accumulator + currentValue;
});
console.log(total) // 6
  • In Redux, the accumulated value is the state object and the values being accumulated are actions. Reducers calculate a new state given the previous state and an action.

  • Reducers must be pure functions (functions that return the exact same output for given inputs).

  • A Reducer is a subscriber to actions.

  • When creating a Redux instance you must give it a reducer function so that Redux can call this function(s) on the application state each time an action occurs.

  • By passing reducers to createStore Redux registers action handlers.

import { createStore } from 'redux';

// Boilerplate structure for reducers
const store = createStore(() => {});

const reducer_0 = (...args) => {
  console.log('Reducer was called with args', args);
};

const store_0 = createStore(reducer); // Reducer was called with args [ undefined, { type: '@@redux/INIT' } ]
  • [ undefined, { type: '@@redux/INIT' } ]

    • To initialize the state of the application Redux dispatches an INIT action ({ type: '@@redux/INIT' })
    • When this initialization takes place the state is yet to be defined and so we get undefined.
    • A reducer receives both the current state and an action as parameters: (state, action)
    • Since none of these parameters were declared in the example above it returns the defaults: [ undefined, { type: '@@redux/INIT' } ]

Store

  • A store is an object that holds the application's state tree.

  • There should only be a single store.

  • The base dispatch function always synchronously sends an action to the store's reducer, along with the previous state returned by the store, to calculate a new state. Actions are expected to be plain objects.

store = {
  dispatch: Dispatch // The base dispatch function
  getState: () => State // Returns the current state of the store
  subscribe: (listener: () => void) => () => void // Registers a function to be called on state changes
  replaceReducer: (reducer: Reducer) => void // Can be used to implement hot reloading and code splitting.
}

State

import { createStore } from 'redux';

const reducer_0 = (state, action) => {
  console.log(`reducer_0 was called with state, ${state}, and action, ${action}`);
}

const store_0 = createStore(reducer_0); // reducer_0 was called with state, undefined, and action, { type: '@@redux/INIT' }
  • To get the state you call getState E.g:
console.log('store_0 state after initialization:', store_0.getState()); // store_0 state after initialization: undefined
  • After initialization state returns undefined because the reducer in this case (reducer_0) isn't modifying the state object of the application
const reducer_1 = (state, action) => {
  console.log(`reducer_1 was called with state, ${state}, and action, ${action}`);
  if (typeof state === 'undefined') {
    return {};
  }
  return state;
}

const store_1 = createStore(reducer_1); // reducer_1 was called with state, undefined, and action, { type: '@@redux/INIT' }

console.log('store_1 state after initialization:', store_1.getState()); // store_1 state after initialization: {}
const reducer_2 = (state = {}, action) => {
  console.log(`reducer_2 was called with state, ${state}, and action, ${action}`);
  return state;
}

const store_2 = createStore(reducer_2); // reducer_2 was called with state, {}, and action, { type: '@@redux/INIT' }

console.log('store_2 state after initialization:', store_2.getState()); // store_2 state after initialization: {}
  • The state parameter has been initialized in the reducer and no longer returns undefined

  • However a reducer is only called in response to an action that has been dispatched. E.g

const reducer_3 = (state= {}, action) => {
  console.log(`reducer_3 was called with state, ${state}, and action, ${action}`);
  switch (action.type) { // `action.type` assumes that the action contains a type property
    case 'SAY_SOMETHING':
    return {
      ...state, // [ES7 object spread](https://goo.gl/ST86RO) is used to merge the current state with the new returned state {message: action.value}
      message: action.value // `action.value` assumes that the action contains a value property
    }
    default: // Always use `default` to `return state` to avoid getting `undefined` and losing the current state
      return state;
  }
}

const store_3 = createStore(reducer_3); // reducer_3 was called with state {}, and action, { type: '@@redux/INIT' }

console.log('store_3 state after initialization:', store_3.getState()); // store_3 state after initialization {}

Combining Reducers

  • Boilerplate reducer function:
const reducer_0 = (state = {}, action) => {
  console.log(`reducer_0 was called with state, ${state}, and action, ${action}`);
  switch (action.type) {
    case 'SAY_SOMETHING':
    return {
      ...state,
      message: action.value
    }
    default:
    return state;
  }
};
  • Boilerplate reducer function with multiple actions:
const reducer_1 = (state = {}, action) => {
  console.log(`reducer_1 was called with state, ${state}, and action, ${action}`);
  switch (action.type) {
    case 'SAY_SOMETHING':
    return {
      ...state,
      message: action.value
    }
    case 'DO_SOMETHING':
    // ...
    case 'LEARN_SOMETHING':
    // ...
    case 'HEAR_SOMETHING':
    // ...
    case 'GO_SOMEWHERE':
    // ...
    // etc...
    default:
    return state;
  }
};
  • The boilerplate with multiple actions will get hard to maintain so it makes sense to split these actions into multiple reducers E.g:
const userReducer = (state = {}, action) => {
  console.log(`userReducer was called with state, ${state}, and action, ${action}`);
  switch (action.type) {
    // ...etc
    default:
    return state;
  }
};

const itemsReducer = (state= [], action) => {
  console.log(`itemsReducer was called with state, ${state}, and action, ${action}`);
  switch (action.type) {
    // ...etc
    default:
    return state;
  }
};
  • A reducer can handle any type of data structure; in the code above userReducer state has been initialized to an object literal ({}) while itemsReducer state has been initialized to an array ([]).

  • By splitting the actions into multiple reducers each reducer will handle only a slice of the application state

  • Though the createStore() method only accepts one reducer function, so we will have to combine them

  • We can do this by using the combineReducers() method; it takes an object and returns a function that, when invoked, will call all the reducers, retrieve the new slice of state and combine them into a state object E.g:

import { createStore, combineReducers } from 'redux';
const reducer = combineReducers({
  user: userReducer,
  items: itemsReducer
});

const store_0 = createStore(reducer);
console.log('store_0 state after initialization:', store_0.getState());

/*
 * Output:
 * userReducer was called with state {} and action { type: '@@redux/INIT' }
 * userReducer was called with state {} and action { type: * *  '@@redux/PROBE_UNKNOWN_ACTION_l.5.a.9.z.q.f.f.l.x.r' }
 * itemsReducer was called with state [] and action { type: '@@redux/INIT' }
 * itemsReducer was called with state [] and action { type: * '@@redux/PROBE_UNKNOWN_ACTION_v.d.0.f.i.b.g.w.r.k.9' }
 * userReducer was called with state {} and action { type: '@@redux/INIT' }
 * itemsReducer was called with state [] and action { type: '@@redux/INIT' }
 * store_0 state after initialization: { user: {}, items: [] }
 */

Handling actions

  • The dispatch method is used to handle actions and it is provided by Redux on the store object

  • It propagates our action(s) to all the available reducers E.g:

import { createStore, combineReducers } from 'redux';

// Create reducers:
const userReducer = (state = {}, action) => {
  console.log(`userReducer was called with state, ${state} and action, ${action}`);
  switch (action.type) {
    case 'SET_NAME':
    return {
      ...state,
      name: action.name
    }
    default:
      return state;
  }
}

const itemsReducer = (state = {}, action) => {
  console.log(`itemsReducer was called with state, ${state} and action, ${action}`);
  switch (action.type) {
    case 'ADD_ITEM':
    return [
      ...state,
      action.item
    ]
    default:
      return state;
  }
}

const reducer = combineReducers({
  user: userReducer,
  items: itemsReducer
});

// Pass reducers into store:
const store = createStore(reducer);

console.log('store state after initialization:', store.getState()); // { user: {}, items: [] }

// Dispatch an action
store.dispatch({
  type: 'AN_ACTION'
});

/* Output:
 * userReducer was called with state {} and action { type: '@@redux/INIT' }
 * userReducer was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_d.i.h.9.w.g.9.1.9.k.9' }
 * itemsReducer was called with state [] and action { type: '@@redux/INIT' }
 * itemsReducer was called with state [] and action { type: '@@redux/PROBE_UNKNOWN_ACTION_z.u.l.y.x.6.i.g.g.b.9' }
 * userReducer was called with state {} and action { type: '@@redux/INIT' }
 * itemsReducer was called with state [] and action { type: '@@redux/INIT' }
 * store state after initialization: { user: {}, items: [] }
 * userReducer was called with state {} and action { type: 'AN_ACTION' }
 * itemsReducer was called with state [] and action { type: 'AN_ACTION' }
 */
  • In the code above an action is passed directly into the dispatch method and we can see that the action (AN_ACTION) has been called on each reducer based on the last 2 lines in the output

  • But this action (AN_ACTION) doesn't modify the state and we can confirm this by calling the getState method after it was dispatched E.g

console.log('store state after action AN_ACTION :', store.getState()); // store state after action AN_ACTION: { user: {}, items: [] }
  • Using an action creator:
const setNameActionCreator = (name) => {
  type: 'SET_NAME',
  name
}

store.dispatch(setNameActionCreator('bob'));

console.log('store state after action SET_NAME :', store.getState());

/* Output:
 * userReducer was called with state {} and action { type: 'SET_NAME', name: 'bob' }
 * itemsReducer was called with state [] and action { type: 'SET_NAME', name: 'bob' }
 * store state after action SET_NAME: { user: { name: 'bob' }, items: [] }
 */
  • In the code above an action creator is used to pass the the action into the dispatch method instead and it also modifies state using the name parameter

  • The name property is initially returned in userReducer (name: action.name) and also setNameActionCreator() returns a type property that matches that case (SET_NAME). Therefore the name property returned in setNameActionCreator will update that which is returned in userReducer.

  • In the output above you can see that the application state has been updated to reflect this update: { user: { name: 'bob' }, items: [] }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment