TodoList—react+ts(基础入门项目)
本文是面向react零基础,速通react基础知识后夯实基础的练手小项目,网上我找了很多,并没有非常详细,那些教程对于零基础的友友们有一定难度。
本文涵盖react全面的核心知识点,能够熟悉react的开发实战,也会在每一步解释讲解以及点出遇到的错误解决,确保做完项目同时理解知识点,希望大家跟着一起做,一定会有所收获的!
注意:本文项目是我跟着AI一步步完成,进行整理总结得出的,如果有错误或者更好的办法,欢迎指正!
项目简单展示查看

目录
该项目涵盖知识点
-
从零创建项目
-
React 函数组件 + TS 类型
-
最常用 Hooks(useState/useEffect/useRef)
-
多路由管理(一级/二级子路由)
-
CSS 样式(模块化 / 普通样式)
-
全局状态管理
-
组件拆分、事件、表单
-
实现增删改查+筛选
-
本地存储持久化
第一步:创建项目
前提:需要提前配置node.js
1.打开终端(window+R 或者 开始搜索cmd)

2.先创建存放项目文件夹,进入文件夹
cd 你的文件夹路径
或者你直接在文件夹右击打开终端,就不需要cd切换了
3.创建项目
输入指令
npx create-react-app react-ts-todo --template typescript
等待安装,安装成功进入项目
cd react-ts-todo
启动项目(如果要选择是否继续,点击Y即可)
npm start
浏览器自动打开,出现react页面即创建成功

第二步:清理项目+定义ts
我们可以把 src 原来的项目全部删除,也可以选择保留以下文件,这里我是选择删除全部
index.tsx
App.tsx
App.css
index.css
1.创建src/index.tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
// 这里暂时先不引入样式,避免报错
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
2.创建App.tsx
// src/App.tsx
function App() {
return <div>Hello React TS</div>;
}
export default App;
此时在控制台运行项目,页面显示Hello React TS,就初始化成功了!


第三步:实现多页面+多级路由
在这一步,我是打算有一个导航栏,实现一级路由的切换
- 路由分类(一级,二级)
- 全局导航挂载页面
- 子路由出口
- 实现路由
1.安装依赖
npm install react-router-dom
2.新建所需页面
在pages新建4个文件夹

3.编写简单页面
- pages/Home.tsx
function Home() {
return <h2>首页</h2>;
}
export default Home;
- pages/Todo.tsx
function Todo() {
return <h2>Todo 待办事项</h2>;
}
export default Todo;
- pages/User.tsx
function User() {
return <h2>用户中心</h2>;
}
export default User;
- pages/UserDetail.tsx
function UserDetail() {
return <h3>用户详情页(多级子路由)</h3>;
}
export default UserDetail
4.创建全局导航栏
import { Link, Outlet } from 'react-router-dom';
export default function Layout() {
return (
<div>
{/* 导航:只放一级! */}
<nav style={{ padding: '1rem 2rem', background: '#f5f5f5', marginBottom: '2rem' }}>
<Link to="/" style={{ marginRight: 16 }}>首页</Link>
<Link to="/todo" style={{ marginRight: 16 }}>待办清单</Link>
<Link to="/user" style={{ marginRight: 16 }}>用户中心</Link>
</nav>
<div style={{ padding: '0 2rem' }}>
<Outlet />
</div>
</div>
);
}
5.子路由显示
对于子路由UserDetail,必须在其父路由outlet
//pages/User.tsx
import { Outlet ,Link} from 'react-router-dom';
function User() {
return (
<div>
<h2>用户中心</h2>
<Link to="detail">前往用户详情</Link>
{/* 子路由会显示在这里 */}
<Outlet />
</div>
);
}
export default User;
6.新建router
src/
├── router/ ← 你要的路由文件夹
在router创建文件index.tsx
7.编写代码
import { createBrowserRouter } from 'react-router-dom';
import Layout from '../components/Layout';
import Home from '../pages/Home';
import Todo from '../pages/Todo';
import User from '../pages/User';
import UserDetail from '../pages/UserDetail';
const router = createBrowserRouter([
{
//全局导航栏
element: <Layout />,
//一级路由
children: [
{ path: '/', element: <Home /> },
{ path: '/todo', element: <Todo /> },
// 用户中心 + 子路由
{
path: '/user',
element: <User />,
// 二级子路由
children: [
{ path: 'detail', element: <UserDetail /> },
],
},
],
},
]);
export default router;
核心知识点
BrowserRouter
整个应用的路由容器,必须包在最外层,让所有页面都能用路由。
Routes
路由的 “容器”,里面放一个个页面。
Route
代表一个页面:
path="/"就是网址路径element={<div>...</div>}就是这个页面显示什么内容
注意:
复杂项目开发,我们通常要使用router进行路由管理
如果是简单的路由,就在App通过route跳转
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<div>首页</div>} />
<Route path="/todo" element={<div>Todo 页面</div>} />
</Routes>
</BrowserRouter>
);
}
export default App;
8.编写App.tsx
// App.tsx
import { RouterProvider } from 'react-router-dom';
import router from './router'; // 引入路由配置
function App() {
return (
// 路由挂载共享
<RouterProvider router={router} />
);
}
export default App;
此时,打开你的页面,能够实现路由跳转就可以下一步了

第四步:实现Todo待办
1.新增待办
打开 src/pages/Todo.tsx,清空内容,开始编写正式代码
知识前置
interface:数据定义类型限制
useState:react组件的“记忆功能”,存储数据+改数据
受控组件:value+onChange实现输入框的双向绑定
1.待办事项类型定义
interface TodoItem {
id: number; // 唯一编号
content: string; // 内容
completed: boolean; // 是否完成
}
2.实现输入框双向绑定
import { useState } from 'react';
export default function Todo() {
const [inputValue, setInputValue] = useState('');
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>待办清单</h2>
{/* 输入框 + 新增按钮 */}
<div style={{ marginBottom: 20, display: 'flex', gap: '8px' }}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请输入待办内容"
style={{ flex: 1, padding: '8px' }}
/>
</div>
);
}
3.列表渲染
//定义列表
const [list, setList] = useState<TodoItem[]>([]);
//实现列表渲染
<div>
{list.map((item) => (
<div
key={item.id}
style={{
padding: '12px',
border: '1px solid #eee',
marginBottom: '8px',
borderRadius: '4px',
}}
>
{item.content}
</div>
))}
</div>
</div>
4.添加新增按钮
const addTodo = () => {
// 空内容不添加
if (!inputValue.trim()) return;
// 新待办对象(严格遵循 TS 类型)
const newTodo: TodoItem = {
id: Date.now(), // 用时间戳当唯一 id
content: inputValue,
completed: false,
};
// 修改数据:...list = 复制原来的列表,再追加新的
setList([...list, newTodo]);
// 清空输入框
setInputValue('');
};
<button onClick={addTodo} style={{ padding: '8px 16px' }}>
添加
</button>
5.完整代码
import { useState } from 'react';
// ======================
// TS 知识点:定义类型
// 给待办事项规定数据结构,防止乱写
// ======================
interface TodoItem {
id: number; // 唯一编号
content: string; // 内容
completed: boolean; // 是否完成
}
export default function Todo() {
// ======================
//Hook 知识点:useState
// list:数据本身
// setList:修改数据的方法
// ======================
// 待办列表(初始为空数组)
const [list, setList] = useState<TodoItem[]>([]);
// 输入框的值(双向绑定)
const [inputValue, setInputValue] = useState('');
// ======================
// 新增待办事项
// ======================
const addTodo = () => {
// 空内容不添加
if (!inputValue.trim()) return;
// 新待办对象(严格遵循 TS 类型)
const newTodo: TodoItem = {
id: Date.now(), // 用时间戳当唯一 id
content: inputValue,
completed: false,
};
// 修改数据:...list = 复制原来的列表,再追加新的
setList([...list, newTodo]);
// 清空输入框
setInputValue('');
};
// ======================
// 页面渲染
// ======================
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📋 待办清单</h2>
{/* 输入框 + 新增按钮 */}
<div style={{ marginBottom: 20, display: 'flex', gap: '8px' }}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请输入待办内容"
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={addTodo} style={{ padding: '8px 16px' }}>
添加
</button>
</div>
{/* 待办列表 */}
<div>
{/* 列表为空时显示 */}
{list.length === 0 ? (
<div style={{ padding: '20px', textAlign: 'center', color: '#999' }}>
暂无任务,快去添加吧!
</div>
) : (
list.map((item) => (
<div
key={item.id}
style={{
padding: '12px',
border: '1px solid #eee',
marginBottom: '8px',
borderRadius: '4px',
}}
>
{item.content}
</div>
))
)}
</div>
);
}
注意:
1.return后面是使用(),而不是{}
2.map如果写了箭头函数+{},需要手写return,箭头函数+()可以自动return
3.修改数组/对象,不可以直接该值(像inputValue就可以直接set新值),需要存为新对象再合并修改
检验
1.清空所有任务,显示暂无任务
2.添加任务,任务列表渲染正确
2.删除任务
1.添加删除方法
// 删除待办
const deleteTodo = (id: number) => {
// 过滤掉 id 相同的项
setList(list.filter(item => item.id !== id));
};
2.添加删除按钮
{/* 待办列表 */}
<div>
{/* 省略... */}
{item.content}
<button
onClick={() => deleteTodo(item.id)}
style={{
padding: '4px 8px',
backgroundColor: '#ff4444',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
删除
</button>
</div>
))
)}
</div>
知识点
1.filter会返回一个新数组,保留所有 id 不相等的项** → 等于删除了点击的那一个
2.方法带参必须写成箭头函数,否则方法后面()会立即执行函数,而不是点击后触发
检验
点击删除,列表对应id任务删除成功
3.实现状态切换
1.编写切换状态函数
// 切换 完成/未完成
const toggleComplete = (id: number) => {
setList(
list.map((item) =>
item.id === id ? { ...item, completed: !item.completed } : item
)
);
};
2.列表添加复选框
{/* 待办列表 */}
<div>
{/* 切换复选框 */}
<input
type="checkbox"
checked={item.completed}
onChange={() => toggleComplete(item.id)}
style={{ cursor: 'pointer', scale: '1.2' }}
/>
{item.content}
{/* 省略... */}
</div>
))
)}
</div>
3.设置完成文字样式(删除线)
{/* 待办列表 */}
<div>
{/* 省略 */}
<span
style={{
textDecoration: item.completed ? 'line-through' : 'none',
opacity: item.completed ? 0.6 : 1
}}
>
{item.content}
</span>
{/* 省略... */}
</div>
))
)}
</div>
知识点
1.map用于遍历任务,如果id匹配—>复制+completed取反,不匹配直接返回原item
2.react状态不允许直接修改,必须返回新数组/新对象
4.本地存储
1.导入useEffect
import { useState, useEffect } from 'react';
2.修改useState初始值
从本地存储读取:
const [list, setList] = useState<TodoItem[]>(() => {
const local = localStorage.getItem('todolist');
return local ? JSON.parse(local) : [];
});
3.useEffect监听list变化并保存
useEffect(() => {
localStorage.setItem('todolist', JSON.stringify(list));
}, [list]);
知识点
1.useEffect(fn,[list]):list变化时自动执行副作用
2.localstorage只存储字符串,需要转义
为什么不把本地存储存放在add方法中呢?
因为每次list变化都需要更新localstorage的数据,意味着除了add方法,delete/toggle方法都需要添加setItem的方法,不雅观且冗余
useEffect就可以实现监听list变化执行函数,一次书写,自动监听变化,因此使用useEffect
检验
添加/修改列表,进行刷新,查看列表数据是否仍然存在
第五步:全局状态管理
全局状态管理(Context + useReducer)是 React 面试 + 实战重中之重,这里我会很详细的说清楚每一步
目标
把 Todo 的 list、add、delete、toggle、修改 从组件内抽出去,变成全局状态,任何页面都能用。
1.搭建context架子
(基本结构,没有具体的逻辑)
(1)创建全局状态文件夹
src/
├── store/ ← 新建这个
│ └── todo/
│ └── index.tsx
├── pages/
├── router/
...
(2)定义类型,创建context
- 导入需要的api
import { createContext, ReactNode } from 'react';
- 定义Todo类型,和上面Todo.tsx文件的类型定义一致
export interface TodoItem {
id: number;
content: string;
completed: boolean;
}
- 定义context要暴露的属性+方法
interface TodoContextType {
list: TodoItem[];
addTodo: (content: string) => void;
deleteTodo: (id: number) => void;
toggleComplete: (id: number) => void;
}
- 创建context
export const TodoContext = createContext<TodoContextType | null>(null);
知识点
createContext—创建全局容器
TodoContextType—规定容器中有什么内容
(3)创建provider包裹组件
// 提供者组件,把状态包起来给子组件用
export const TodoProvider = ({ children }: { children: ReactNode }) => {
return (
<TodoContext.Provider value={{
list: [],
addTodo: () => {},
deleteTodo: () => {},
toggleComplete: () => {}
}}>
{children}
</TodoContext.Provider>
);
};
知识点
TodoProvider是状态管理者里面的
value就是全局共享的东西
(4)入口文件全局包裹
import { TodoProvider } from './store/todo';
root.render(
<React.StrictMode>
<TodoProvider>
<App />
</TodoProvider>
</React.StrictMode>
);
作用:
此时就已经把list以及增删改查的方法都暴露全局,所有组件都可以导入使用
2.状态由组件转移context
(1)将Todo页面的useState和localStorage逻辑搬到TodoProvider
- 导入api
import { createContext, ReactNode, useState, useEffect } from 'react';
TodoProvider状态逻辑实现
export const TodoProvider = ({ children }: { children: ReactNode }) => {
// 👇 这部分从页面移到全局
const [list, setList] = useState<TodoItem[]>(() => {
const saved = localStorage.getItem('todolist');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem('todolist', JSON.stringify(list));
}, [list]);
// 暂时空实现,后面填
const addTodo = (content: string) => {};
const deleteTodo = (id: number) => {};
const toggleComplete = (id: number) => {};
return (
<TodoContext.Provider
value={{
list,
addTodo,
deleteTodo,
toggleComplete,
}}
>
{children}
</TodoContext.Provider>
);
};
作用:
1.list实现全局,任何页面都可以使用
2.持久化逻辑共享
(2)实现全局方法
addTodo方法
将原来的空方法占位修改成以下代码
const addTodo = (content: string) => {
if (!content.trim()) return;
const newTodo = {
id: Date.now(),
content,
completed: false,
};
setList([...list, newTodo]);
};
deleteTodo方法
const deleteTodo = (id: number) => {
setList(list.filter(item => item.id !== id));
};
toggleComplete方法
const toggleComplete = (id: number) => {
setList(
list.map((item) =>
item.id === id ? { ...item, completed: !item.completed } : item
)
);
};
3.页面修改,使用共享数据+方法
全局已经共享了list/add/delete/toggle方法,我们的Todo.tsx页面可以直接使用这些,不需要单独实现,因此可以删除重复的代码逻辑
- 导入
Todocontext
import { useState, useContext } from 'react';
import { TodoContext } from '../store/todo';
- 解构取出共享属性+方法
const todoCtx = useContext(TodoContext);
const [inputValue, setInputValue] = useState('');
if (!todoCtx) return null;
const { list, addTodo, deleteTodo, toggleComplete } = todoCtx;
- 按钮直接使用全局
//增加
onClick={() => addTodo(inputValue)}
//转换
onChange={() => toggleComplete(item.id)}
//删除
onClick={() => deleteTodo(item.id)}
此时,页面组件不再有:
- 自己的
listuseState - 自己的
addTodo/deleteTodo/toggleComplete - 自己的
useEffect存 localStorage
检验
走一遍流程(增删改),查看功能是否仍可以使用,是否还可以持久化存储
4.useReducer 优化
为什么要 useReducer?
- 现在
Context里有:add /delete/toggle - 以后再加:编辑、清空已完成、筛选、排序…
- 函数会越来越多,代码越来越乱
useReducer = 把所有修改状态的逻辑,统一收到一个函数里管理
(1)定义action类型
type TodoAction =
| { type: 'ADD'; payload: string }
| { type: 'DELETE'; payload: number }
| { type: 'TOGGLE'; payload: number }
| { type: 'INIT'; payload: TodoItem[] };
这里是所有操作的方法类型,如果需要添加功能就要在这里声明
type:方法类型
payload:参数
(2)编写reducer函数
将所有修改状态的函数,统一放在reducer函数
// 纯函数:根据不同的 action,返回新的 state
export function todoReducer(state: TodoItem[], action: TodoAction) {
switch (action.type) {
case 'ADD':
return [
...state,
{
id: Date.now(),
content: action.payload,
completed: false,
},
];
case 'DELETE':
return state.filter(item => item.id !== action.payload);
case 'TOGGLE':
return state.map(item =>
item.id === action.payload
? { ...item, completed: !item.completed }
: item
);
case 'INIT':
return action.payload;
default:
return state;
}
}
(3)将useState换成useReducer
- 导入api
import { useReducer } from 'react';
- 替换
useState为useReducer
将这个代码
const [list, setList] = useState<TodoItem[]>(() => {
const saved = localStorage.getItem('todolist');
return saved ? JSON.parse(saved) : [];
});
换成:
// 初始值从 localStorage 读取
const initialState = JSON.parse(localStorage.getItem('todolist') || '[]');
const [list, dispatch] = useReducer(todoReducer, initialState);
(4)替换增删改逻辑
原来:
const addTodo = ...
const deleteTodo = ...
const toggleComplete = ...
替换成:
const addTodo = (content: string) => {
dispatch({ type: 'ADD', payload: content });
};
const deleteTodo = (id: number) => {
dispatch({ type: 'DELETE', payload: id });
};
const toggleComplete = (id: number) => {
dispatch({ type: 'TOGGLE', payload: id });
};
useReducer的作用流程
initialState从本地存储取出useReducer将initialState当成初始的state传入给todoReducer- 根据对应的
dispatch+state一同传入todoReducer,todoReducer处理后返回修改后的state,实现数据的修改
作用:
所有逻辑添加在reducer统一管理
所有修改状态通过dispatch发指令
不直接操作setList
总结
到这里我们的练手项目就完成了基本的功能,如果你完整走了一遍,在实战中理解,那你就掌握了:
- React 组件状态
- 状态提升
- 上下文 Context
- 副作用 useEffect
- 状态统一管理 useReducer
- TypeScript 类型约束
- localStorage 持久化
- 路由管理
实现功能:
- 添加任务
- 删除任务
- 切换完成状态
- 空列表提示
- 刷新不丢失(localStorage)
- 全局状态共享
- useReducer 规范管理
拓展—筛选+CSS Module
如果还想再继续练手,那可以拓展一个筛选的功能(全部/已完成/未完成)
实现筛选的功能,最重要是要先理解:
- 我们需要添加filter存储筛选数据的状态(和state同级)
- 添加setFilter方法修改数据
为什么不是直接使用setFilter修改state之后直接渲染就可以了?
因为filter需要存放筛选后的数据状态(全部/已完成/未完成),在组件当中通过filter进行判断渲染+高亮
如果单纯的修改state,可能会导致状态难以回退比如我筛选了已完成的,但是再点击显示全部,state已经被修改,数据只剩下已完成,无法再显示出全部
因此需要filter专门存储筛选状态
实现
1.修改全局状态
打开src/store/todo/index.tsx
(1)修改store类型定义
// 任务类型
export interface TodoItem {
id: number;
content: string;
completed: boolean;
}
// 筛选类型
export type FilterType = 'all' | 'active' | 'completed';
// 整个 store 的 state 结构
interface TodoState {
list: TodoItem[];
filter: FilterType;
}
(2) TodoContextType添加
interface TodoContextType {
list: TodoItem[];
filter: FilterType //👈 新增
setFilter: (f: FilterType) => void; // 👈 新增
addTodo: (content: string) => void;
deleteTodo: (id: number) => void;
toggleComplete: (id: number) => void;
}
(3) TodoAction添加
| { type: 'SET_FILTER'; payload: FilterType }
(4)在 reducer 里处理 SET_FILTER
case 'SET_FILTER':
return action.payload;
(5)重构reducer
export function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
// 新增
case 'ADD':
return {
...state,
list: [
...state.list,
{
id: Date.now(),
content: action.payload,
completed: false,
},
],
};
// 删除
case 'DELETE':
return {
...state,
list: state.list.filter((item) => item.id !== action.payload),
};
// 切换完成状态
case 'TOGGLE':
return {
...state,
list: state.list.map((item) =>
item.id === action.payload
? { ...item, completed: !item.completed }
: item
),
};
// 初始化
case 'INIT':
return { ...state, list: action.payload };
// 筛选
case 'SET_FILTER':
return { ...state, filter: action.payload };
default:
return state;
}
}
解释
1.当前的state是一个对象,包含list+filter
2.对象不能直接修改,必须返回新对象
3.react规定状态是只读的,因此state.list赋值会报错
4.修改必须复制+覆盖修改部分
return {
...state, // 先复制原来所有内容
list: 新数组 // 再覆盖要改的部分
}
(6)修改初始值
// 初始状态
const initialState: TodoState = {
list: JSON.parse(localStorage.getItem('todolist') || '[]'),
filter: 'all', // 默认显示全部
};
(7)修改useReducer
const [state, dispatch] = useReducer(todoReducer, initialState);
(8)修改TodoProvider方法实现
export const TodoProvider = ({ children }: { children: ReactNode }) => {
//省略...
const list = state.list;
const filter = state.filter;
useEffect(() => {
localStorage.setItem("todolist", JSON.stringify(list));
}, [list]);
//add、delete、toggle方法...
const setFilter = (f: FilterType) => {
dispatch({ type: "SET_FILTER", payload: f });
};
};
(9)将新加方法添加到value
value={{
list,
filter,
setFilter,
addTodo,
deleteTodo,
toggleComplete,
}}
2.修改组件
Todo.tsx
(1)循环渲染修改成filterList,实现筛选展示
//添加解构filter/setFilter
const { list, filter, addTodo, deleteTodo, toggleComplete, setFilter } = todoCtx;
//渲染filterList创建
const filteredList = list.filter(item => {
if (filter === 'active') return !item.completed
if (filter === 'completed') return item.completed
return true
})
//遍历渲染
filteredList.map(item => (...)
(2)添加筛选按钮+高亮
<div style={{ margin: '10px 0' }}>
<button
onClick={() => setFilter('all')}
style={{
marginRight: 8,
backgroundColor: filter === 'all' ? '#1890ff' : '#fff',
color: filter === 'all' ? '#fff' : '#000',
}}
>
全部
</button>
<button
onClick={() => setFilter('active')}
style={{
marginRight: 8,
backgroundColor: filter === 'active' ? '#1890ff' : '#fff',
color: filter === 'active' ? '#fff' : '#000',
}}
>
未完成
</button>
<button
onClick={() => setFilter('completed')}
style={{
backgroundColor: filter === 'completed' ? '#1890ff' : '#fff',
color: filter === 'completed' ? '#fff' : '#000',
}}
>
已完成
</button>
</div>
改进css封装
实际在项目中我们会将style复用封装,因此也可以单独封装成一个样式文件
用 className + css 文件
- 创建 Todo.module.css
.filterBtn {
padding: 4px 12px;
margin-right: 8px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
background: #fff;
}
.filterBtn.active {
background: #1890ff;
color: white;
border-color: #1890ff;
}
- Todo.tsx 引入
import styles from './Todo.module.css';
- 按钮写法
<div style={{ margin: '12px 0' }}>
<button
onClick={() => setFilter('all')}
className={`${styles.filterBtn} ${filter === 'all' ? styles.active : ''}`}
>
全部
</button>
<button
onClick={() => setFilter('active')}
className={`${styles.filterBtn} ${filter === 'active' ? styles.active : ''}`}
>
未完成
</button>
<button
onClick={() => setFilter('completed')}
className={`${styles.filterBtn} ${filter === 'completed' ? styles.active : ''}`}
>
已完成
</button>
</div>
知识点
1.
Todo.module.css→ 自动生成唯一类名2.不会样式冲突
3.动态className—>前面公共样式,后面条件激活样式
问题
我在抽离公共样式,再import导入时,ts无法识别到CSS Module
解决:
1.关闭vscode,重新打开,ts可能就能够识别到,然后重启项目
2.重启还不行,添加src/custom.d.ts
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
作用:
TypeScript 不知道 .module.css 是什么文件
必须告诉它:这是一个模块,导出的是类名字符串
加了custom.d.ts就等于给 TS 一个 “说明文档”
总结plus
到这里就结束啦,如果你坚持到这,我相信对你一定有所帮助,能够清楚了解项目从0到1的搭建和完善,以及添加功能的操作流程。
项目现在拥有的所有功能:
- 新增任务
- 删除任务
- 切换完成 / 未完成
- 筛选:全部 / 未完成 / 已完成
- 筛选按钮高亮(CSS Modules 标准写法)
- localStorage 持久化
- 全局状态管理:Context + useReducer
- TypeScript 完整类型约束
- 样式隔离、不重复、企业级写法
学完能够掌握的知识点
- 状态为什么要不可变更新
- 为什么要用 useReducer
- 为什么 数据和方法要分开
- 为什么 filter 必须存在 state 里
- 企业级 CSS Modules 用法
- TypeScript 类型设计
希望这个文章对你有帮助,也欢迎指正,大家一起加油喔~
更多推荐

所有评论(0)