hacker0limbo / my-blog

个人博客
5 stars 1 forks source link

简单用 React+Redux+TypeScript 实现一个 TodoApp (三) #17

Open hacker0limbo opened 3 years ago

hacker0limbo commented 3 years ago

前言

上一篇文章讲了讲如何结合 Redux Thunk 完成 store 中核心 Todo 切片的状态编写. 由于关于 store 部分已经全部完成了, 这篇主要谈一谈如何使用 React-Redux 结合 React Hooks 来完成 UI 部分

该篇也是本系列最后一篇文章

想跳过文章直接看代码的: 完整代码

最后的效果: todoapp

思路

这里我简单就分为三个组件:

组件分的多细其实完全看个人偏好, 比如这个项目, 完全可以抽成粒度更细致的, 比如添加 Todo 的输入框可以是单独一个组件, Todo 列表也可以是一个组件, 底下的 Footer 也可以成为一个独立的. 这里为了方便就不抽成很细的了

所有的组件都是用 hooks 编写, 包括 react-redux 部分. 所以关于 class 组件以及相关 react-redux 使用(比如 conntect) 可能需要自行谷歌了

App

先从最基本的开始, 这个组件需要配置一下 Store, 以及引入一下样式:

// components/App.tsx

import React from "react";
import TodoApp from "./TodoApp";
import { Provider } from "react-redux";
import { store } from "../store";
import "../style.css";
import "antd/dist/antd.css";

export default function App() {
  return (
    <Provider store={store}>
      <TodoApp />
    </Provider>
  );
}

这里提一下 css, 主要会用 antd 的一些组件, 同时有自定义一些样式, 都在 style.css 文件下, 有兴趣可以自己去查看, 不做深究

至此这个组件就写完了. 唯一的作用就是提供一个 store, 所有在该 provider 下的子组件都可以拿到里面的状态, 同时有别于原生的 context, 组件可以根据自己拿到的状态按需重新渲染, 不会出现有部分状态更新之后, 所有组件都重新渲染而造成性能问题.

TodoItem

一个 TodoItem 应该具有对应 store 上的如下操作:

而一个 TodoItem 里面的数据是无法单独在这个这个组件里连接 Redux 获取的(你咋知道你要的 todo 是哪个 todo). 所以正确做法应该是在父组件(也就是 TodoApp) 里面获取数据, 通过 props 传给 TodoItem, 包括对 redux 里面 action 操作也是如此

代码如下:

// components/TodoItem.tsx

import React, { useState } from "react";
import { TodoState } from "../store/todo/types";
import { Checkbox, Input, List } from "antd";
import CloseOutlined from "@ant-design/icons/CloseOutlined";

export type TodoItemProps = {
  todo: TodoState;
  handleToogle: (todoId: string, done: boolean) => void;
  handleUpdate: (todoId: string, text: string) => Promise<void>;
  handleRemove: (todoId: string) => void;
};

const TodoItem: React.FC<TodoItemProps> = props => {
  const { todo, handleToogle, handleUpdate, handleRemove } = props;
  const [updating, setUpdating] = useState(false);
  const [text, setText] = useState(todo.text);

  const handlePressEnter = () => {
    handleUpdate(todo.id, text).then(() => setUpdating(false));
  };

  return (
    <List.Item className="todo-item" onDoubleClick={() => setUpdating(true)}>
      <span className="todo-left">
        <Checkbox
          className="todo-check"
          checked={todo.done}
          onChange={() => handleToogle(todo.id, !todo.done)}
        />
        {updating ? (
          <Input
            value={text}
            onChange={e => setText(e.target.value)}
            autoFocus
            onPressEnter={handlePressEnter}
            onBlur={() => setUpdating(false)}
          />
        ) : (
          <span className={`todo-text ${todo.done ? "done" : ""}`}>
            {todo.text}
          </span>
        )}
      </span>
      <span className="todo-right" onClick={() => handleRemove(todo.id)}>
        <CloseOutlined />
      </span>
    </List.Item>
  );
};

export default TodoItem;

TodoApp

核心组件, 需要去 Redux 里面取数据以及对应的 action, 同时初始化的时候要向服务端请求数据, 所以结构可能是这样的:

// components/TodoApp.tsx

const TodoApp: React.FC = () => {
  const dispatch = useDispatch()
  const todos = useSelector(selectFilteredTodos);

  useEffect(() => {
    dispatch(setTodosRequest());
  }, [dispatch]);

  return (
    // ...
  )
}

然而很可惜, 这样很有可能 ts 编译器会报错...直接谷歌了一下发现一个类似的问题: type-safe useDispatch with redux-thunk. 其实原因很简单, 我们现在 Dispatch 的方法不是一个标准的 Action, 这个 Action 是被 Thunk 包装过的. 包括我们直接去看一下源码:

/**
 * A hook to access the redux `dispatch` function.
 *
 * Note for `redux-thunk` users: the return type of the returned `dispatch` functions for thunks is incorrect.
 * However, it is possible to get a correctly typed `dispatch` function by creating your own custom hook typed
 * from the store's dispatch function like this: `const useThunkDispatch = () => useDispatch<typeof store.dispatch>();`
 *
 * @returns redux store's `dispatch` function
 *
 */
export function useDispatch<TDispatch = Dispatch<any>>(): TDispatch;
export function useDispatch<A extends Action = AnyAction>(): Dispatch<A>;

可以看到源码的注释也非常清晰的解释了如果用到了 Thunk 那么需要自己传入泛型类型

当然包括 React Redux 官网也有写使用套路.

所以我们只需改一下:

// components/TodoApp.tsx

import { AppDispatch } from "../store";

const TodoApp: React.FC = () => {
  const dispatch = useDispatch<AppDispatch>()
  const todos = useSelector(selectFilteredTodos);

  useEffect(() => {
    dispatch(setTodosRequest());
  }, [dispatch]);

  return (
    // ...
  )
}

后面就没什么好说的了, 要拿数据只需要 useSelector(), dispatch 一个 action 不管是不是 Thunk Action 现在类型都不会有问题了. Reac Redux 和 TypeScript 的结合相比原生的 Redux 还是好很多的

最后贴一下代码:

import React, { useEffect, useState, useCallback } from "react";
import { Input, List, Radio, Spin } from "antd";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "../store";
import {
  addTodoRequest,
  removeTodoRequest,
  setTodosRequest,
  toogleTodoRequest,
  updateTodoRequest
} from "../store/todo/actions";
import { setFilter } from "../store/filter/actions";
import { FilterStatus } from "../store/filter/types";
import {
  selectFilteredTodos,
  selectUncompletedTodos
} from "../store/todo/selectors";
import { selectLoading } from "../store/loading/selectors";
import TodoItem from "./TodoItem";

const TodoApp: React.FC = () => {
  const dispatch = useDispatch<AppDispatch>();
  const todos = useSelector(selectFilteredTodos);
  const uncompletedTodos = useSelector(selectUncompletedTodos);
  const loading = useSelector(selectLoading);
  const [task, setTask] = useState("");

  useEffect(() => {
    dispatch(setTodosRequest());
  }, [dispatch]);

  const handleAddTodo = () => {
    dispatch(addTodoRequest(task)).then(() => setTask(""));
  };

  const handleToogleTodo = useCallback(
    (id: string, done: boolean) => {
      dispatch(toogleTodoRequest(id, done));
    },
    [dispatch]
  );

  const handleRemoveTodo = useCallback(
    (id: string) => {
      dispatch(removeTodoRequest(id));
    },
    [dispatch]
  );

  const handleUpdateTodo = useCallback(
    (id: string, text: string) => {
      return dispatch(updateTodoRequest(id, text));
    },
    [dispatch]
  );

  const handleFilter = (filterStatus: FilterStatus) => {
    dispatch(setFilter(filterStatus));
  };

  return (
    <div className="todo-app">
      <h1>Todo App</h1>
      <Input
        size="large"
        placeholder="新任务"
        value={task}
        onChange={e => setTask(e.target.value)}
        onPressEnter={handleAddTodo}
      />
      <Spin spinning={loading.status} tip={loading.tip}>
        <List
          className="todo-list"
          footer={
            <div className="footer">
              {uncompletedTodos.length > 0 && (
                <span className="todo-needed">
                  还剩 {uncompletedTodos.length} 项
                  <span role="img" aria-label="Clap">
                    🎉
                  </span>
                </span>
              )}
              <Radio.Group
                onChange={e => handleFilter(e.target.value)}
                size="small"
                defaultValue="all"
                buttonStyle="solid"
              >
                <Radio.Button className="filter-item" value="all">
                  全部
                </Radio.Button>
                <Radio.Button className="filter-item" value="done">
                  已完成
                </Radio.Button>
                <Radio.Button className="filter-item" value="active">
                  待完成
                </Radio.Button>
              </Radio.Group>
            </div>
          }
          bordered
          dataSource={todos}
          renderItem={todo => (
            <TodoItem
              handleRemove={handleRemoveTodo}
              handleToogle={handleToogleTodo}
              handleUpdate={handleUpdateTodo}
              todo={todo}
            />
          )}
        />
      </Spin>
    </div>
  );
};

export default TodoApp;

总结

最后一篇文章想来想去发现其实没啥好写的, 当然可能是因为我懒了只想罗列代码.

其实我甚至根本没在真实项目里用过 Redux + TypeScript. 这篇文章可以算是我一时兴起的 Demo 文章. 所以完全有可能存在很多错误. 因为很简单, 我连 TypeScript 和 React 都没写过啥项目...而且一个 TodoApp 状态来用 Redux 来管理实在有点大材小用.

讲实话, Redux 和 TypeScript 写起来是真的挺啰嗦的, 而且坑也有一些. 起码我觉得对新手不是特别友好. 有些时候为了一个非常小的类型问题需要大动周折去翻源码搜 issue 实在是有点不值得. 虽然我觉得 Redux 的文档真的已经写的很详细了. 但是有时候过分详细又会让开发者很迷茫手足无措. 写的太多, 反而找不到我想要的东西了的那种感觉

有机会我再去啾啾 Redux Toolkit 这个库吧

参考