Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
hacker0limbo opened this issue Nov 28, 2020 · 0 comments
Open

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

hacker0limbo opened this issue Nov 28, 2020 · 0 comments
Labels
react react 笔记整理 redux redux 笔记整理 typescript typescript 笔记整理

Comments

@hacker0limbo
Copy link
Owner

前言

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

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

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

最后的效果:
todoapp

思路

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

  • App
  • TodoApp
  • TodoItem

组件分的多细其实完全看个人偏好, 比如这个项目, 完全可以抽成粒度更细致的, 比如添加 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 上的如下操作:

  • 左边有一个 checkbox 能够进行勾选 toogleTodo
  • 右边有一个图标点击可以删除该 todo
  • 正常情况下中间显示 todo 的内容, 但是点击可以进行修改更新内容

而一个 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 这个库吧

参考

@hacker0limbo hacker0limbo added redux redux 笔记整理 react react 笔记整理 typescript typescript 笔记整理 labels Nov 28, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
react react 笔记整理 redux redux 笔记整理 typescript typescript 笔记整理
Projects
None yet
Development

No branches or pull requests

1 participant