Taming Large React Applications w/ Redux

About the Speaker

  • Senior UI Developer at PureCars, a digital marketing and automation company in the automotive space

  • Moonlights as a consultant with his brother where they work with small businesses to create modern dashboard applications.

  • Love ultimate frisbee and watching NFL

Joel Kanzelmeyer

React

React Primer

  • Simple
  • Declarative
  • Composable components

Thinking in React: Components

facebook.github.io/react

Redux

Flux Architecture Primer 

Traditional MVC

Controller

Model

Model

Model

Model

Model

View

View

View

View

View

Flux Architecture Primer 

Flux

Dispatcher

Store

View

Action

Action

What is Redux?

"A predictable state container for JavaScript apps."

Single store for all application state

Never modify state directly

"Actions" are dispatched and "reducers" know how state should be changed for each action

Popular implementation of the Flux architecture

Why Redux Helps Applications Scale Well

Easily Testable

Easily Testable

// reducer.js

export function reducer(state, action) {
  switch (action.type) {
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      });
    default:
      return state;
  }
}
// reducer-spec.js

import { reducer } from './reducer';

const state = {
  todos: []
};

const action = {
  type: 'ADD_TODO',
  text: 'Write unit tests'
};

describe('todos reducer', () => {
  it('should handle the "ADD_TODO" action', () => {
    const newState = reducer(state, action);
    expect(newState.todos[0].text).toEqual(action.text);
    expect(newState.todos[0].completed).toBeFalsy();
  })
})

Easy to reason about

(RIP Gene Wilder)

Good separation of concerns

Your business logic is kept out of your views

Developer adoption

A community of great developers support Redux

Middleware

Enhance base functionality with custom middleware

React Boilerplate

The struggle is real

JS Tool Fatigue

React Boilerplate to the rescue!

  • Most stable/production-ready solution I've found
  • Follows good practices for large React apps
  • Server-side rendering
  • Code splitting (lazy load code when needed)
  • Hot module reloading (excellent for debugging)
  • ES6 support and CSS modules/PostCSS

React Boilerplate to the rescue!

Best Practices

Redux at Scale

Best Practice #1

Smart containers and dumb components

Redux at Scale

Dumb Components

(Presentational)

Dumb Component Example

const DumbComponent = (props) => {

  const { text, onChange } = props;
  
  return (
    <div>
      <p>Your text: {text}</p>
      <input value={text} onChange={onChange} />
    </div>
  );

};

Smart Containers

(Stateful)

Smart Container Example

//...
import { updateText } from '../actions';
//...

class SmartContainer extends React.Component {
  render() {
    const { text, updateText } = this.props;
    return (
      <DumbComponent text={text} onChange={updateText} />
    );
  }
}

const mapStateToProps = (state) => ({
  text: state.text
});

const mapDispatchToProps = (dispatch) => ({
  updateText: (evt) => dispatch(updateText(evt.target.text))
});

export default connect(mapStateToProps, mapDispatchToProps)(SmartContainer);

Benefits

  • Better separation of concerns
  • Better reusability
  • A library of UI components

Best Practice #2

Immutability

Redux at Scale

Equality checking in Javascript

1 === 1
'string' === 'string'
true === true
const obj1 = { prop: ’someValue’ };
const obj2 = { prop: ’someValue’ };
console.log(obj1 === obj2);   // false

Why is this a problem?

Fine-tuning React Performance

shouldComponentUpdate

class NewsFeed extends React.Component {

  shouldComponentUpdate(nextProps) {
    return !_.isEqual(this.props.articles, nextProps.articles) ||
      !_.isEqual(this.props.user, nextProps.user);
  }

  render() {
    const { user, articles } = this.props;
    return (
      <div>
        <p>Hello, {user.firstName}! Here is your news for the day:</p>
        <div>
        {
          articles.map((article) => (
            <Article article=[article} />
          ))
        }
        </div>
      </div>
    );
  }

}

Immutable.js

shouldComponentUpdate with Immutable.js


  shouldComponentUpdate(nextProps) {
    return this.props.articles !== nextProps.articles ||
      this.props.user !== nextProps.user;
  }

  shouldComponentUpdate(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  }

react-addons-shallow-compare

Benefits

  • Improves performance
  • Simplifies shouldComponentUpdate
  • Easier to reason about

Best Practice #3

Normalized state tree

Redux at Scale

API returns deeply nested objects

// API response
[
  {
    "id": 1,
    "title": "Why React is Awesome",
    "author": {
      "id": 1,
      "name": "Joel Kanzelmeyer"
    }
  },
  {
    "id": 2,
    "title": "Why Redux is Great",
    "author": {
      "id": 1,
      "name": "Joel Kanzelmeyer"
    }
  }
]
// Redux state
{
  articles: [{
    id: 1,
    title: 'Why React is Awesome',
    author: {
      id: 1,
      name: 'Joel Kanzelmeyer'
    }
  }, {
    id: 2,
    title: 'Why Redux is Great',
    author: {
      id: 1,
      name: 'Joel Kanzelmeyer'
    }
  }]
}
// Redux state
{
  articles: [{
    id: 1,
    title: 'Why React is Awesome',
    author: {
      id: 1,
      name: 'Joel Kanzelmeyer'
    }
  }, {
    id: 2,
    title: 'Why Redux is Great',
    author: {
      id: 1,
      name: 'Joel Cancelmeyer'
    }
  }]
}

normalizr

Turns this...

// API response
[
  {
    "id": 1,
    "title": "Why React is Awesome",
    "author": {
      "id": 1,
      "name": "Joel Kanzelmeyer"
    }
  },
  {
    "id": 2,
    "title": "Why Redux is Great",
    "author": {
      "id": 1,
      "name": "Joel Kanzelmeyer"
    }
  }
]
// Normalized result
{
  entities: {
    authors: {
      1: {
        id: 1,
        name: 'Joel Kanzelmeyer'
      }
    },
    articles: {
      1: {
        id: 1,
        name: 'Why React is Awesome'
      },
      2: {
        id: 2,
        name: 'Why Redux is Great'
      }
    }
  },
  result: {
    authors: [1],
    articles: [1, 2]
  }
}

into this.

Example with normalizr

// Redux state
{
  articles: [{
    id: 1,
    title: 'Why React is Awesome',
    author: {
      id: 1,
      name: 'Joel Kanzelmeyer'
    }
  }, {
    id: 2,
    title: 'Why Redux is Great',
    author: {
      id: 1,
      name: 'Joel Kanzelmeyer'
    }
  }]
}
// Redux state with normalizr
{
  authors: {
    byId: {
      1: {
        id: 1,
        name: 'Joel Kanzelmeyer'
      }
    },
    all: [1]
  },
  articles: {
    byId: {
      1: {
        id: 1,
        title: 'Why React is Awesome',
        author: 1
      },
      2: {
        id: 2,
        title: 'Why Redux is Great',
        author: 1
      }
    },
    all: [1, 2]
  }
}

redux-normalizr-middleware

Action/reducer example

import { Schema, arrayOf } from 'normalizr';  

// define your schemas
const article = new Schema('articles');
const author = new Schema('authors');

// define nesting rules
article.define({
  author: author
});

export const getArticlesSuccess = (response) => ({
  type: 'GET_ARTICLES_SUCCESS',
  payload: response.body,
  meta: {
    // provide middleware with normalizr schema
    schema: arrayOf(article)
  }
});
// API response
[
  {
    "id": 1,
    "title": "Why React is Awesome",
    "author": {
      "id": 1,
      "name": "Joel Kanzelmeyer"
    }
  },
  {
    "id": 2,
    "title": "Why Redux is Great",
    "author": {
      "id": 1,
      "name": "Joel Kanzelmeyer"
    }
  }
]
// define initial state for reducers
const initialState = Immutable.Map({
  byId: Immutable.Map(),
  all: Immutable.Set()
});

// define default reducer that will merge in normalized state
const defaultReducer = (schemaName, state, action) => {
  if (action.entities && action.entities[schemaName]) {
    return state
      .mergeIn(['byId'], action.entities[schemaName])
      .mergeIn(['all'], action.result[schemaName]);
  }
  return state;
}

// authors reducer
const authors = (state = initialState, action) => {
  switch (action.type) {
    case 'UPDATE_AUTHOR':
      return state.mergeIn(['byId', action.id], action.author);
    default:
      return defaultReducer('authors', state, action);
  }
};

// articles reducer
const articles = (state = initialState, action) => {
  return defaultReducer('articles', state, action);
};

// root reducer
const reducer = combineReducers({
  authors,
  articles
});

Benefits

  • Normalizes deeply nested objects
  • Prevents duplicate state
  • Normalization can happen in middleware

Problems You'll Encounter

Redux at Scale

(and solutions to them)

Problem #1

Asynchronous/chained actions

(actions with side-effects)

Redux at Scale

Example

// API service
const getBooks = () =>
  fetch('/api/books')
    .then(handleErrors)
    .then(parseJSON);
// View
vm.loadBooks = () => {
  vm.booksLoading = true;
  booksApi.getBooks()
    .then((books) => {
      vm.booksLoading = false;
      vm.books = books;
    })
    .catch((err) => {
      vm.booksLoading = false;
      vm.error = err;
    });
};

MVC

// Actions
const getBooks = () => {
  return fetch('/api/books')
    .then(handleErrors)
    .then(parseJSON)
    .then((books) => ({
      type: 'GET_BOOKS',
      payload: books
    });
};
onComponentWillMount() {
  this.props.getBooks();
  // Uncaught Error: Actions must be plain objects.
  // Use custom middleware for async actions.
}

React/Redux

What We Really Need

// Actions

const getBooks = () => ({
  type: 'GET_BOOKS'
});

const getBooksSuccess = (books) => ({
  type: 'GET_BOOKS_SUCCESS',
  payload: books
});

const getBooksFailure = (error) => ({
  type: 'GET_BOOKS_FAILURE',
  payload: error
});
// Reducer

const booksReducer = (state, action) => {
  switch (action.type) {
    case 'GET_BOOKS':
      return {
        ...state,
        loading: true,
        error: null
      };
    case 'GET_BOOKS_SUCCESS':
      return {
        ...state,
        loading: false,
        books: action.payload
      };
    case 'GET_BOOKS_FAILURE':
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    default:
      return state;
  }
};

redux-thunk

Solution #1

Thunked Action

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

Example with redux-thunk

// Actions

// create a thunked action
const getBooks = () => {
  return (dispatch, getState) => {
    // check to see if books have already been fetched
    if (!getState().books) {
      // if not, dispatch the GET_BOOKS action
      dispatch({
        type: 'GET_BOOKS'
      });
      // then you make your http request
      booksApi.getBooks()
        .then((books) => {
          // dispatch success action
          dispatch(getBooksSuccess(books));
        })
        .catch((error) => {
          // dispatch failure action
          dispatch(getBooksFailure(error));
        });
    }
  };
};

const getBooksSuccess = (books) => ({
  type: 'GET_BOOKS_SUCCESS',
  payload: books
});

const getBooksFailure = (error) => ({
  type: 'GET_BOOKS_FAILURE',
  payload: error
});
// in your component

onComponentWillMount() {
  this.props.getBooks();
  /**
    dispatches GET_BOOKS and
    then when response comes back
    it will dispatch either
    GET_BOOKS_SUCCESS or
    GET_BOOKS_FAILURE
  **/
}

render() {
  const {
    isLoading,
    error,
    books
  } = this.props;
  if (isLoading) {
    return (
      <div>loading...</div>
    );
  }
  if (error) {
    return (
      <div>error: {error}</div>
    );
  }
  return (
    <BooksList books={books} />
  );
}

redux-thunk

  • Handle side-effects within your action creators
  • Able to unit test, must mock redux store and http requests

redux-saga

Solution #2

(My preference, for what it's worth)

redux-saga

class UserComponent extends React.Component {
  ...
  onSomeButtonClicked() {
    const { userId, dispatch } = this.props
    dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
  }
  ...
}
import { takeEvery } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
import Api from '...'

// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: 'USER_FETCH_SUCCEEDED', user: user});
   } catch (e) {
      yield put({type: 'USER_FETCH_FAILED', message: e.message});
   }
}

/*
  Starts fetchUser on each dispatched 'USER_FETCH_REQUESTED' action.
  Allows concurrent fetches of user.
*/
function* mySaga() {
  yield* takeEvery('USER_FETCH_REQUESTED', fetchUser);
}

export default mySaga;

Why I prefer redux-saga over redux-thunk

Easier to reason about

import { takeEvery } from 'redux-saga'
import { call, put, select } from 'redux-saga/effects'
import { getBooksSuccess, getBooksFailure } from './actions';

const selectBooks = (state) => state.books;

function* fetchBooks(action) {
  // check to see if books exist in state already
  const books = yield select(selectBooks);
  if (!books) {
    try {
      // call booksApi.getBooks to get books from API
      const _books = yield call(booksApi.getBooks);
      // dispatch GET_BOOKS_SUCCESS
      yield put(getBooksSuccess(_books));
    } catch (e) {
      // if request fails, dispatch GET_BOOKS_FAILURE
      yield put(getBooksFailure(e.message));
    }
  }
}

// "listener" function that will listen for GET_BOOKS actions
function* booksSaga() {
  yield* takeEvery('GET_BOOKS', fetchBooks);
}

export default booksSaga;
// Actions

// create a thunked action
const getBooks = () => ({
  type: 'GET_BOOKS'
});

const getBooksSuccess = (books) => ({
  type: 'GET_BOOKS_SUCCESS',
  payload: books
});

const getBooksFailure = (error) => ({
  type: 'GET_BOOKS_FAILURE',
  payload: error
});

Why I prefer redux-saga over redux-thunk

Testing is simpler

import test from 'tape';

import { put, call } from 'redux-saga/effects'
import { delay } from 'redux-saga'
import { incrementAsync } from './sagas'

test('incrementAsync Saga test', (assert) => {
  const gen = incrementAsync()

  assert.deepEqual(
    gen.next().value,
    call(delay, 1000),
    'incrementAsync Saga must call delay(1000)'
  )

  assert.deepEqual(
    gen.next().value,
    put({type: 'INCREMENT'}),
    'incrementAsync Saga must dispatch an INCREMENT action'
  )

  assert.deepEqual(
    gen.next(),
    { done: true, value: undefined },
    'incrementAsync Saga must be done'
  )

  assert.end()
});
import { put, call } from 'redux-saga/effects'
import { delay } from 'redux-saga'

export function* incrementAsync() {
  // use the call Effect
  yield call(delay, 1000)
  yield put({ type: 'INCREMENT' })
}

Why I prefer redux-saga over redux-thunk

Highly de-coupled

  • Your actions no longer contain business logic
  • Business logic can be encapsulated in sagas
  • Add new logic to existing actions by creating sagas

redux-saga

  • Keeps your actions pure
  • Handle side-effects in a "separate thread" with sagas
  • Able to unit test without mocking surrounding environment

Problem #2

Selecting state, especially derived state, is painful and non-performant

Redux at Scale

Example

// SomeComponent.js
...
const mapStateToProps = (state) => {
  const books = state.books; // Immutable map
  const filterText = state.filterText;
  return {
    filteredBooks: books.filter((book) => {
      return book.name.includes(filterText);
    })
  };
};
...
export default connect(mapStateToProps)(SomeComponent);

Why is this a problem?

const mapStateToProps = (state) => {
  const books = state.books; // Immutable map
  const filterText = state.filterText;
  return {
    filteredBooks: books.filter((book) => {
      return book.name.includes(filterText);
    })
  };
};
  • mapStateToProps is called on every state change
  • Shallowly compares and re-renders only when changed
  • Filtering, sorting, mapping, etc always returns new instance

reselect

Solution

reselect

// selectors.js
import { createSelector } from 'reselect';

const booksSelector = (state) => state.books;
const filterTextSelector = (state) => state.filterText;

export const filteredBooksSelector = createSelector(
  booksSelector,
  filterTextSelector,
  (books, filterText) =>
    books.filter((book) => {
      return book.name.includes(filterText);
    })
);
// SomeComponent.js
import { filteredBooksSelector } from './selectors.js';
...
const mapStateToProps = (state) => {
  return {
    filteredBooks: filteredBooksSelector(state)
  };
};
...
export default connect(mapStateToProps)(SomeComponent);

reselect

  • Better performance via memoized selectors
  • Selectors are now composable
  • Added benefit of encapsulating state selection logic

Problem #3

Too much boilerplate code and too reliant on string constants

Redux at Scale

export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const EDIT_TODO = 'EDIT_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const COMPLETE_ALL = 'COMPLETE_ALL';
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
import * as types from './constants';

const addTodo = (text) => ({
  type: types.ADD_TODO,
  payload: { text }
});

const removeTodo = (id) => ({
  type: types.REMOVE_TODO,
  payload: { id }
});

const editTodo = (id, text) => ({
  type: types.EDIT_TODO,
  payload: { id, text }
});

const completeTodo = (id) => ({
  type: types.COMPLETE_TODO,
  payload: { id }
});

const completeAll = () => ({
  type: types.COMPLETE_ALL
});

const clearCompleted = () => ({
  type: types.CLEAR_COMPLETED
});
import {
  ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO,
  COMPLETE_ALL, CLEAR_COMPLETED
} from './constants';

const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        },
        ...state
      ]

    case DELETE_TODO:
      return state.filter(todo =>
        todo.id !== action.id
      )

    case EDIT_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, text: action.text } :
          todo
      )

    case COMPLETE_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, completed: !todo.completed } :
          todo
      )

    case COMPLETE_ALL:
      const areAllMarked = state.every(todo => todo.completed)
      return state.map(todo => ({
        ...todo,
        completed: !areAllMarked
      }))

    case CLEAR_COMPLETED:
      return state.filter(todo => todo.completed === false)

    default:
      return state
  }
}

Constants

Actions

Reducer

Example

redux-act

Solution

createAction

Simpler, more consistent way to create redux action creators

const addTodo = createAction('Add todo');

addTodo('content');
// return {
//   type: '[1] Add todo',
//   payload: 'content'
// }

const editTodo = createAction(
  'Edit todo',
  (id, content) => ({id, content})
);

editTodo(42, 'the answer');
// return {
//   type: '[2] Edit todo',
//   payload: {
//     id: 42,
//     content: 'the answer'
//   }
// }

const serializeTodo = createAction('SERIALIZE_TODO');
serializeTodo(1);
// return { type: 'SERIALIZE_TODO', payload: 1 }

createReducer

Simpler, more consistent way to create redux reducers

const increment = createAction();
const add = createAction();


// First pattern
const reducerMap = createReducer({
  [increment]: (state) => state + 1,
  [add]: (state, payload) => state + payload
}, 0);


// Second pattern
const reducerFactory = createReducer(function (on, off) {
  on(increment, (state) => state + 1);
  on(add, (state, payload) => state + payload);
  // 'off' remove support for a specific action
}, 0);
import { createAction } from 'redux-act';

const addTodo = createAction('ADD_TODO');
const removeTodo = createAction('REMOVE_TODO');
const editTodo = createAction('EDIT_TODO');
const completeTodo = createAction('COMPLETE_TODO');
const completeAll = createAction('COMPLETE_ALL');
const clearCompleted = createAction('CLEAR_COMPLETED');
import {
  addTodo, deleteTodo, editTodo, completeTodo,
  completeAll, clearCompleted
} from './actions';
import { createReducer } from 'redux-act';

const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default createReducer({
  [addTodo]: (state, { text }) => [
    {
      id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
      completed: false,
      text
    },
    ...state
  ],
  [deleteTodo]: (state, { id }) => state.filter(todo => todo.id !== id),
  [editTodo]: (state, { id, text }) => state.map(todo =>
    todo.id === id ?
      { ...todo, text } :
      todo
  ),
  [completeTodo]: (state, { id }) => state.map(todo =>
    todo.id === id ?
      { ...todo, completed: !todo.completed } :
      todo
  ),
  [completeAll]: (state) => {
    const areAllMarked = state.every(todo => todo.completed)
    return state.map(todo => ({
      ...todo,
      completed: !areAllMarked
    }))
  },
  [clearCompleted]: (state) => state.filter(todo => todo.completed === false)
}, initialState)

Actions

Reducer

export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
export const EDIT_TODO = 'EDIT_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const COMPLETE_ALL = 'COMPLETE_ALL';
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
import * as types from './constants';

const addTodo = (text) => ({
  type: types.ADD_TODO,
  payload: { text }
});

const removeTodo = (id) => ({
  type: types.REMOVE_TODO,
  payload: { id }
});

const editTodo = (id, text) => ({
  type: types.EDIT_TODO,
  payload: { id, text }
});

const completeTodo = (id) => ({
  type: types.COMPLETE_TODO,
  payload: { id }
});

const completeAll = () => ({
  type: types.COMPLETE_ALL
});

const clearCompleted = () => ({
  type: types.CLEAR_COMPLETED
});
import {
  ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO,
  COMPLETE_ALL, CLEAR_COMPLETED
} from './constants';

const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        },
        ...state
      ]

    case DELETE_TODO:
      return state.filter(todo =>
        todo.id !== action.id
      )

    case EDIT_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, text: action.text } :
          todo
      )

    case COMPLETE_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, completed: !todo.completed } :
          todo
      )

    case COMPLETE_ALL:
      const areAllMarked = state.every(todo => todo.completed)
      return state.map(todo => ({
        ...todo,
        completed: !areAllMarked
      }))

    case CLEAR_COMPLETED:
      return state.filter(todo => todo.completed === false)

    default:
      return state
  }
}

Constants

Actions

Reducer

redux-act

  • Reduce boilerplate
  • Use actions themselves as references rather than constants 
  • Flux Standard Action compliant

Honorable Mentions

redux-actions

Conclusion

  • Utilize "smart containers" and "dumb components"
  • Use Immutable data to help performance
  • A normalized state tree helps avoid duplication
  • Conquer side-effects with redux-thunk or redux-saga
  • Create performant, composable selectors with reselect
  • Use redux-act to reduce boilerplate and simplify

Links

Joel Kanzelmeyer

Questions?

Taming Large React Applications w/ Redux

By Joel Kanzelmeyer

Taming Large React Applications w/ Redux

In this talk, Joel will introduce concepts that make large React applications more scalable and maintainable. You will learn the benefits of Redux a predictable, single-way data flow model as he walks through sample code and best practices like top down approach. You’ll walk away with what you need to know to architect a React application with all the patterns that help it scale well with your team. We will cover bootstrapping a modern React application with react-boilerplate (http://reactboilerplate.com) and using libraries like redux-saga and reselect to improve code re-use, removing coupling, and reducing complexity.

  • 23,325