-
Notifications
You must be signed in to change notification settings - Fork 1
Description
前言
上一篇文章讲了讲如何结合 Redux Thunk 完成 store 中核心 Todo 切片的状态编写. 由于关于 store 部分已经全部完成了, 这篇主要谈一谈如何使用 React-Redux 结合 React Hooks 来完成 UI 部分
该篇也是本系列最后一篇文章
想跳过文章直接看代码的: 完整代码
思路
这里我简单就分为三个组件:
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 这个库吧