抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
1324 words
7 minutes
A Translated Introduction to Redux: Basics (Part 1)

The Three Principles of Redux#

  • The entire application state is stored in a single store as an object tree.
  • The only way to change this object tree is by dispatching actions, which are objects describing what happened.
  • By writing pure function reducers, you describe how an action transforms the entire object tree.

Action#

Actions are payloads of information that send data from your application to your store. They are the sole source of information for the store. You send them to the store using store.dispatch(). An action might look like this:

const ADD_TODO = 'ADD_TODO'
{
    type: ADD_TODO,
    text: 'Bulid my first Redux App'
}

Actions are plain JavaScript objects, but they must have a type property that indicates the type of action being performed. Types should be defined as string constants. As your project grows, you might want to move them into a separate, isolated module.

import { ADD_TODO, REMOVE_TODO } from "../actionTypes";

Unlike type, the rest of the action’s structure is entirely up to you. However, you can refer to the Flux Standard Action guidelines to learn how to better organize the structure of your actions.

Action Creators#

Action Creators are functions that create actions. In Redux, action creators simply return an action object:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text: "Some text",
  };
}

This makes it easier to create a class of actions and improves testability.

Dispatch#

To actually initiate a dispatch, you pass the result to the dispatch() function:

store.dispatch(addTodo(text));

Alternatively, you can create a bound action creator that automatically dispatches:

const boundAddTodo = (text) => dispatch(addTodo(text));

Now you can call them directly:

boundAddTodo(text);

The dispatch() function can directly access the store via store.dispatch(), but you might prefer to access it using a helper function like connect() from react-redux. You can use bindActionCreators() to automatically bind many action creators to the dispatch() function.

Reducers#

Actions describe the fact that something happened, but they don’t specify how the application’s state changes. That’s what reducers do.

Designing State Structure#

In Redux, all application state is stored in a single object. Therefore, it’s important to consider how to design your state before writing code. How can you describe the application state as an object in the simplest way? For a todo application, we want to store two different things:

  • The currently selected visibility filter
  • The actual list of todos

A simple state would look like this:

{
   visibilityFilter: 'SHOW_ALL',
   todos: [
       {
           text: 'Consider using Redux',
           completed: true
       },{
           text: 'Keep all state in a single tree',
           completed: false
       }
   ]
}

Handling Actions#

A reducer is a pure function that takes the previous state and an action as arguments, and returns the next state.

(previousState, action) => newState;

It’s called a reducer because it’s meant to be passed to the Array.prototype.reduce(reducer, ?initialValue) function. Therefore, it’s very important to keep reducers pure. Never do the following inside a reducer:

  • Mutate its function arguments
  • Perform side effects, such as API calls and routing transitions
  • Call impure functions, such as Date.now() and Math.random()

With this understanding, let’s start with the reducer function. We begin by specifying the initial state. Redux will pass an undefined state on the first call to the reducer. At this point, we need to return an initialized state:

import { VisibilityFilters } from "./actions";

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todo: [],
};

function todoApp(state = initialState, action) {
  //采用 ES2015 写法,当 state 传递为 undefined 时,会被赋值为 initialState
  return state;
}

Next, let’s handle SET_VISIBILITY_FILTER. What we need to do is change visibilityFilter on the state.

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.SET_VISIBILITY_FILTER,
      });
    default:
      return state;
  }
}

It’s important to note:

  1. We do not modify the original state. We create a copy by merging the original state with the changes using Object.assign().
  2. When no matching action type is found (i.e., the default case), you must return the previous state.

Handling More Actions#

There are still some actions to handle, so let’s add them one by one.

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter,
      });
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false,
          },
        ],
      });
    default:
      return state;
  }
}

We can make the reducer function easier to understand by separating it, as putting todo-related logic and visibilityFilter logic together isn’t very clear. Separating reducers is also very simple.

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false,
        },
      ];
    default:
      return state;
  }
}

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter;
    default:
      return state;
  }
}

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action),
  };
}

As you can see, each reducer manages its own part of the overall state. The state parameter for each reducer is different, corresponding to the specific part of the state they manage. When an application grows large, we can separate reducers into multiple files, maintaining independence and managing different data sources. Finally, Redux provides a combineReducers() function that does the same logic as the todoApp above, merging multiple reducers, which can eliminate a lot of boilerplate code.

import { combineReducers } from "redux";

const todoApp = combineReducers({
  visibilityFilter,
  todos,
});

export default todoApp;

It is completely equivalent to the following:

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action),
  };
}

What combineReducers() does is generate a function that passes the corresponding state to each reducer function and merges them into a single object. It’s not black magic.

How combineReducers Works#

When I first looked at Redux in 2015, I didn’t read the documentation, and I never understood how combineReducers() actually worked, thinking it was some kind of black magic. And it could also generate the initial store using the function returned by combineReducers().

Today, after re-reading the documentation and the black magic issue, I finally understood. Actually, combineReducers() works like this. Suppose you have two reducers, one for managing the todo list and another for managing the currently selected filter state:

 function todos(state = [], action) {
   // Somehow calculate it...
   return nextState
 }
function visibleTodoFilter(state = 'SHOW_ALL', action) {
   // Somehow calculate it...
   return nextState
 }
let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })

As you can see, each reducer defines its default state: todos is [], and visibleTodoFilter is SHOW_ALL. When an action is dispatched, the todoApp returned by combineReducers will call all reducers:

let nextTodos = todos(state.todos, action);
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);

Ultimately, it will merge the state returned by each reducer into a single state tree.

return {
  todos: nextTodos,
  visibleTodoFilter: nextVisibleTodoFilter,
};

This way, you can understand how combineReducers works. Of course, you can choose not to use combineReducers; after all, it’s just an official helper utility, and you can implement your own root reducer.

Store#

The Store is an object that brings them all together. The Store’s responsibilities include:

  • Holds the entire application state
  • Allows access to the state via getState()
  • Allows the state to be updated via dispatch(action)
  • Registers listeners via subscribe(listener)
  • Handles unregistering listeners via the value returned by subscribe(listener)

It’s important to note that there can only be one store in a Redux application. When you want to separate data logic, you can do so by creating more reducers instead of more stores. Once you have a reducer, creating a store is easy. We can create a store using the root reducer created after combineReducers().

import { createStore } from "redux";

import todoApp from "./reducers";

let store = createStore(todoApp);

You can also initialize the state by passing an optional argument. This is useful when developing a universal application, where you can use the state passed from the server as the client’s initial state.

let store = createStore(todoApp, window.STATE_FROM_SERVER);

Summary#

With this understanding, you should have a good grasp of how Redux works. It’s clear that reading the documentation is very helpful, unlike last year when I went straight to the source code of official examples, which left me confused and ultimately without a clear understanding. Next, I will continue to learn about data flow and integration with React, and build a TodoList example to deepen my understanding.

References#

Basics|Redux

This article was published on April 27, 2016 and last updated on April 27, 2016, 3449 days ago. The content may be outdated.

A Translated Introduction to Redux: Basics (Part 1)
https://blog.kisnows.com/en-US/2016/04/27/step-to-redux-1/
Author
Kisnows
Published at
2016-04-27
License
CC BY-NC-ND 4.0