Redux 搭配 React

2021-09-16 09:51 更新

搭配 React

這里需要再?gòu)?qiáng)調(diào)一下:Redux 和 React 之間沒(méi)有關(guān)系。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。

盡管如此,Redux 還是和 ReactDeku 這類(lèi)框架搭配起來(lái)用最好,因?yàn)檫@類(lèi)框架允許你以 state 函數(shù)的形式來(lái)描述界面,Redux 通過(guò) action 的形式來(lái)發(fā)起 state 變化。

下面使用 React 來(lái)開(kāi)發(fā)一個(gè) todo 任務(wù)管理應(yīng)用。

安裝 React Redux

Redux 默認(rèn)并不包含 React 綁定庫(kù),需要單獨(dú)安裝。

npm install --save react-redux

智能組件(Smart Components)和笨拙組件(Dumb Components)

Redux 的 React 綁定庫(kù)擁抱了 “智能”組件和“笨拙”組件相分離 的開(kāi)發(fā)思想。

明智的做法是只在最頂層組件(如路由操作)里使用 Redux。內(nèi)部組件應(yīng)該像木偶一樣保持“呆滯”,所有數(shù)據(jù)都通過(guò) props 傳入。

位置 使用 Redux 讀取數(shù)據(jù) 修改數(shù)據(jù)
“智能”組件 最頂層,路由處理 從 Redux 獲取 state 向 Redux 發(fā)起 actions
“笨拙”組件 中間和子組件 從 props 獲取數(shù)據(jù) 從 props 調(diào)用回調(diào)函數(shù)

在這個(gè) todo 應(yīng)用中,只應(yīng)有一個(gè)“智能”組件,它存在于組件的最頂層。在復(fù)雜的應(yīng)用中,也有可能會(huì)有多個(gè)智能組件。雖然你也可以嵌套使用“智能”組件,但應(yīng)該盡可能的使用傳遞 props 的形式。

設(shè)計(jì)組件層次結(jié)構(gòu)

還記得當(dāng)初如何 設(shè)計(jì) reducer 結(jié)構(gòu) 嗎?現(xiàn)在就要定義與它匹配的界面的層次結(jié)構(gòu)。其實(shí)這不是 Redux 相關(guān)的工作,React 開(kāi)發(fā)思想在這方面解釋的非常棒。

我們的概要設(shè)計(jì)很簡(jiǎn)單。我們想要顯示一個(gè) todo 項(xiàng)的列表。一個(gè) todo 項(xiàng)被點(diǎn)擊后,會(huì)增加一條刪除線并標(biāo)記 completed。我們會(huì)顯示用戶(hù)新增一個(gè) todo 字段。在 footer 里顯示一個(gè)可切換的顯示全部/只顯示 completed 的/只顯示 incompleted 的 todos。

以下的這些組件(和它們的 props )就是從這個(gè)設(shè)計(jì)里來(lái)的:

  • AddTodo 輸入字段的輸入框和按鈕。

  • onAddClick(text: string) 當(dāng)按鈕被點(diǎn)擊時(shí)調(diào)用的回調(diào)函數(shù)。

  • TodoList 用于顯示 todos 列表。

  • todos: Array{ text, completed } 形式顯示的 todo 項(xiàng)數(shù)組。
  • onTodoClick(index: number) 當(dāng) todo 項(xiàng)被點(diǎn)擊時(shí)調(diào)用的回調(diào)函數(shù)。

  • Todo 一個(gè) todo 項(xiàng)。

  • text: string 顯示的文本內(nèi)容。
  • completed: boolean todo 項(xiàng)是否顯示刪除線。
  • onClick() 當(dāng) todo 項(xiàng)被點(diǎn)擊時(shí)調(diào)用的回調(diào)函數(shù)。

  • Footer 一個(gè)允許用戶(hù)改變可見(jiàn) todo 過(guò)濾器的組件。

  • filter: string 當(dāng)前的過(guò)濾器為: 'SHOW_ALL''SHOW_COMPLETED''SHOW_ACTIVE'。
  • onFilterChange(nextFilter: string): 當(dāng)用戶(hù)選擇不同的過(guò)濾器時(shí)調(diào)用的回調(diào)函數(shù)。

這些全部都是“笨拙”的組件。它們不知道數(shù)據(jù)是哪里來(lái)的,或者數(shù)據(jù)是怎么變化的。你傳入什么,它們就渲染什么。

如果你要把 Redux 遷移到別的上,你應(yīng)該要保持這些組件的一致性。因?yàn)樗鼈儾灰蕾?lài) Redux。

直接寫(xiě)就是了!我們已經(jīng)不用綁定到 Redux。你可以在開(kāi)發(fā)過(guò)程中給出一些實(shí)驗(yàn)數(shù)據(jù),直到它們渲染對(duì)了。

笨拙組件

這就是普通的 React 組件,所以就不在詳述。直接看代碼:

components/AddTodo.js

import React, { findDOMNode, Component, PropTypes } from 'react';

export default class AddTodo extends Component {
  render() {
    return (
      <div>
        <input type='text' ref='input' />
        <button onClick={e => this.handleClick(e)}>
          Add
        </button>
      </div>
    );
  }

  handleClick(e) {
    const node = findDOMNode(this.refs.input);
    const text = node.value.trim();
    this.props.onAddClick(text);
    node.value = '';
  }
}

AddTodo.propTypes = {
  onAddClick: PropTypes.func.isRequired
};

components/Todo.js

import React, { Component, PropTypes } from 'react';

export default class Todo extends Component {
  render() {
    return (
      <li
        onClick={this.props.onClick}
        style={{
          textDecoration: this.props.completed ? 'line-through' : 'none',
          cursor: this.props.completed ? 'default' : 'pointer'
        }}>
        {this.props.text}
      </li>
    );
  }
}

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired,
  completed: PropTypes.bool.isRequired
};

components/TodoList.js

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

export default class TodoList extends Component {
  render() {
    return (
      <ul>
        {this.props.todos.map((todo, index) =>
          <Todo {...todo}
                key={index}
                onClick={() => this.props.onTodoClick(index)} />
        )}
      </ul>
    );
  }
}

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

components/Footer.js

import React, { Component, PropTypes } from 'react';

export default class Footer extends Component {
  renderFilter(filter, name) {
    if (filter === this.props.filter) {
      return name;
    }

    return (
      <a href='#' onClick={e => {
        e.preventDefault();
        this.props.onFilterChange(filter);
      }}>
        {name}
      </a>
    );
  }

  render() {
    return (
      <p>
        Show:
        {' '}
        {this.renderFilter('SHOW_ALL', 'All')}
        {', '}
        {this.renderFilter('SHOW_COMPLETED', 'Completed')}
        {', '}
        {this.renderFilter('SHOW_ACTIVE', 'Active')}
        .
      </p>
    );
  }
}

Footer.propTypes = {
  onFilterChange: PropTypes.func.isRequired,
  filter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
};

就這些,現(xiàn)在開(kāi)發(fā)一個(gè)笨拙型的組件 App 把它們渲染出來(lái),驗(yàn)證下是否工作。

containers/App.js

import React, { Component } from 'react';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';

export default class App extends Component {
  render() {
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            console.log('add todo', text)
          } />
        <TodoList
          todos={[{
            text: 'Use Redux',
            completed: true
          }, {
            text: 'Learn to connect it to React',
            completed: false
          }]}
          onTodoClick={todo =>
            console.log('todo clicked', todo)
          } />
        <Footer
          filter='SHOW_ALL'
          onFilterChange={filter =>
            console.log('filter change', filter)
          } />
      </div>
    );
  }
}

渲染 <App /> 結(jié)果如下:

單獨(dú)來(lái)看,并沒(méi)有什么特別,現(xiàn)在把它和 Redux 連起來(lái)。

連接到 Redux

我們需要做出兩個(gè)變化,將 App 組件連接到 Redux 并且讓它能夠 dispatch actions 以及從 Redux store 讀取到 state。

首先,我們需要獲取從之前安裝好的 react-redux 提供的 Provider,并且在渲染之前將根組件包裝進(jìn) <Provider>。

index.js

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';

let store = createStore(todoApp);

let rootElement = document.getElementById('root');
React.render(
  // 為了解決在 React 0.13 的一個(gè)問(wèn)題
  // 子標(biāo)簽必須包裝成一個(gè) function。
  <Provider store={store}>
    {() => <App />}
  </Provider>,
  rootElement
);

這使得我們的 store 能為下面的組件所用。(在內(nèi)部,這個(gè)是通過(guò) React 的 "context" 特性實(shí)現(xiàn)。)

接著,我們想要通過(guò) react-redux 提供的 connect() 方法將包裝好的組件連接到Redux。盡量只做一個(gè)頂層的組件,或者 route 處理。從技術(shù)上來(lái)說(shuō)你可以將應(yīng)用中的任何一個(gè)組件 connect() 到 Redux store 中,但盡量要避免這么做,因?yàn)檫@個(gè)數(shù)據(jù)流很難追蹤。

任何一個(gè)從 connect() 包裝好的組件都可以得到一個(gè) dispatch 方法作為組件的 props。connect() 的唯一參數(shù)是 selector。此方法可以從 Redux store 接收到全局的 state,然后返回一個(gè)你的組件中需要的 props。最簡(jiǎn)單的情況下,可以返回一個(gè)初始的 state ,但你可能希望它發(fā)生了變化。

為了組合 selectors 更有效率,不妨看看 reselect。在這個(gè)例子中我們不會(huì)用到它,但它適合更大的應(yīng)用。

containers/App.js

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';

class App extends Component {
  render() {
    // Injected by connect() call:
    const { dispatch, visibleTodos, visibilityFilter } = this.props;
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={this.props.visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    );
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  })),
  visibilityFilter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
};

function selectTodos(todos, filter) {
  switch (filter) {
  case VisibilityFilters.SHOW_ALL:
    return todos;
  case VisibilityFilters.SHOW_COMPLETED:
    return todos.filter(todo => todo.completed);
  case VisibilityFilters.SHOW_ACTIVE:
    return todos.filter(todo => !todo.completed);
  }
}

// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  };
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(App);

到此為止,迷你型的任務(wù)管理應(yīng)用就開(kāi)發(fā)完畢。

下一步

參照 本示例完整 來(lái)深化理解。然后就可以跳到 高級(jí)教程 學(xué)習(xí)網(wǎng)絡(luò)請(qǐng)求處理和路由。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)