banner
他山之石

他山之石

react-redux開發簡單的To do應用

redux 專注於狀態管理,和 react 解耦,為了方便使用,redux 的作者封裝了一個 react 專用的庫 react-redux。

image

前面通過一個簡單的計數器應用來學習 redux 的基本原理和用法,在本篇文章,將介紹 redux 如何與 react 結合使用。為了方便使用,redux 的作者封裝了一個 react 專用的庫 react-redux,這也是接下來的重點。

本文完整代碼請查看 Github:https://github.com/YanYuanFE/redux-app

// clone repo
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// checkout branch
git checkout part-2

// install
npm install

// start
npm start

安裝#

npm install react-redux --save

基本 API#

組件#

react-redux 提供 Provider 組件,包裹在根組件最外層,用於傳遞 store 到組件內部,讓應用內部的任何子孫組件非常方便地獲取到全局狀態,核心原理是 react 提供的 context API。示例代碼如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

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

connect()#

react-redux 提供 connect 方法用於將 react 組件與 redux 的 store 進行連接,通過 connect 方法,可以在任何 react 組件中,將 store 中的全局 state 和 Action Creator 傳遞到組件的 props 中,以供組件使用。

connect 方法接受兩個參數,mapStateToPropsmapDispatchToProps,用於定義組件的輸入輸出。mapStateToProps 負責輸入邏輯,將 state 映射到組件的 props,後者負責輸出邏輯,將 Action Creator 方法傳遞到組件的 props 中,在組件內即可發起 Action。示例代碼如下:

import { connect } from 'react-redux';

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

export default FilterLink;

mapStateToProps#

mapStateToProps 是一個函數,接受兩個參數,state 和 props,state 包含整個全局對象樹,props 代表組件的 props,一般情況下使用 state 情況最多。執行後返回一個對象,對象的鍵值對就是一個映射。示例代碼如下:

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

上述代碼中,在 mapStateToProps 中,通過全局 state 和組件的 props 進行計算,得到包含鍵 active 的對象,傳入組件,在組件中即可通過 this.props.active 來獲取值。

mapDispatchToProps#

mapDispatch 是一個函數或者對象,用於將 store.dispatch 映射到組件的 props。當 mapDiapatchToProps 是一個函數時,傳入 dispatch 和 ownProps 兩個參數,返回一個對象,對象的鍵值對定義了組件的 props 傳遞的方法以及對應的 action。示例代碼如下:

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

上述代碼將 dispatch 方法映射到組件的 props 參數 onClick,在組件內部即可通過 this.props.onClick()進行調用。

當 mapDispatch 是一個對象時,它的鍵值對分別是組件的 props 參數和一個作為 Action Creator 的函數,通常在大型應用中,會在專門的文件中定義 actionCreators 文件,如下:

export const setVisibilityFilter = (filter) => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

上述代碼就是一系列 actionCreator 之一,返回一個 Action,由 Redux 自動發出,上述代碼使用如下:

import { setVisibilityFilter } from '../actions';

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

這種方法跟寫成函數的方法其實是一樣的,只是將 action 進行了單獨的抽離,這樣可以避免更多的冗餘代碼,如需要在多個組件中發起相同的 action 時。

實戰 To do 應用#

前面已經了解了 react-redux 的核心 API,下面來完成一個完整的應用。

整個應用實現的界面如下:

image

功能主要有,輸入文字添加 todo 事項,顯示 todo list,點擊 todo 項切換狀態,點擊 tab 篩選不同的 todo 狀態。

在應用開發過程中,使用 components,首先,使用 create-react-app 來初始化 react 應用,src 為主要的開發目錄,src 目錄下,分別新建 actions、components、containers、reducers 文件夾,actions 用於編寫 actionCreator,reducers 文件夾下用於編寫應用的 reducer,component 文件夾下編寫 UI 組件,即無狀態組件,只負責 UI 的呈現,不負責業務邏輯,數據通過 props 傳入,也不操作 redux 相關的 api。containers 文件夾下編寫容器組件,負責管理數據和業務邏輯,操作 redux 的 api。

首先,在App.js中,包含了整個應用的 UI 界面。

import React, { Component } from 'react';
import Footer from './components/Footer';
import AddTodo from './containers/AddTodo';
import VisibleTodoList from './containers/VisibleTodoList';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {

    return (
      <div className="App">
        <div className="todoapp">
          <AddTodo/>
          <VisibleTodoList/>
          <Footer/>
        </div>
      </div>
    );
  }
}

export default App;

可以看到,整個應用包含三個組件AddTodoVisibleTodoListFooter

下面逐步介紹整個界面的實現。
從 containers 文件夾開始,首先是 AddTodo 組件,用於添加 todo。

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

let AddTodo = ({ dispatch }) => {
  let input

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

export default AddTodo

AddTodo 包含一個 Form 表單,在 Form 表單的 onSubmit 事件中,獲取輸入值,提交至 action,整個組件被 redux 的 connect 組件包裹,從而使得 dispatch 傳入到組件的 props 中。

下面是 actions 的 addTodo 方法。

let nextTodoId = 0
export const addTodo = (text) => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})

addTodo 用於新建 Todo,將 todo 的 id 加 1, 傳入 todo 的 text 屬性。

下面是 VisibleTodoList 組件,根據當前選中的 tab 來顯示不同狀態下的 todo list。

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)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

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

const mapDispatchToProps = {
  onTodoClick: toggleTodo
}

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

export default VisibleTodoList

從代碼中可以看到,VisibleTodoList 沒有界面相關的部分,僅僅是將 TodoList 組件進行包裹返回一個新的組件。mapStateToProps 中,根據當前所有的 todos 和篩選條件 visibilityFilter,通過 getVisibleTodos 函數,返回一個篩選後的 todos,mapDispatchToProps 則是將 actionCreator 方法通過 props 傳入到 UI 組件。

在 actions 文件夾下,index.js 中,定義了 toggleTodo 如下:

export const toggleTodo = (id) => ({
  type: 'TOGGLE_TODO',
  id
})

toggleTodo 通過傳入的 id 對當前操作的 todo 進行處理。

下面是 components 下的 TodoList 組件,負責顯示 todo list。

import React from 'react'
import PropTypes from 'prop-types'
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

從上述代碼可以看到,TodoList 接收 props 中的 todos, onTodoClick,並對 todos 進行渲染,每個 todo 又抽離成單獨的 Todo 組件,在每個 Todo 上傳遞點擊事件,點擊事件中,調用 onTodoClick,並傳入當前 todo 的 id,以此來改變 todo 的狀態。

Todo 組件如下所示:

import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
)

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

export default Todo

在 TodoList 組件中,渲染所有的 todo list 時,將每個 todo 進行對象解構,傳入 Todo 組件的 props,在 Todo 組件中,獲取到組件需要的參數 onClick, completed, text,onClick 用於綁定到 todo 的點擊事件,completed 用於表示 todo 的狀態是否為完成,根據 completed 的值來渲染 todo 的樣式,text 渲染為 todo 的內容。
下面是 components 中的 Footer 組件:

import React from 'react'
import FilterLink from '../containers/FilterLink'

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

export default Footer

Footer 組件中,用於對 todos 的狀態進行篩選,共包含三個篩選項,SHOW_ALL 顯示所有 todos,SHOW_ACTIVE 顯示剛添加的 todo,SHOW_COMPLETED 顯示已經完成的 todo。每個篩選項又抽離成 FilterLink 組件,傳入 filter 作為篩選參數。

下面是 container 中 FilterLink 的代碼。

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

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

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

export default FilterLink

FilterLink 組件也僅僅是負責數據的邏輯,沒有 UI 呈現。mapStateToProps 中判斷當前組件的 props.filter 與篩選條件 state.visibilityFilter 來計算 active 屬性傳入 props,mapDispatchToProps 定義 onClick 方法,用於 dispatch 一個 action 來改變當前的篩選條件,即 state.visibilityFilter,傳入當前組件的 filter 屬性作為參數。

actions 的 setVisibilityFilter 代碼如下:

export const setVisibilityFilter = (filter) => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

下面是 Link 組件。

import React from 'react'
import PropTypes from 'prop-types'

const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    // eslint-disable-next-line
    <a href="#"
       onClick={e => {
         e.preventDefault()
         onClick()
       }}
    >
      {children}
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link

Link 組件通過 props 接收 active,children,onClick 屬性值,當 active 為 true 時,Link 渲染為 span,否則渲染為 a,a 標籤綁定點擊事件,點擊時,觸發 props 中的 onClick 方法。

自此,界面部分的代碼都已經介紹完畢,下面是 reducer 的編寫。
在 reducer 的編寫中,為了讓 reducer 的職責更為清晰,將 reducer 拆分為 todos 和 visibilityFilter,todos.js 中,用於處理 todo 的添加,狀態改變。代碼如下:

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case 'TOGGLE_TODO':
      return state.map(todo =>
        (todo.id === action.id)
          ? {...todo, completed: !todo.completed}
          : todo
      )
    default:
      return state
  }
}

export default todos

此處,todos 初始化為一個空數組,reducer 必須為純函數,處理 actions,不能直接對 state 進行修改,應該每次都返回一個新的 state,當添加 Todo 時,使用數組展開運算符對 state 進行解構,並且,將新的 todo 對象作為數組的最後一個值,最終,生成一個全新的 state。當修改 todo 狀態時,使用數組的 map 方法,同樣返回一個新的 state,在 map 方法內部,使用三元運算符對 action 參數 id 和數組每一項的 id 進行判斷,目的是尋找到當前被點擊時的 todo,將其 completed 值取反,否則直接返回當前 todo。

visibilityFilter.js 中,主要是用於更改 todo list 的篩選條件,代碼如下:

const visibilityFilter = (state = 'SHOW_ALL', action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

export default visibilityFilter

此時,state 初始化為 SHOW_ALL,即應用加載時,默認顯示為全部的 todos,當處理 actions,直接返回 action.filter。

當 reducer 被拆分為單個文件時,上述 const 命名的函數名即為全局狀態樹中的 state 值,還需要將拆分的 reducer 進行組合,組合 reducer 主要使用了 redux 的 combineReducers API,以下為 reducers 文件夾下 index.js 代碼:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp

最後一步,需要使用 react-redux 的 Provider API 包裹整個應用,src 下 index.js 中:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import './index.css';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);

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

瀏覽器運行效果如下:

image

至此,整個 To do 應用就開發完畢了,對比使用 redux 開發,react-redux 讓我們開發 react 應用的時候更加簡單。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。