Redux 搭配 React

2021-09-16 09:51 更新

搭配 React

這里需要再強調(diào)一下:Redux 和 React 之間沒有關(guān)系。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。

盡管如此,Redux 還是和 ReactDeku 這類框架搭配起來用最好,因為這類框架允許你以 state 函數(shù)的形式來描述界面,Redux 通過 action 的形式來發(fā)起 state 變化。

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

安裝 React Redux

Redux 默認并不包含 React 綁定庫,需要單獨安裝。

npm install --save react-redux

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

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

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

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

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

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

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

我們的概要設(shè)計很簡單。我們想要顯示一個 todo 項的列表。一個 todo 項被點擊后,會增加一條刪除線并標記 completed。我們會顯示用戶新增一個 todo 字段。在 footer 里顯示一個可切換的顯示全部/只顯示 completed 的/只顯示 incompleted 的 todos。

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

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

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

  • TodoList 用于顯示 todos 列表。

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

  • Todo 一個 todo 項。

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

  • Footer 一個允許用戶改變可見 todo 過濾器的組件。

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

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

如果你要把 Redux 遷移到別的上,你應(yīng)該要保持這些組件的一致性。因為它們不依賴 Redux。

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

笨拙組件

這就是普通的 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)在開發(fā)一個笨拙型的組件 App 把它們渲染出來,驗證下是否工作。

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é)果如下:

單獨來看,并沒有什么特別,現(xiàn)在把它和 Redux 連起來。

連接到 Redux

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

首先,我們需要獲取從之前安裝好的 react-redux 提供的 Provider,并且在渲染之前將根組件包裝進 <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 的一個問題
  // 子標簽必須包裝成一個 function。
  <Provider store={store}>
    {() => <App />}
  </Provider>,
  rootElement
);

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

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

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

為了組合 selectors 更有效率,不妨看看 reselect。在這個例子中我們不會用到它,但它適合更大的應(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)用就開發(fā)完畢。

下一步

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號