Redux 基礎教程

前言

在沒有使用 ReduxReact 項目中的組件通訊和狀態管理是特別繁瑣的,好比子組件和父組件通訊改變值,要經過父組件的方法。使用 Redux 能夠解決組件之間的通訊問題,由於 React 提出將展現組件與容器組件分離的思想,因此下降了 ReactRedux之間的耦合度。不少人都說,簡單的應用能夠不用此工具。可是我我的認爲,中小型應用使用的話,可使文件結構更加規範,代碼可讀性更強。javascript

做用

  1. 分離UI與邏輯
  2. 跨組件通訊

安裝

npm i redux -Sjava

核心概念

1. state

單一數據源原則:整個應用的 state 數據都被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。node

state 其實就是一個普通的 JSON 對象。該對象包含 redux 的全部數據。react

當前的 state,能夠經過 store.getState() 獲取。npm

Redux 規定, 一個 State 對應一個 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什麼樣,反之亦然。redux

建議定義 state tree 層級結構不要太深。bash

{
  text: 'text 文本',
  todos: [1,2,3,4]
}
複製代碼

2. action

State 只讀原則:惟一改變 state 的方法就是觸發 actionaction 是一個用於描述已發生事件的普通對象。dom

咱們約定:action 內必須使用一個字符串類型的 type 字段來表示將要執行的動做。一般其餘參數用 payload 字段來存儲以便管理方便,結構清晰。異步

const ADD_TODO = 'ADD_TODO'

// action
{
  type: ADD_TODO,
  payload: {
    text: 'Build my first action'
  }
}
複製代碼

3. Action 建立函數

Action 建立函數其實就是生成 action 的方法。「action」 和 「action 建立函數」 這兩個概念很容易混在一塊兒,使用時最好注意區分。ide

有時候改變多少消息是,就會有多少個 action。若是每次都寫一遍相似的操做,就會顯得繁瑣、麻煩。咱們能夠經過定義一個函數來生成一個 Action,這個函數就叫 Action 建立函數。

const updateText = 'updateText'

// action 建立函數
function addTodo(text) {
  return {
    type: updateText,
    payload: {
      text,
    }
  }
}

const action = addTodo('Action Creator');
複製代碼

4. Reducer

使用純函數修改state原則:爲了描述 action 如何改變 state tree ,你須要編寫 reducers

Reducer 是一個純函數,只能經過使用Reducer 用來修改 state tree 的數據。

Reducer 是接收 stateaction 參數,返回新的 state 的一個函數。

Reducer 必須遵照的約束條件:

  1. 修改傳入參數
  2. 執行有反作用的操做,如 API 請求和路由跳轉
  3. 調用非純函數,如 Date.now()Math.random(),由於每次會獲得不同的結果,沒法預測 Reducer 執行後的結果。
(previousState, action) => newState
複製代碼
const initialState = {
  text: 'text 文本',
  todos: [1,2,3,4]
};
function updateText(state = initialState, action) {
  switch (action.type) {
    case 'updateText':
      return Object.assign({}, state, {
        text: action.payload.text
      })
    default:
      return state
  }
}
複製代碼

5. Store

使用 action 來描述「發生了什麼」,和使用 reducers 來根據 action 更新 state 的用法。Store 就是把它們聯繫到一塊兒的對象。Store 有如下職責:

  1. 維持應用的 state
  2. 提供 getState() 方法獲取 state
  3. 提供 dispatch(action) 方法更新 state
  4. 經過 subscribe(listener) 註冊監聽器
  5. 經過 subscribe(listener) 返回的函數註銷監聽器

工做流程

  1. 用戶調用 store.dispatch(action) 觸發 Action
  2. 接着 Store 調用傳入的 reducer 函數(此過程傳入當前的 state tressaction
  3. 而後 reducer 計算下一個 state 返回給 Store 並更新 state tree
  4. 最後 Store 根據新的 state tree 從新渲染 View

React 集成 Redux

1. 介紹

前面咱們介紹了 redux 的核心概念和工做流程,接着咱們介紹 React 集成 Redux 功能。

首先,安裝 ReduxReact 綁定庫:

npm i react-redux -S

爲了讓頁面的全部容器組件均可以訪問 Redux store,因此能夠手動監聽它。

一種方式是把它以 props 的形式傳入到全部容器組件中。但這太麻煩了,由於必需要用 store 把展現組件包裹一層,僅僅是由於剛好在組件樹中渲染了一個容器組件。

還有就是使用 React Redux 組件 <Provider> 來讓全部容器組件均可以訪問 store,而沒必要顯示地傳遞它。(推薦使用)。

2. 純 Redux 例子

這裏須要再強調一下: ReduxReact 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript

儘管如此,Redux 仍是和 ReactDeku 這類庫搭配起來用最好,由於這類庫容許你以 state 函數的形式來描述界面,Redux 經過 action 的形式來發起 state 變化。

這裏咱們介紹一下單純使用 Redux 來實現其用法:

  1. store 管理
reduxDemo/store.ts
複製代碼
import { createStore, Store, Reducer } from 'redux';

interface IState {
  num: number;
}

const initState = {
  num: 1,
};

const reducers: Reducer<IState> = (state = initState, action) => {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        num: state.num + 1,
      };
    case 'minus':
      return {
        ...state,
        num: state.num - 1,
      };
    default:
      return state;
  }
};

const store: Store<IState> = createStore(reducers);

export default store;

複製代碼
  1. 組件
reduxDemo/index.tsx
複製代碼
import * as React from 'react';
import { Unsubscribe } from 'redux';
import store from './store';

class Index extends React.PureComponent {
  subscribe?: Unsubscribe;

  componentDidMount(): void {
    this.subscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }

  componentWillUnmount(): void {
    if (this.subscribe) {
      this.subscribe();
    }
  }

  render(): JSX.Element {
    return (
      <div>
        <div>{store.getState().num}</div>
        <button type="button" onClick={() => store.dispatch({ type: 'add' })}>加一</button>
        <button type="button" onClick={() => store.dispatch({ type: 'minus' })}>減一</button>
      </div>
    );
  }
}

export default Index;

複製代碼

3. 使用 react-redux 例子

經過上面的例子咱們看到單純在 React 項目中使用 Redux,咱們還須要手動監聽 Storestate 的變化和取消監聽,而後在手動進行強制更新從新渲染組件。

接着咱們舉例使用在 React 項目中使用 react-redux 綁定庫例子:

  1. 展現組件

純UI展現組件,沒有任何交互邏輯,並不關心數據來源和如何改變。

components/Todo.tsx
複製代碼
import React from 'react';

interface IProps {
  text: string;
  completed: boolean;
  onClick?: (e: React.MouseEvent) => void;}

export default function Todo({
  onClick,
  completed,
  text,
}: IProps): JSX.Element {
  return (
    <li
      onClick={onClick}
      style={{
        textDecoration: completed ? 'line-through' : 'none',
      }}
    >
      {text}
    </li>
  );
}
複製代碼
components/TodoList.tsx
複製代碼
import React from 'react';
import Todo from './Todo';

interface IProps {
  todos: any[];
  onTodoClick: (id: number) => void;
}

export default function TodoList({
  todos,
  onTodoClick,
}: IProps): JSX.Element {
  return (
    <ul>
      {todos.map((todo) => (
        <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
      ))}
    </ul>
  );
}
複製代碼
components/Link.tsx
複製代碼
import React from 'react';

interface IProps {
  active: boolean;
  children: any;
  onClick: () => void;
}

export default function Link({
  active,
  children,
  onClick,
}: IProps): JSX.Element {
  if (active) {
    return <span>{children}</span>;
  }

  return (
    <a
      href="javascript(0);"
      onClick={(e) => {
        e.preventDefault();
        onClick();
      }}
    >
      {children}
    </a>
  );
}
複製代碼
components/Footer.tsx
複製代碼
import React from 'react';
import FilterLink from '../containers/FilterLink';

const Footer = (): JSX.Element => (
  <p>
    Show:
    {' '}
    <FilterLink filter="SHOW_ALL">
      All
    </FilterLink>
    {', '}
    <FilterLink filter="SHOW_ACTIVE">
      Active
    </FilterLink>
    {', '}
    <FilterLink filter="SHOW_COMPLETED">
      Completed
    </FilterLink>
  </p>
);

export default Footer;
複製代碼
  1. 容器組件

展現組件 鏈接到 Redux

containers/FilterLink.tsx
複製代碼
import { connect } from 'react-redux';
import { setVisibilityFilter, namespace } from '../actions';
import Link from '../components/Link';

const mapStateToProps = (state: any, ownProps: any): object => ({
  active: ownProps.filter === state[namespace].visibilityFilter,
});

const mapDispatchToProps = (dispatch: any, ownProps: any): object => ({
  onClick: () => {
    dispatch(setVisibilityFilter(ownProps.filter));
  },
});

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps,
)(Link);

export default FilterLink;
複製代碼
containers/VisibleTodoList.tsx
複製代碼
import { connect } from 'react-redux';
import { toggleTodo, namespace } from '../actions';
import TodoList from '../components/TodoList';

const getVisibleTodos = (todos: any[], filter: string): any[] => {
  switch (filter) {
    case 'SHOW_COMPLETED':
      return todos.filter((t) => t.completed);
    case 'SHOW_ACTIVE':
      return todos.filter((t) => !t.completed);
    case 'SHOW_ALL':
    default:
      return todos;
  }
};

const mapStateToProps = (state: any): { todos: any[] } => ({
  todos: getVisibleTodos(state[namespace].todos, state[namespace].visibilityFilter),
});

interface TDispatchProps {
  onTodoClick: (id: number) => void;
}

const mapDispatchToProps = (dispatch: any): TDispatchProps => ({
  onTodoClick: (id: number) => {
    dispatch(toggleTodo(id));
  },
});

const VisibleTodoList = connect<
  {
    todos: any[];
  },
  TDispatchProps,
  any
>(
  mapStateToProps,
  mapDispatchToProps,
)(TodoList);

export default VisibleTodoList;
複製代碼
containers/AddTodo.tsx
複製代碼
import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';

interface IProps {
  onAddClick: (text: string) => void;
}

export const AddTodo = ({ onAddClick }: IProps): JSX.Element => {
  let input: any = null;

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }
          onAddClick(input.value);
          input.value = '';
        }}
      >
        <input
          ref={(node) => {
            input = node;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

export default connect(undefined, (dispatch: any) => ({
  onAddClick: (text: string) => dispatch(addTodo(text)),
}))(AddTodo);
複製代碼
  1. action

書寫須要的 action

let nextTodoId = 2;

export const namespace = 'pages/index';

export const ADD_TODO = 'ADD_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
export const TOGGLE_TODO = 'TOGGLE_TODO';

export const addTodo = (text: string): object => {
  nextTodoId += 1;
  return {
    type: ADD_TODO,
    payload: {
      id: nextTodoId,
      text,
    },
  };
};

export const setVisibilityFilter = (filter: string): object => ({
  type: SET_VISIBILITY_FILTER,
  payload: {
    filter,
  },
});

export const toggleTodo = (id: number): object => ({
  type: TOGGLE_TODO,
  payload: {
    id,
  },
});

複製代碼
  1. Reducer

書寫 Reducer 更新 ```state tree``。

import { ReducerBuildOptions } from '@src/redux/reducer/typings';
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  namespace,
} from './actions';

export interface ModalState {
  visibilityFilter: string;
  todos: {
    id: number;
    text?: string;
    completed?: boolean;
  }[];
}

export const initialState: ModalState = {
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      id: 1,
      text: 'Consider using Redux',
      completed: true,
    },
    {
      id: 2,
      text: 'Keep all state in a single tree',
      completed: false,
    },
  ],
};

export interface ModalReducers {
  updateState: ReducerBuildOptions<ModalState, ModalState>;
  [TOGGLE_TODO]: ReducerBuildOptions<ModalState, { id: number }>;
  [ADD_TODO]: ReducerBuildOptions<ModalState, { id: number; text: string }>;
  [SET_VISIBILITY_FILTER]: ReducerBuildOptions<ModalState>;
}

const reducers: ModalReducers = {
  updateState: (state, { payload }) => ({ ...state, ...payload }),
  [TOGGLE_TODO]: (state, { payload }) => {
    const { todos } = state;

    return {
      ...state,
      todos: todos.map((todo) => (todo.id === payload?.id
        ? {
          ...todo,
          completed: !todo.completed,
        }
        : todo)),
    };
  },
  [ADD_TODO]: (state, { payload }) => ({
    ...state,
    todos: state.todos.concat({
      id: payload?.id || Math.random(),
      text: payload?.text,
      completed: false,
    }),
  }),
  [SET_VISIBILITY_FILTER]: (state, { payload }) => ({
    ...state,
    ...payload,
  }),
};

const createReducer = (initialState, handlers) =>(state = initialState, action){
  const { type } = action;

  if (type in handlers) {
    return handlers[type](state, action);
  }
  return state;
}

export default combineReducers({
  [namespace]: createReducer(initialState,reducers)
});
複製代碼
  1. React 集成 Redux
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
// 你的 reducer 集合
import reducers from './reducers'
import App from './components/App'

let store = createStore(reducers)

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

後續的計劃

  1. Redux 高級教程,其中包含異步流數據處理,動態讀取 reducersaga
  2. 手寫實現 Redux
  3. ...
相關文章
相關標籤/搜索