Skip to content

Instantly share code, notes, and snippets.

@mondaychen
Last active June 4, 2022 03:16
Show Gist options
  • Save mondaychen/47e9ba6133c6a190abe7 to your computer and use it in GitHub Desktop.
Save mondaychen/47e9ba6133c6a190abe7 to your computer and use it in GitHub Desktop.
Sparks from Redux

Sparks from Redux

##Intro to Redux

###Some important concepts of Redux

Reference: Three Principles, Basics

  • A single Store to maintain the state tree

    • The state tree is a pure JS object.
    • The shape of the Store should be designed top-down. You are always aware of the whole tree.
    • The states tree should be normalized instead of deeply nested. An officially recommended approach: https://github.com/gaearon/normalizr
  • A single root Reducer to manage the states tree

    • Reducers are pure functions which calculate a new state given an old state.
    • You can have many reducers, each of which manages a small part of the state tree, and finally combine them into one root reducer. Redux offers a helper utility functions combineReducers().
    • Reducers can both initialize (when old state is not given) and mutate (when old state is given) the state tree. But a reducer never mutate an object directly. Instead, it returns a new state without touching the old state.

Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API calls. No mutations. Just a calculation.

  • Many Actions to trigger the Reducers
    • Actions are created by Action Creator functions
    • type of an Action indicates which part of Reducer will be executed

Data flow

store.dispatch(action) -> store call reducer to update itself

Details explained here: http://redux.js.org/docs/basics/DataFlow.html

Redux architecture revolves around a strict unidirectional data flow.

###How Redux works with React

The package react-redux provides a component wrapper called <Provider> and an important helper function called connect.

<Provider> is used to connect React dom tree to store.

let store = createStore(rootReducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

connect is used to bind data and functions to component. The following example is how connect provide props to a component TodoList.

components/TodoList.js:

Please note that TodoList is not aware of Redux.

import React, { PropTypes } from 'react'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map(todo =>
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => onTodoClick(todo.id)}
      />
    )}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    completed: PropTypes.bool.isRequired,
    text: PropTypes.string.isRequired
  }).isRequired).isRequired,
  onTodoClick: PropTypes.func.isRequired
}

export default TodoList

containers/VisibleTodoList.js:

connect takes two (actually four, but later two are less important) optional arguments. mapStateToProps allows you to map data from the state tree to data needed in props of the component. mapDispatchToProps allows you to provide functions to props, with access to store.dispatch.

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

##Thoughts on Redux

Global store actually works

A global store made no sense to me when I first saw it, because you can always change something inside it secretly (it's not even Immutable) and the complexity of the state tree can explode very quickly. If all components rely on the same Store, the performance will be terrible.

However, following the convention of Redux:

  1. The state tree will never be updated without notifying the system.
  2. Components do NOT rely on the whole tree. Instead, using mapStateToProps, they only read some small pieces from the tree, and only be re-rendered when these pieces change.
  3. When the state tree is normalized, it's no longer deeply nested. It means, you will not write a long chained statement like state.todos[id].fields.author.history[id] to read the data. And the performance of reducer will not get worse and worse.

A normalized global store + getter & setter is like database in frontend

A globally accessible Store certainly provides flexibility. For example, two components that are far away from each other in the dom structure can be bound to a same piece of data. Even if they both need to read and write the data, there's no need to worry about passing instances around. However, this may also cause hidden dependencies which are very hard to maintain. Here's the solution: treat your Store like it's your database in frontend.

Although not mentioned as a primary principal in the official docs, I believe it's a good practice to write getter functions for the state. Then we have APIs for both GET method (getter functions) and POST/PUT/PATCH/DELETE method (Actions describe the API and Reducers describe the behavior) to Store. A component will use getter functions to subscribe the data it needs into its props, and call "API"s to update the Store. When you feel necessary to refactor the data structure, the only thing you need to make sure of is that the APIs are still functioning in the same ways. It should not be a hard thing to do for fine-grained APIs.

Provider and connect involve too much black magic

This is what I dislike about react-redux:

  • They tell you it works in this way but no one explains the underlying mechanism.

  • connect is an example of badly designed APIs. It takes four optional arguments.

    • [mapStateToProps(state, [ownProps]): stateProps] (Function)
    • [mapDispatchToProps(dispatch, [ownProps]): dispatchProps](Object or Function)
    • [mergeProps(stateProps, dispatchProps, ownProps): props](Function)
    • [options] (Object)
  • And everyone is using mapDispatchToProps in different ways.

Conclusion (or Random Thoughts)

  • A globally accessible Store working as a database in frontend, with proper APIs associated, will provide good flexibility.
  • Redux demands a strict holding of conventions. Any abuse of accessibility or bad implementation could break the unidirectional data flow.
  • We don't have to use react-redux as long as we have a good replacement offering access to the Store, dispatch, and a good way to do data mapping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment