React 快速入门学习
中文官网:https://zh-hans.react.dev/learn
使用前端开发工具 VSCode 来学习
React 基础知识
安装 React
进入要存放代码的目录,打开cmd,输入一下目录:
npx create-react-app 项目名字
例如:npx create-react-app react-learn-demo
项目目录
使用 VSCode 打开查看具体目录结构:
启动项目
进入 package.json 文件,查看启动项目的命令:
npm run start
打开谷歌浏览器输入:http://localhost:3000/,看到一下界面表示项目启动成功!
核心语法
React 组件的两种创建方式
- 函数组件:官方推荐使用,本文章以这种方式学习
- 类组件:相对写起来比函数组件复杂一些
App.js:函数组件方式
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
App.js:类组件方式
import React, { Component } from'react';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
}
export default App;
function App() {}
定义了一个名为 App
的函数组件。在 React 中,组件是构建用户界面的基本单元
使用 export default App;
将组件导出,以便在其他模块中可以导入和使用这个组件
return 语句
函数组件必须使用 return
语句来返回要渲染的内容,return
后面通常是搭配 "()" 来使用,如果你是单行写完所有的代码,就可以省略这个花括号,否则就要加上这个括号(换行、多行写代码情况下),相对于return
应该表达式返回,()里面的内容就代表一个表达式一样,可以这么理解
返回单行代码:这个只能单行写完所有要返回的代码
function App() {
return <div>hello react!</div>
}
返回多行:
function App() {
return (
<div>hello react!</div>
);
}
export default App;
闭合标签
在 JSX 中,所有的标签都必须正确闭合,例如 <div>
要有对应的 </div>
错误示范:<div>hello react!</div
function App() {
return (
<div>hello react!</div
);
}
export default App;
只能返回一个根元素
正确的写法:
function App() {
return (
<div>hello react!</div>
);
}
export default App;
错误的写法:编译会出错
function App() {
return (
<div>hello react!</div>
<div>hello react!</div>
<div>hello react!</div>
);
}
export default App;
解决不能返回多个根元素的问题
使用jsx
提供的空标签:<></>
function App() {
return (
<>
<div>hello react!</div>
<div>hello react!</div>
<div>hello react!</div>
</>
);
}
export default App;
再包一层div
容器
function App() {
return (
<div>
<div>hello react!</div>
<div>hello react!</div>
<div>hello react!</div>
</div>
);
}
export default App;
ClassName
在 React 中,className
用于为组件或元素添加 CSS 类名(不使用class),以应用相应的样式
function App() {
return <div class="myDiv">myDiv</div>;
}
export default App;
以下是一些关于 className
的要点和示例:
- 基本用法:
- 可以直接将字符串作为
className
的值。
例如:<div className="my-class">...</div>
- 可以直接将字符串作为
- 多个类名:
- 可以通过空格分隔多个类名。
例如:<div className="class1 class2">...</div>
- 可以通过空格分隔多个类名。
- 动态类名:
- 可以根据组件的状态或其他条件来动态设置
className
。
例如:
- 可以根据组件的状态或其他条件来动态设置
const [isActive, setIsActive] = useState(false);
<button className={isActive? 'active' : ''}>按钮</button>
- 与 CSS 模块结合:
- 如果使用 CSS 模块,可以通过导入并使用特定的对象来设置
className
。
例如:
- 如果使用 CSS 模块,可以通过导入并使用特定的对象来设置
import styles from './styles.module.css';
<div className={styles.myDiv}>...</div>
插值语法
大花括号:{ 变量 }
,应用很广泛,很灵活!!!
最简单的实现:
function App() {
const value = 10;
return <div>{value}</div>;
}
export default App;
条件渲染
function App() {
let value = ''
const flag = true
if (flag) {
value = <a href="https://github.com/nanshuo0814">这是一个链接</a>
// 加上单引号变成字符串
// value = '<a href="https://github.com/nanshuo0814">这是一个链接</a>'
} else {
value = <span>这是一个span</span>
}
return (
<div>{ value }</div>
);
}
export default App;
flag:true
如果 value 的值加上了单引号,表示它就是一个单纯是字符串了,而不是渲染标签了
flag:false
列表渲染
function App() {
const list = [
{ id: 1, name: "南烁" },
{ id: 2, name: "小南" },
{ id: 3, name: "小烁" },
];
const listContent = list.map(item =>(
<li key={item.id}>{item.name}</li>
));
return <ul>{listContent}</ul>;
}
export default App;
<li key={item.id}>{item.name}</li>
:其中 key 属性里面也使用了插值语法,就是动态获取 item.idFragment
标签的使用:
由于 React 的返回只能有一个根元素,导致如果列表的每一行返回都加上分隔符返回的话,可能就需要借助它了,需求时下面这样
代码实现:
import { Fragment } from "react";
function App() {
const list = [
{ id: 1, name: "南烁" },
{ id: 2, name: "小南" },
{ id: 3, name: "小烁" },
];
const listContent = list.map((item) => (
<Fragment key={item.id}>
<li key={item.id}>{item.name}</li>
<div>===========</div>
</Fragment>
));
return <ul>{listContent}</ul>;
}
export default App;
为什么不使用 <></> ?
因为需要为子元素添加key
唯一标识
<></>
是 Fragment 的一种简写形式,它的作用是在无需向 DOM 添加额外节点的情况下,对一组子元素进行分组。使用<></>
可以使代码更加简洁,并且避免了不必要的 DOM 嵌套,提高了性能和可维护性- 然而,
<></>
不支持添加key
属性。而key
在 React 中是非常重要的,特别是在处理列表渲染等情况下。key
可以帮助 React 更高效地更新和渲染组件,识别列表中元素的变化,从而正确地进行元素的添加、删除和移动等操作 - 当渲染一个列表时,如果没有为每个列表项提供
key
,React 会发出警告,并且在性能和渲染的准确性上可能会出现问题 - 在这种需要为子元素添加
key
的场景下,就不能使用<></>
,而必须使用完整的<Fragment>
形式并添加key
属性
事件触发
import { Fragment } from "react";
function App() {
const handleClick = () => {
alert('按钮被点击了');
};
return (
<button onClick={handleClick}>按钮</button>
);
}
export default App;
useState 的使用
import { useState } from "react";
function App() {
// 声明一个名为 count 的状态变量,并将其初始值设为 0
// setCount 是用于更新 count 状态的函数
const [count, setCount] = useState(0);
const handleIncrement = () => {
// 通过调用 setCount 函数来更新 count 的值
setCount(count + 1);
console.log("count++");
};
return (
<div>
<p>数量: {count}</p>
<button onClick={handleIncrement}>数量 + 1</button>
</div>
);
}
export default App;
引入问题:由于上面定义的 setCount 是直接替换旧值,对于只有单个属性的修改,可能影响不大,但对于列表、数组数据的修改则会出现数据丢失的问题
实例代码:
import { useState } from "react";
function App() {
const [data, setData] = useState([
{ id: 1, name: "南烁" },
{ id: 2, name: "小南" },
{ id: 3, name: "小烁" },
]);
const listData = data.map(item => (
<li key={item.id}>{item.name}</li>
))
const [id, setId] = useState(4)
const handleClick = () => {
setData([
// 浅拷贝
...data,
{ id: id, name: "小光" + id }
]);
setId(id + 1)
console.log(id)
}
return (
<>
<ul>{listData}</ul>
<button onClick={handleClick}>添加一条数据</button>
</>
);
}
export default App;
正常情况下:
如果没有添加...data
的话:也就是注释掉这行代码,点击添加一条数据就会出现下面的现象,之前的数据都没了,只显示新添加的数据
如果将...data
搞到最后,点击添加一条数据时,就会在头部添加,展开运算符
setData([
{ id: id, name: "小光" + id },
// 浅拷贝
...data,
]);
filter 过滤器
过滤 id == 2 的数据:
import { useState } from "react";
function App() {
const [data, setData] = useState([
{ id: 1, name: "南烁" },
{ id: 2, name: "小南" },
{ id: 3, name: "小烁" },
]);
const listData = data.map((item) => <li key={item.id}>{item.name}</li>);
const handleClick = () => {
setData(data.filter((item) => item.id !== 2));
console.log("已过滤~");
};
return (
<>
<ul>{listData}</ul>
<button onClick={handleClick}>过滤 id 为2的数据</button>
</>
);
}
export default App;
组件通信与插槽
插槽
插槽的使用场景演示,根据下面的案例,可以以此类推出许多可能的场景,例如:父子组件传值等等场景
以 img 标签为例:style 样式里面的字属性名字如果是下划线连接的都变成了驼峰式命名了,如:background-color ==> backgroundColor
import favicon from "./logo.svg";
function App() {
const imgStyle = {
width: 200,
height: 200,
backgroundColor: "red",
};
const imgData = {
className: "App-logo",
style: {
width: 200,
height: 200,
backgroundColor: "green",
},
};
return (
<div>
{/* 第一种实现方式,常规 */}
<img
src={favicon}
alt="logo"
className="App-logo"
style={{
width: 200,
height: 200,
backgroundColor: "grey",
}}
/>
{/* 第二种实现方式,使用插槽 */}
<img src={favicon} alt="logo" className="App-logo" style={imgStyle} />
{/* 第三种实现方式,使用展开运算符+插槽,简化标签的属性,让看起来更加清晰 */}
<img src={favicon} alt="logo" {...imgData} />
</div>
);
}
export default App;
React 组件的Props
// 第一种写法
function Article1(props) {
return (
<div>
<h2>{props.title}</h2>
<p>{props.content}</p>
<p>{props.active ? "真值" : "假值"}</p>
</div>
);
}
// 第二种写法
function Article2({ title, content, active }) {
return (
<div>
<h2>{title}</h2>
<p>{content}</p>
<p>{active ? "真值" : "假值"}</p>
</div>
);
}
function App() {
return (
<>
<Article1
title="React"
content="React 是由 Facebook 推出的一个用于构建用户界面的 JavaScript 库。"
active
/>
<Article2
title="Vue"
content="Vue.js 是一套构建用户界面的渐进式 JavaScript 框架。"
/>
</>
);
}
export default App;
React 组件中展开 Props 使用场景
嵌套式 props,多层嵌套,实际开发业务里会出现这种需求,也是一样的道理使用插槽和展开运算符传递参数,以此类推
function Detail({ title, content, active }) {
return (
<div>
<h1>{title}</h1>
<div>{content}</div>
<div>{active ? "真" : "假"}</div>
</div>
);
}
function Article({ title, content }) {
return (
<div>
<h1>{title}</h1>
<Detail
{...content}
/>
</div>
);
}
function App() {
const data = {
title: "标题",
content: {
title: "Vue",
content: "Vue.js 是一套构建用户界面的渐进式 JavaScript 框架。",
active: true,
},
};
return (
<>
<Article {...data} />
</>
);
}
export default App;
将 JSX 作为 Props 传递(组件插槽)
参数footer
的默认值 + children
(用于接收列表项)
function List({ children, title, footer = <div>默认底部</div> }) {
return (
<>
<h2>{title}</h2>
<ul>{children}</ul>
{footer}
</>
);
}
function App() {
return (
<>
<List title="列表标题1" footer={<div>列表底部1</div>}>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</List>
<List title="列表标题2">
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</List>
</>
);
}
export default App;
子组件向父组件传递
import { useState } from "react";
function Detail({ onActive }) {
const [status, setStatus] = useState(false);
function handleClick() {
setStatus(!status);
onActive(status);
}
return (
<>
<button onClick={handleClick}>按钮</button>
{status && <div>hello world</div>}
</>
);
}
function App() {
function handleActive(status) {
console.log(status);
}
return (
<>
<Detail onActive={handleActive} />
</>
);
}
export default App;
使用 Content 进行多级组件传值
Context 提供了一种在组件树中跨层级共享数据的方式,无需通过中间组件逐层传递 props
在最顶层的组件中(通常是父组件或祖先组件),使用Provider
来包裹子组件,并通过value
属性提供要共享的数据
使用 Content 前:
import { createContext } from "react";
export function Section({ children }) {
return <section className="section">{children}</section>;
}
export function Heading({ level, children }) {
switch (level) {
case 1:
return <h1 className="heading-level-1">{children}</h1>;
case 2:
return <h2 className="heading-level-2">{children}</h2>;
case 3:
return <h3 className="heading-level-3">{children}</h3>;
case 4:
return <h4 className="heading-level-4">{children}</h4>;
case 5:
return <h5 className="heading-level-5">{children}</h5>;
case 6:
return <h6 className="heading-level-6">{children}</h6>;
default:
throw Error("未知的 level: " + level + ", 仅支持 1-6");
}
}
export default function App() {
return (
<div>
<Section>
<Heading level={1}>h1</Heading>
<Heading level={2}>h2</Heading>
<Heading level={3}>h3</Heading>
<Heading level={4}>h4</Heading>
<Heading level={5}>h5</Heading>
<Heading level={6}>h6</Heading>
</Section>
<Section>
<Heading level={1}>h1</Heading>
</Section>
</div>
);
}
使用 Content 后:
import { createContext, useContext } from "react";
export function Section({ children }) {
const level = useContext(LevelContent);
return (
<section className="section">
<LevelContent.Provider value={level + 1}>
{children}
</LevelContent.Provider>
</section>
);
}
export function Heading({ children }) {
const level = useContext(LevelContent);
switch (level) {
case 1:
return <h1 className="heading-level-1">{children}</h1>;
case 2:
return <h2 className="heading-level-2">{children}</h2>;
case 3:
return <h3 className="heading-level-3">{children}</h3>;
case 4:
return <h4 className="heading-level-4">{children}</h4>;
case 5:
return <h5 className="heading-level-5">{children}</h5>;
case 6:
return <h6 className="heading-level-6">{children}</h6>;
default:
throw Error("未知的 level: " + level + ", 仅支持 1-6");
}
}
const LevelContent = createContext(0);
export default function App() {
return (
<div>
<Section>
<Heading>h1</Heading>
<Section>
<Heading>h2</Heading>
<Section>
<Heading>h3</Heading>
<Section>
<Heading>h4</Heading>
<Section>
<Heading>h5</Heading>
<Section>
<Heading>h6</Heading>
</Section>
</Section>
</Section>
</Section>
</Section>
</Section>
<Section>
<Heading>h1</Heading>
</Section>
</div>
);
}
useReducer 用于统一管理状态
跟 useState 一样的功能,不过这个能够处理更加复杂一点的状态,更细的操作
- 使用 useState:
import { useState } from "react";
export default function App() {
// 计数器
const [count, setCount] = useState(0);
const handleIncrease = () => {
setCount(count + 1);
console.log("+1");
};
const handleDecrease = () => {
setCount(count - 1);
console.log("-1");
};
return (
<div style={{ padding: 10 }}>
<button style={{ margin: 10 }} onClick={handleDecrease}>
-
</button>
<span>{count}</span>
<button style={{ margin: 10 }} onClick={handleIncrease}>
+
</button>
</div>
);
}
- 使用 useReducer:
import { useReducer, useState } from "react";
function countReducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
throw new Error();
}
}
export default function App() {
// 计数器
const [state, dispatch] = useReducer(countReducer, 0);
const handleIncrease = () => {
dispatch({ type: "increment" });
console.log("+1");
};
const handleDecrease = () => {
dispatch({ type: "decrement" });
console.log("-1");
};
return (
<div style={{ padding: 10 }}>
<button style={{ margin: 10 }} onClick={handleDecrease}>
-
</button>
<span>{state}</span>
<button style={{ margin: 10 }} onClick={handleIncrease}>
+
</button>
</div>
);
}
- 运行结果
useRef 的使用
import { useRef, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const prevCount = useRef();
function handleClick() {
prevCount.current = count;
setCount(count + 1);
console.log("count+1");
}
return (
<div>
<p>最新的 count: {count}</p>
<p>上次的 count: {prevCount.current}</p>
<button onClick={handleClick}>增大count</button>
</div>
);
}
ref useImperativeHandle 的使用
ref 用于引用 DOM 元素或组件实例
- 访问 DOM 元素:可以对 DOM 进行直接操作,例如获取元素的尺寸、位置等属性,或者调用 DOM 方法。
- 操作子组件实例:与子组件进行交互,调用子组件的方法。
使用方式
ref
可以通过字符串形式创建,但这种方式已不被推荐。- 更常用的是使用回调函数形式,例如:
<div ref={(node) => this.myDiv = node}>
useImperativeHandle
是一个 React Hook,它允许你自定义父组件通过 ref
获取子组件实例的公开方法。通过使用 useImperativeHandle
,可以选择性地暴露子组件的特定属性或方法给父组件
import { forwardRef, useImperativeHandle, useRef, useState } from "react";
const Child = forwardRef(function (props, ref) {
useImperativeHandle(ref, () => ({
myFn: () => {
console.log('子组件的myFn方法');
}
}));
return (
<div>子组件</div>
);
})
export default function App() {
const childRef = useRef()
function handleClick() {
childRef.current.myFn()
}
return (
<div>
<Child ref={childRef} />
<button onClick={handleClick}>按钮</button>
</div>
);
}
useEffect 的使用
useEffect
是 React 中的一个钩子函数,用于在函数组件中处理副作用操作。
副作用操作包括但不限于数据获取、订阅事件、手动修改 DOM 等操作,这些操作不应该在组件的渲染过程中直接进行。useEffect
接受两个参数:
- 第一个参数是一个回调函数,用于执行副作用操作。
- 第二个参数是一个数组,用于控制副作用函数的执行时机。
如果第二个参数数组为空 []
,则副作用函数仅在组件挂载和卸载时执行。
如果数组中有值,只有当这些值发生变化时,副作用函数才会重新执行
import { useEffect, useReducer, useState } from "react";
function countReducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
throw new Error();
}
}
export default function App() {
// 计数器
const [state, dispatch] = useReducer(countReducer, 0);
const handleIncrease = () => {
dispatch({ type: "increment" });
};
const handleDecrease = () => {
dispatch({ type: "decrement" });
};
// 监听state的变化
useEffect(() => {
console.log("useEffect");
},[state]);
return (
<div style={{ padding: 10 }}>
<button style={{ margin: 10 }} onClick={handleDecrease}>
-
</button>
<span>{state}</span>
<button style={{ margin: 10 }} onClick={handleIncrease}>
+
</button>
</div>
);
}
如果第二个参数为空 [] :
useEffect(() => {
console.log("useEffect");
}, []);
useMemo 的使用
React 中的一个钩子函数,用于对计算结果进行缓存和优化
它接受两个参数:
- 第一个参数是一个函数,用于执行计算操作并返回结果。
- 第二个参数是一个数组,用于指定依赖项。只有当依赖项中的值发生变化时,才会重新计算结果。
import React, { useState, useMemo } from'react';
export default function App() {
const [num1, setNum1] = useState(5);
const [num2, setNum2] = useState(10);
const product = useMemo(() => {
console.log('Calculating product...');
return num1 * num2;
}, [num1, num2]); // 当 num1 或 num2 变化时重新计算乘积
return (
<div>
<p>Num1: {num1}</p>
<p>Num2: {num2}</p>
<p>Product: {product}</p>
<button onClick={() => setNum1(num1 + 1)}>Increase Num1</button>
<button onClick={() => setNum2(num2 + 1)}>Increase Num2</button>
</div>
);
}
useCallback 的使用
useCallback
是 React 中的一个钩子函数,用于缓存函数定义,以避免在每次组件重新渲染时都创建新的函数实例。
它接受两个参数:
- 要缓存的回调函数
fn
:这是你希望在重新渲染之间缓存的函数。它可以接受任何参数并返回任何值。在初始渲染时,useCallback
会返回这个函数。在后续渲染中,如果依赖项没有改变,它将返回之前缓存的函数;如果依赖项发生变化,则返回新传入的函数并进行缓存。 - 依赖项数组
dependencies
:这是一个数组,其中包含了在回调函数中使用到的所有响应式的值(如 props、state 以及在组件内部直接声明的变量和函数等)。如果依赖项中的任何一个值发生变化,useCallback
将会返回一个新的函数。
使用 useCallback
的主要目的是优化性能,特别是在将函数作为 props 传递给子组件时。如果不使用 useCallback
缓存函数,每次父组件重新渲染时,子组件接收到的函数都是一个新的实例,即使函数的内容没有变化,也可能导致子组件不必要的重新渲染。通过缓存函数,可以确保在依赖项不变的情况下,子组件接收到的是同一个函数引用,从而避免不必要的渲染
import React, { useState, useCallback } from'react';
export default function App() {
const [count, setCount] = useState(0);
// 使用 useCallback 缓存 increment 函数
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
React TodoList 案例实现
接着巩固昨天的 React 知识学习,做一个 TodoList 清单案例实现
https://www.bilibili.com/video/BV1WC4y1B7Uq/?p=4&spm_id_from=pageDriver
中文 React 文档官网:https://zh-hans.react.dev/learn
安装 React
npx create-next-app@latest
项目结构
使用 VSCode 打开项目查看
启动项目
执行命令:
npm run dev
打开浏览器输入:http://localhost:3000/,看到下面的页面表示启动成功
排除其他的依赖干扰
注释掉全局的 css 样式代码文件 global.css
,或者直接在layout.tsx
注释掉导入global.css
的代码即可
import type { Metadata } from "next";
import { Inter } from "next/font/google";
// 注释掉全局css代码
// import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
入口文件 page.tsx
在这个文件作为程序的入口
先清空原有的代码,进行开发 TodoList 清单项目
// 导入React相关的hooks和组件
"use client";
import AddTodo from "./component/AddTodo";
import TodoList from "./component/TodoList";
import TodoFilter from "./component/TodoFilter";
import { useCallback, useMemo, useState } from "react";
import { Todo } from "./types";
// 定义Home组件
export default function Home() {
// 初始化todos状态和设置todos状态的方法,todos是一个Todo类型的数组
const [todos, setTodos] = useState<Todo[]>([]);
// 初始化filter状态和设置filter状态的方法,filter是一个字符串,表示当前的过滤状态
const [filter, setFilter] = useState("all");
// 使用useCallback hook定义一个添加todo的方法,确保在todos状态变化时,该方法不会被重新创建
const addTodo = useCallback((text: string) => {
const newTodo = {
id: Math.floor(Math.random() * 100), // 生成一个0-99之间的随机整数作为id
title: text,
completed: false,
};
setTodos([...todos, newTodo]); // 将新的todo添加到todos数组的末尾
}, [todos]);
// 定义一个删除todo的方法
const deleteTodo = (id: number) => {
setTodos(todos.filter((todo) => todo.id !== id)); // 过滤掉指定id的todo
};
// 定义一个切换todo完成状态的方法
const toggleTodo = (id: number) => {
setTodos(
todos.map((todo) => {
if (id === todo.id) {
return { ...todo, completed: !todo.completed }; // 如果todo的id匹配,则切换其完成状态
}
return todo; // 如果todo的id不匹配,则返回原todo
})
);
};
// 使用useMemo hook定义一个根据filter状态过滤todos的方法
// 当todos或filter状态变化时,该方法会重新计算并返回一个新的todos数组
const filterTodos = useMemo(() => {
switch (filter) {
case "all":
return todos; // 返回所有todos
case "active":
return todos.filter((todo) => !todo.completed); // 返回未完成的todos
case "completed":
return todos.filter((todo) => todo.completed); // 返回已完成的todos
default:
throw new Error("Unknown filter"); // 如果filter状态不是"all"、"active"或"completed",则抛出一个错误
}
}, [todos, filter]);
// 返回JSX,定义组件的UI结构
return (
<div style={{ width: "360px" }}>
<h1>TodoList</h1>
{/* 将addTodo方法传递给AddTodo组件 */}
<AddTodo addTodo={addTodo}></AddTodo>
{/* 将getFilterTodos、deleteTodo和toggleTodo方法传递给TodoList组件 */}
<TodoList todos={filterTodos} deleteTodo={deleteTodo} toggleTodo={toggleTodo}></TodoList>
{/* 将setFilter方法传递给TodoFilter组件 */}
<TodoFilter setFilter={setFilter}></TodoFilter>
</div>
);
}
新建 types.ts
接口定义:代办事件 Todo
- id:唯一标识
- title:代办事件名称
- completed:是否完成代办事件
export interface Todo {
id: number;
title: string;
completed: boolean;
}
新建 component 文件夹
存放用到的各个组件:顾名思义
- AddTodo:添加代办事件
- TodoList:代办事件列表
- TodoItem:代办事件各项
- TodoFilter:查询过滤全部、代办、已办事件
AddTodo
import { useEffect, useState } from "react";
interface AddTodoProps {
addTodo: (text: string) => void;
}
export default function AddTodo({ addTodo }: AddTodoProps) {
const [text, setText] = useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (text.trim() !== "") {
addTodo(text);
setText("");
} else {
alert("代办内容不能为空");
}
};
const inputStyle = {
width: 250,
height: 23,
borderRadius: 5,
};
const buttonStyle = {
height: 30,
backgroundColor: "green",
color: "#fff",
marginLeft: "5px",
cursor: "pointer",
borderRadius: 5,
};
// 监听文本框内容变化
useEffect(() => {
console.log(text);
}, [text]);
return (
<form onSubmit={handleSubmit}>
<input
style={inputStyle}
type="text"
placeholder="输入代办事件"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button style={buttonStyle}>添加代办事件</button>
</form>
);
}
TodoList
import { Todo } from "../types";
import TodoItem from "./TodoItem";
interface TodoListProps {
todos: Array<Todo>;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
}
export default function TodoList({
todos,
toggleTodo,
deleteTodo,
}: TodoListProps) {
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
></TodoItem>
))}
</div>
);
}
TodoItem
export default function TodoItem({ todo, toggleTodo, deleteTodo }: any) {
return (
<div
style={{
textDecoration: todo.completed ? "line-through" : "none",
marginTop: "10px",
}}
>
{todo.title}
<span style={{ float: "right" }}>
<button
style={{ marginRight: "10px", cursor: "pointer" }}
onClick={() => toggleTodo(todo.id)}
>
切换
</button>
<button
style={{ marginRight: "10px", cursor: "pointer" }}
onClick={() => deleteTodo(todo.id)}
>
删除
</button>
</span>
</div>
);
}
TodoFilter
export default function TodoFilter({ setFilter }: any) {
return (
<div style={{marginTop: '10px'}}>
<button style={{ marginRight: '10px',cursor: 'pointer' }} onClick={() => setFilter("all")}>全部</button>
<button style={{ marginRight: '10px',cursor: 'pointer' }} onClick={() => setFilter("active")}>代办</button>
<button style={{ cursor: 'pointer' }} onClick={() => setFilter("completed")}>已办</button>
</div>
);
}