TS + React Hooks TodoMVC

TodoMVC 與 Redux

TodoMVC

TodoMVC是一個示例項目,它使用目前流行的不一樣JavaScript框架的來實現同一個Demo,來幫助你熟悉和選擇最合適的前端框架。css

怎樣快速學習和了解一門JavaScript框架或學習使用新框架的特性? 個人經驗是作一個TodoMVC應用吧html

TodoMVC官網 React示例前端

TodoMVC 應用功能點vue

  • 數據的 CRUD
  • 數據模型的管理
  • MVC的理論實踐
  • 組件拆分
  • 狀態管理

TodoMVC 是一個功能和UI都十分完備的教學示例,實乃學習框架之必備🌰react

Redux 簡介

Redux是遵循 Flux 模式的一種實現,是一個狀態管理庫,適用於 ReactAngularVueJs 等框架或庫,而不是侷限於某一特定UI庫。git

Redux中文文檔github

Redux 核心概念npm

+----------+
|          |                +-----------+       sliceState,action        +------------+
|  Action  |dispacth(action)|           +-------------------------------->            |
|          +---------------->   store   |                                |  Reducers  |
| Creators |                |           <--------------------------------+            |
|          |                +-----+-----+    (state, action) => state    +------------+
+-----^----+                      |
      |                           |
      |                           |subscribe
      |                           |
      |                           |
      |                           |
      |              +--------+   |
      +--------------+  View  <---+
                     +--------+
複製代碼

更新View的方式redux

  • dispatch 觸發 action
  • action 攜帶方法名 type, 和數據 payloadstore(期間可能進行異步數據處理)
  • store 接收到 typepayload 交給相應的 reducer
  • reducer 找到對應的方法更新 state

Store、Action 與 Reducerbash

  • Store 是整個 Redux 應用的狀態容器,是一個對象
  • Action 也是一個對象,代表事件,須要有 type 字段
  • Reducer 是一個函數,會根據不一樣 Action 來決定返回不一樣的數據

目標

  • 應用TodoMVC UI
  • 所有使用函數式組件
  • 使用TypeScript
  • 學習使用Hooks(useEffect、useContext、useRef、useMemo、useState)
  • 模擬Redux

需求分析

新增Todo

  • 在輸入框填寫新增的值,按Enter鍵新增並清空輸入框

刪除Todo

  • 點擊刪除「按鈕x」刪除選擇數據
  • 點擊右下角「clear complete」清空已 complete 的數據

修改Todo

  • 雙擊某行數據時切換到編輯狀態並自動得到焦點
  • 按回車鍵或光標離開能夠修改數據

查詢Todo

  • 當list有值時,能夠正常顯示數據
  • 左下角「item left」根據當前item數自動變化
  • 點擊下方的all active complete能夠過濾list,而且高亮自動能切換
  • 勾選item能夠切換 activecomplete 樣式
  • 當item全選時,新增數據框左側的「小箭頭」高亮
  • 點擊新增數據框左側的「小箭頭」,能夠全選或全不選item
  • 進入首頁時,新增輸入框自動得到焦點

安裝 TodoMVC 模塊

npm install todomvc-common todomvc-app-css

import 'todomvc-common/base.css';
import 'todomvc-app-css/index.css';
複製代碼

直接使用vue版TodoMVC模板,去掉vue相關代碼,保留結構

使用 todomvc UI, 能夠免去樣式和dom結構編寫,讓咱們專一於使用JS框架快速實現業務邏輯

組件拆分

頁面基本結構

// App.tsx
const Input: SFC<any> = () => {...}
const TodoItem: SFC<ITodoItemProps> = {...}
const TodoList: SFC<any> = () => {...}
const Footer: SFC<any> = () => {...}

function TodoAPP() {
  return (
      <section className="todoapp"> <header className="header"> <h1>todos</h1> <Input /> </header> <TodoList /> <Footer /> </section>
  );
}
複製代碼

Store

肯定應用狀態和事件方法

應用全局狀態

const initialState: IAPPState = {
  todos: todoStore.list,
  newTodo: '',
  editTodo: '',
  visibility: ShowType.ALL,
};
複製代碼

這裏狀態的劃分爲全局狀態和局部狀態,局部狀態由組件內部管理,後面會說到,因此TodoMVC 的全局狀態基本就是這4個就夠了,在實際項目應用中,全局狀態儘可能少,要作到每一個狀態都是全局必須的,避免在全局存放過多的狀態,可能會引發組件沒必要要的更新。

事件方法

export enum ActionType {
  // 新增Todo
  CREATE = 'create',
  // 更新Todo
  UPDATE = 'update',
  // 刪除Todo
  DELETE = 'delete',
  // 刪除狀態是已完成的Tdo
  REMOVE_COMPLETED = 'removeCompleted',
  // 設置當前編輯的Todo
  EDIT_SET = 'setEdit',
  // 改變當前顯示類型
  CHANGE_SHOW_TYPE = 'changeShowType',
  // 更新當前編輯的Todo
  UPDATE_EDIT_TODO = 'updateEditTodo',
  // 所有切換爲 完成/未完成
  TOGGLE_ALL = 'toggleAll',
}
複製代碼

使用 createContext 建立 Context 組件

使用 useReducer 管理複雜的 state

useReducer 接收一個形如 (state, action) => newStatereducer,並返回當前的 state 以及與其配套的 dispatch 方法

使用 <Context.Provider> 作爲父組件的組件,將在 state 更新時,觸發更新

// Store.tsx
const initialState: IAPPState = {
  todos: todoStore.list,
  newTodo: '',
  editTodo: '',
  visibility: ShowType.ALL,
};

const reducer = (state: IAPPState, action: IAction): IAPPState => {
  const { type, payload } = action;
  return methods[type] ? methods[type](state, payload) : state;
};

const Context = React.createContext({
  state: initialState,
  dispatch: (() => 0) as React.Dispatch<IAction>,
});

const Provider: SFC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return <Context.Provider value={{ state, dispatch}}>{children}</Context.Provider>;
};

export { Provider, Context };
複製代碼

上面的 methods 是具體更新 state 的方法對象

使用Store

Provider 的子組件能夠從 Context 中獲取全局的 statedispatch

// App.tsx
import { Provider, Context } from './Store'

function TodoAPP() {
  return (
    <Provider>
      <section className="todoapp">
        ...
      </section>
    </Provider>
  );
}
複製代碼

Input 輸入組件

使用 useContext 接收 Store 中的 Context 對象,並返回 statedispatch

當組件上層最近的 <Context.Provider> 更新時,該 hook 會觸發組件更新,並使用最新的 state

調用了 useContext 的組件總會在 context 值變化時從新渲染

使用 useState 建立組件內部狀態 title, 該狀態在組件內部使用或銷燬,並不影響全局state, 接收一個初始狀態的初始值和更新狀態的函數 changeTitle

使用 dispatch 觸發一個 action 操做,建立一個 todo

const Input: SFC<any> = () => {
  const { state, dispatch } = useContext(Context);
  // 內部狀態 title
  const [title, changeTitle] = useState<string>('');
  
  // 輸入時更新 title
  function onChange(e: ChangeEvent<HTMLInputElement>) { 
    changeTitle(e.target.value.trim());
  }
  
  // 回車新增一條Todo
  function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {
    // 新增
    dispatch({
      type: ActionType.CREATE,
      payload: { id: utils.uuid(), title, completed: false },
    });
    // 清空
    changeTitle('');
  }

  return (
    <input
      className="new-todo"
      autoFocus
      autoComplete="off"
      placeholder="What needs to be done?"
      value={title}
      onChange={onChange}
      onKeyDown={onKeyDown}
    />
  );
};
複製代碼

TodoItem 組件

editing 是屬於每個 TodoItem 的內部狀態

使用 useRef 來返回一個可變的 ref 引用對象,其 .current 屬性被初始化爲傳入的參數,經常使用於訪問 DOM對象,這裏用來使用編輯輸入框獲取焦點

useEffect 告訴組件須要在渲染後執行某些操做。React 會保存你傳遞的函數(咱們將它稱之爲 「effect」),而且在執行 DOM 更新以後調用它, 這裏傳入第二個參數 [editing] 作爲依賴,以優化 effect 執行次數,只有當 editing 改變時,才執行

const TodoItem: SFC<ITodoItemProps> = ({ completed, title, index }) => {
  const [editing, changeEditing] = useState<boolean>(false);
  const iptRef = useRef(null);

  const { state, dispatch } = useContext(Context);
  const { editTodo } = state;

  // DOM更新,獲取焦點
  useEffect(() => (editing && iptRef.current.focus()), [editing]);
  // 雙擊編輯
  function onDoubleClick() {...}
  // 失焦,更新Todo
  function onEditBlur() {...}
  // 回車,更新Todo
  function onEditEnter(e: KeyboardEvent<HTMLInputElement>) {...}
  // 更新當前編輯的Todo
  function onEditChange(e: ChangeEvent<HTMLInputElement>) {...}
  // 切換完成狀態
  function onToggleComplete(e: ChangeEvent<HTMLInputElement>) {...}
  // 刪除
  function onDestroy() {
  
  return (
    <li>
    ...
    </li>
  )
}
複製代碼

Footer 組件

使用 useMemo 獲取相似Vue中的 計算屬性,避免在每次渲染時都進行高開銷的計算,傳入 todos, 當 todos 更新時才從新計算

const Footer: SFC<any> = () => {
  const { state, dispatch } = useContext(Context);
  const { todos, visibility } = state;

  // 計算屬性:正在進行中的數量、已完成的數量、提示文字
  const { activedNum, completedNum, activeTodoWord } = useMemo(() => {...})
  
  // 改變顯示類型
  function onChangeShowType(type: ShowType) {...}
  
  // 清空已完成的Todo
  function onClearCompleted() {...}
  
  return (
    <footer className="footer">
    ...
    </footer>
  )
}
複製代碼

TodoList 組件

const TodoList: SFC<any> = () => {
  const { state, dispatch } = useContext(Context);
  const { todos, visibility } = state;

  const activedNum = useMemo(() => utils.filterTodos(todos, ShowType.ACTIVE).length, [todos]);
  const todoList = useMemo(() =>utils.filterTodos(todos, visibility), [todos, visibility]);

  function onToggleAll(e: ChangeEvent<HTMLInputElement>) {...}
  
  return (
    <section className="main">
      <input
        id="toggle-all"
        className="toggle-all"
        type="checkbox"
        onChange={onToggleAll}
        checked={activedNum === 0}
      />
      <label htmlFor="toggle-all">Mark all as complete</label>
      <ul className="todo-list">
        {todoList.map((v, i) => {
          return <TodoItem key={v.id} completed={v.completed} title={v.title} index={i} />;
        })}
      </ul>
    </section>
  )
}
複製代碼

爲了防止文章過長,並無粘貼全部代碼,主要說明 React 中的一些 hooksTodoMVC 中的一些實際應用場景,以便於咱們更容易更快速的理解和掌握hooks的用法

優化

在咱們公司的實際應用中,並無使用 redux 來管理應用狀態,而是使用 mobx,在實際開發中我更傾向局部狀態與全局狀態分而治之,在後臺管理系統中,全局的狀態並很少,無非就是一些用戶信息、菜單狀態和數據、消息等等,通常以頁面劃分 store,複雜的組件有本身的 store, store 中包含了全部狀態和操做方法。

Redux 的缺點

  • 樣板代碼管理比較分散,須要創建 actionTypesactionsreducers
  • 多人協做時就會出現名字衝突,類似業務的流程重複
  • dispatch 須要引用 actions 或要記憶 Action Type 等問題

優化 dispatch

在上面的 hooks 實現的僞 redux 中, dispatch 方法使用仍然比較繁瑣,能夠優化下面兩點

  • UI邏輯處理時,不用引用 ActionsTypes
  • 直接調用 Actions, 並有方法提示
import { ActionsTypes } from './types'
import { Context } from './store'

// 如今的調用方式
const {state, dispatch} = useContext(Context)

dispatch({
  type: ActionTypes.TOGGLE_ALL,
  payload: { completed },
});

// 優化後的調用方式
const {state, actions} = useContext(Context)

actions.toggleAll({ completed })
複製代碼

這裏的優化實現主要在 Provider 的實現中, 只要把 Actions 方法掛載到 Context 中就能夠

const Provider: SFC<any> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  // 生成 Actions
  type IActions = { [name in ActionType]?: (data?: Record<string, any>) => void };
  let actions: IActions = {};
  for (let i in ActionType) {
    actions[ActionType[i] as ActionType] = data =>
      dispatch({ type: ActionType[i] as ActionType, payload: data });
  }

  // Context 掛載 state、dispatch、actions
  return <Context.Provider value={{ state, dispatch, actions }}>{children}</Context.Provider>;
};
複製代碼

Vue 相較於 React 有一個優勢就是 Vue 的包比 React 要小,在 React 中使用 setState 來管理應用狀態,相比於 Vue,代碼寫法上稍顯繁瑣,引用第三方的庫如 Reduxmobx 無疑又會增長React的體積,hooks 的出現給了咱們以但願,在一些相對簡單的應用中徹底能夠代替 mobxredux。可是一樣也要當心使用,沒有了生命週期的控制,使用不當也容易形成組件重複屢次渲染產生性能瓶頸,因此在使用 hooks 時,要多思考怎樣使用更合理,怎樣纔不會產生性能問題。

相關文章
相關標籤/搜索