本文是面向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)}

此时,页面组件不再有:

  • 自己的 list useState
  • 自己的 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 文件

  1. 创建 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;
}
  1. Todo.tsx 引入
import styles from './Todo.module.css';
  1. 按钮写法
<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的搭建和完善,以及添加功能的操作流程。

项目现在拥有的所有功能:

  1. 新增任务
  2. 删除任务
  3. 切换完成 / 未完成
  4. 筛选:全部 / 未完成 / 已完成
  5. 筛选按钮高亮(CSS Modules 标准写法)
  6. localStorage 持久化
  7. 全局状态管理:Context + useReducer
  8. TypeScript 完整类型约束
  9. 样式隔离、不重复、企业级写法

学完能够掌握的知识点

  • 状态为什么要不可变更新
  • 为什么要用 useReducer
  • 为什么 数据和方法要分开
  • 为什么 filter 必须存在 state 里
  • 企业级 CSS Modules 用法
  • TypeScript 类型设计

希望这个文章对你有帮助,也欢迎指正,大家一起加油喔~

更多推荐