React 核心语法
目前最流行的现代前端框架 无疑是vue.js和react.js,国内的大部分项目都是vue.js,并且vue3 和react很想象,学习果vue3的人看会觉得相当熟悉,并且在开发跨平台ios或者安卓端的时候 reactnative 也是个不错的选择(原生安卓 ui匮乏,flutter 需要额外掌握一门新语言,uts目前并不完善,开发体验还不成熟)
视频教程参考
某马程序员(视频很完善但是不推荐 学会vue3 看个文档很快就能上手) https://www.bilibili.com/video/BV1ZB4y1Z7o8/?spm_id_from=333.337.search-card.all.click
30分钟速成核心语法 https://www.bilibili.com/video/BV1pF411m7wV/?spm_id_from=333.337.search-card.all.click&vd_source=c4237dd4bb302a93bf1fce783ce1f8cb
官网 https://zh-hans.react.dev/learn (建议快速核心语法过一遍 直接看官网 自己上手 看某马和某谷会浪费很多时间)
生态圈
- 路由中文文档 (对于目前主流的单页面应用路由肯定是构建大型web需要的) https://react-guide.github.io/react-router-cn/
个人笔记:react router - 全局状态管理库 (和vue 的pinia类似) https://cn.redux.js.org/ 3分钟学会redux
- reactNative (我学react 就是为了这个!!!) 跨平台 性能做过优化的 比一般的跨平台好点 稍稍弱于flutter https://reactnative.cn/
- Zustand 一个轻量级的 React 状态管理库,可以用来管理应用中的全局状态。它的目标是提供一种简单、快速且灵活的状态管理解决方案,尤其是在一些不需要 Redux 这种重量级解决方案的场景中。
一些好看的ui
Material-UI:当下流行的 React UI 框架 (mui.com)
Arco Design - 企业级产品的完整设计和开发解决方案
开始
npx create-next-app@latest
使用Vite创建项目
npx create-vite
idea编译器有 可以下载对应插件
核心语法TSX(JSX)
TSX(JSX)是ts和js 和html的混用
{} 对应vue的{{}} 插值语法
可以渲染任意东西,下面给出结构代码实列直观发现组件结构
组件
就是一个大写方法明的方法 然后返回TSX
import {useEffect, useState} from "react";
function HookEffect() {
const style={
border: "1px solid #ccc",
borderRadius: "5px",
boxShadow: "0 0 10px rgba(0,0,0,0.3)",
padding: "20px",
transform: "translate(-50%, -50%)",
color: "red",
margin: "20px 20px",
fontSize: "30px"
}
const [message, set] = useState("演示effect")
const [count, setCount] = useState(0)
const [isShow, setIsShow] = useState(false)
useEffect(() => {
set("特定监听的数据让useEffect渲染完成")
},[count])
return (
<>
<div style={style}>HookEffect
<hr/>
<span>渲染内容</span>
<p>{message}</p>
<p>计数器:{count}</p>
<button onClick={() => setCount(count+1)}>计数器+1</button>
{isShow && <button onClick={() => setIsShow(false)}>卸载时钟</button>}
{!isShow && <button onClick={() => setIsShow(true)}>挂载时钟</button>}
</div>
<div>
{isShow && <Clock/>}
</div>
</>
)
}
const Clock = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date().toLocaleTimeString());
console.log(new Date().toLocaleTimeString())
}, 1000);
// 清理函数,组件卸载时清除计时器
return () => {
clearInterval(timer);
};
}, []); // 依赖项为空,意味着只在组件挂载时执行
return (
<div>
<h2>当前时间: {time}</h2>
</div>
);
};
export default HookEffect;
jsx 语法和java函数式编程一样 返回一行省略() 多行则需要
函数组件
一个大学函数名的函数为一个组件,函数的返回值为要渲染的内容
export default function Home() {
return (
<div>hello world</div>
)
}
如果返回的内容只有一行,跟java的lambda 可以省略
export default function Home() {
return <div>hello world</div>
}
如果返回的内容有多行,括号不能省略
export default function Home() {
return (
<div>
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
</div>
)
}
TXS只能返回一个根元素
注意和vue中的template一样只有一个根元素
如果出现多个根元素 需要使用 标签包裹 简写<>
export default function Home() {
return (
<>
<div>hello world</div>
<div>hello world</div>
<div>hello world</div>
</>
)
}
插值语法
和vue差不多
标签内容
export default function App() {
const msg="世界和平"
return (
<>
{/* 使用在标签内部 */}
<div>{msg}</div>
</>
)
}
标签属性
除去数据,样式,tsx ,方法都可以这样渲染
import React from 'react';
const DemoComponent = () => {
// 定义一些示例数据
const name = 'Alice';
const age = 25;
const isStudent = false;
const items = ['Apple', 'Banana', 'Cherry'];
const user = {
name: 'Bob',
age: 30,
occupation: 'Engineer',
};
// 函数返回字符串
const greet = () => 'Hello, World!';
return (
<div>
<h1>React JSX Rendering Demo</h1>
{/* 渲染字符串 */}
<p>Name: {name}</p>
{/* 渲染数值 */}
<p>Age: {age}</p>
{/* 渲染布尔值(显示不同的文本) */}
<p>Status: {isStudent ? 'Student' : 'Not a Student'}</p>
{/* 渲染数组 */}
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{/* 渲染对象的属性 */}
<p>User Info:</p>
<ul>
<li>Name: {user.name}</li>
<li>Age: {user.age}</li>
<li>Occupation: {user.occupation}</li>
</ul>
{/* 渲染函数的返回值 有返回值的函数都可以渲染 包括返回tsx 和各个元素 */}
<p>{greet()}</p>
</div>
);
};
export default DemoComponent;
条件渲染
采用 &&与运算 实现 v-if
采用三元运算 x? case1:case2 实现 if else
案列也演示了样式的渲染方式
- {}插值行内代码块
- 设为变量 再插值
- 单独写一个css文件 为元素写classname 属性作为类名
const bluecolor={display:"flex",justifyContent:"space-between",alignItems:'center' ,backgroundColor:"blue"}
/**
* 函数组件大写开头
* @constructor
*/
function Chose() {
const [isBlue, setColor] = useState(false)
return(
<div >
<h1>你喜欢蓝色吗</h1>
<div style={{display:"flex",justifyContent:"space-between",alignItems:'center'}}>
<button onClick={()=>setColor(true)}>是</button>
<button onClick={()=>setColor(false)}>否</button>
</div>
{/*类似v-if的用法,但是可以用jsx的语法来实现 如果是v-show则是通过改变style的display来实现*/}
{isBlue && ( //为true时才渲染
<div style={bluecolor}>
这是一个蓝色的内容
</div>
)}
{/*等效三元运算符*/}
{isBlue ? (
<div style={{display:"flex",justifyContent:"space-between",alignItems:'center' ,backgroundColor:"skyblue"}}>
这是一个蓝色的内容
</div>
) : (<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: 'center',
backgroundColor: "red"
}}>
这是一个红色的内容
</div>)}
</div>
)
}
列表渲染
先写的todolist再回过头来看基本语法,map在react中用的应该是非常多
列表中的key用于保证元素的唯一性,类似vue的v-for中的key
type Student = {
name: string,
age: number
}
const students:Student[]=[{name:"John",age:25},{name:"Mary",age:23},{name:"Bob",age:24}]
function Index() {
const ref = useRef(null);
const [array, setStudents] = useState(students)
const [newName, setName] = useState("")
const [newAge, setAge] = useState(0)
return (
<div className={"container"}>
<h1>遍历多个元素</h1>
{/*只要有返回值无论是样式渲染的对象还是函数都可以直接渲染*/}
{array.map((item, index) =>
<div key={index}>{item.name} - {item.age} </div>
)}
<h2>添加元素</h2>
<input type="text" placeholder="请输入姓名" ref={ref} value={newName} onChange={(e) => setName(e.target.value)}/>
<input type="number" placeholder="请输入年龄" value={newAge}
onChange={(e) => setAge(Number(e.target.value))}/>
<button onClick={() => {
const newStudent = {name: newName, age: newAge};
// array.push(newStudent); // 直接修改数组
ref.current.focus(); // 聚焦到输入框
setStudents([...array, newStudent]); // 使用扩展运算符创建新数组
setName(""); // 清空输入框
setAge(0); // 清空输入框
}}>添加元素
</button>
</div>)
}
补充js的数组可以使用类似java的lambda 表达式的一些方法,filter ,map,foreach等,由于{}需要返回值 这里用的是map
事件处理
标签的属性通常使用驼峰
点击事件:onClick ,输入改变事件 onChange…等等等
jsx 语法和java函数式编程一样 一行代码省略{} 多行则需要
这里演示需要借助一个的响应式效果 ,react是通过管理状态来实现响应式的
useState API实现 下面是详细介绍
https://zh-hans.react.dev/learn/state-a-components-memory#adding-a-state-variable
const [value,setValue]=useState()
value 变量名 setvalue()可以实现动态改变数值的方法 从而实现数据的响应式 (直接修改这个数值没有意义,必须调用api)
大部分的事件
function EventTestComponent() {
const [inputValue, setInputValue] = useState('');
const [message, setMessage] = useState('');
const handleClick = (event) => {
setMessage(`点击事件: ${event.type}`);
};
const handleDoubleClick = (event) => {
setMessage(`双击事件: ${event.type}`);
};
const handleMouseEnter = (event) => {
setMessage(`鼠标进入事件: ${event.type}`);
};
const handleMouseLeave = (event) => {
setMessage(`鼠标离开事件: ${event.type}`);
};
/**
* 可以搭配
* 鼠标悬停事件在 React 中可以通过组合使用 onMouseEnter 和 onMouseLeave 来实现。
* onMouseEnter 事件在光标进入元素时触发,而 onMouseLeave 事件在光标离开元素时触发。通过这两个事件,您可以实现鼠标悬停效果。
* @param event
*/
const handleMouseMove = (event) => {
setMessage(`鼠标移动事件: ${event.type}`);
};
const handleMouseDown = (event) => {
setMessage(`鼠标按下事件: ${event.type}`);
};
const handleMouseUp = (event) => {
setMessage(`鼠标释放事件: ${event.type}`);
};
const handleKeyDown = (event) => {
setMessage(`按键按下事件: ${event.type}, 键: ${event.key}`);
};
const handleKeyPress = (event) => {
setMessage(`按键按下事件: ${event.type}, 键: ${event.key}`);
};
const handleKeyUp = (event) => {
setMessage(`按键释放事件: ${event.type}, 键: ${event.key}`);
};
const handleChange = (event) => {
setInputValue(event.target.value);
setMessage(`输入改变事件: ${event.type}, 值: ${event.target.value}`);
};
const handleInput = (event) => {
setMessage(`输入事件: ${event.type}, 值: ${event.target.value}`);
};
const handleSubmit = (event) => {
event.preventDefault();
setMessage(`表单提交事件: ${event.type}`);
};
const handleFocus = (event) => {
setMessage(`获得焦点事件: ${event.type}`);
};
const handleBlur = (event) => {
setMessage(`失去焦点事件: ${event.type}`);
};
const handleTouchStart = (event) => {
setMessage(`触摸开始事件: ${event.type}`);
};
const handleTouchMove = (event) => {
setMessage(`触摸移动事件: ${event.type}`);
};
const handleTouchEnd = (event) => {
setMessage(`触摸结束事件: ${event.type}`);
};
const handleCopy = (event) => {
setMessage(`复制事件: ${event.type}`);
};
const handleCut = (event) => {
setMessage(`剪切事件: ${event.type}`);
};
const handlePaste = (event) => {
setMessage(`粘贴事件: ${event.type}`);
};
return (
<div>
<h1>事件测试组件</h1>
<div
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ padding: '20px', border: '1px solid black', margin: '10px' }}
>
<p>尝试点击、双击、移动鼠标或触摸此区域</p>
</div>
<input
type="text"
value={inputValue}
onChange={handleChange}
onInput={handleInput}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress}
onKeyUp={handleKeyUp}
onFocus={handleFocus}
onBlur={handleBlur}
onCopy={handleCopy}
onCut={handleCut}
onPaste={handlePaste}
style={{ margin: '10px' }}
/>
<form onSubmit={handleSubmit} style={{ margin: '10px' }}>
<button type="submit">提交表单</button>
</form>
<p>事件信息: {message}</p>
</div>
);
}
传值说明
onKeyDown={handleKeyDown} //默认传递事件对象
onClick={()=>onclick(textname)//传递自定义参数
onClick={(e)=>onclick(textname,e)//即传递参数 又传递事件对象
props(父传子)
和vue一样,props 实现父组件传递属性给子组件 可以是传递任意值
父组件在引用的子组件上直接传递数据 <子组件 数据名 ={传递数据}> ,子组件函数的参数添加一个对应的参数props对象就行了 还是很简单的和vue 的组合式api 还是很想
这里用的ts演示 如果是js的话就很简单,无需写类型
细节: props和vue的也是一样的,props只可读 不能修改,如果需要响应式 需要useSate 进行初始化
/**
* 演示props的使用
* 父组件向子组件传递数据
* 只能读取无法修改 需要修改则需要useState
*/
function Parent() {
return (
<div>
<h1>父组件</h1>
<Child name="小明" age={20} />
<Child2 name="小明" age={20}
techs={['TypeScript', 'React']}
space={<span> 父组件传递了 JSX 元素 </span>}
data={{ name: '小明', age: 20 } }
click={() => console.log('点击了子组件')}
/>
<Child3 >
<div
style={{backgroundColor: 'yellow', padding: '10px', border: '1px solid #ccc'
}}
>
<p>子组件3</p>
<p>姓名:小明</p>
<p>年龄:20</p>
<p>这是直接传递的 代码块</p>
</div>
</Child3>
</div>
);
}
function Child(props: { name: string; age: number }) {
return (
<div>
<h2>子组件</h2>
<p>姓名:{props.name}</p>
<p>年龄:{props.age}</p>
</div>
)
;
}
// 子组件3 实现类似插槽
function Child3(porps: {children: JSX.Element}) {
return (
<div>
<h2>子组件3</h2>
{/*插入的代码块*/}
{porps.children}
</div>
)
}
function Child2(props:
{ name: string; age:
number ; techs: string[];
space: JSX.Element;
data: { name: string; age: number }
click: () => void
}) {
return (
<div>
<h2>子组件2</h2>
<p>姓名:{props.name}</p>
<p>年龄:{props.age}</p>
<Child4 name={props.name} age={props.age}></Child4>
<p>技术栈:{props.techs.join(', ')}</p>
<p> JSX 元素:{props.space}</p>
<p> 对象数据:{JSON.stringify(props.data)}</p>
<button onClick={props.click}> 点击子组件 </button>
</div>
);
}
//这样就实现了 父组件->子组件 ->孙组件
function Child4(props: { name: string; age: number }) {
return(
<>
<h3>孙子组件</h3>
<p>姓名:{props.name}</p>
<p>年龄:{props.age}</p>
</>)
}
export default Parent;
插槽(slot)
react的插槽 也就是这样实现了
function Parent() {
return (
<div>
<h1>父组件</h1>
<Child3 >
<div
style={{backgroundColor: 'yellow', padding: '10px', border: '1px solid #ccc'
}}
>
<p>子组件3</p>
<p>姓名:小明</p>
<p>年龄:20</p>
<p>这是直接传递的 代码块</p>
</div>
</Child3>
</div>
);
}
// 子组件3 实现类似插槽
function Child3(porps: {children: JSX.Element}) {
return (
<div>
<h2>子组件3</h2>
{/*插入的代码块*/}
{porps.children}
</div>
)
}
插入多段代码块
和vue 一样可以使用名字来进行区分
import React from 'react';
// 父组件
function Parent() {
return (
<div>
<h1>父组件</h1>
<Child3
header={
<div style={{ backgroundColor: 'lightblue', padding: '10px' }}>
<h3>这是头部插槽</h3>
</div>
}
footer={
<div style={{ backgroundColor: 'lightgreen', padding: '10px' }}>
<p>这是底部插槽</p>
</div>
}
>
{/* 默认插槽内容 */}
<div
style={{
backgroundColor: 'yellow',
padding: '10px',
border: '1px solid #ccc',
}}
>
<p>子组件内容</p>
<p>姓名:小明</p>
<p>年龄:20</p>
</div>
</Child3>
</div>
);
}
// 子组件3 实现具名插槽
function Child3({ header, footer, children }) {
return (
<div>
<h2>子组件3</h2>
{/* 具名插槽:头部 */}
<div>{header}</div>
{/* 默认插槽 没有名 */}
<div>{children}</div>
{/* 具名插槽:底部 */}
<div>{footer}</div>
</div>
);
}
export default Parent;
为了方便记忆 对比vue
<template>
<div>
<h1>父组件</h1>
<Child>
<!-- 默认插槽 -->
<template v-slot:default>
<p>默认内容</p>
</template>
<!-- 具名插槽 -->
<template v-slot:header>
<p>头部插槽</p>
</template>
<template v-slot:footer>
<p>底部插槽</p>
</template>
</Child>
</div>
</template>
<script>
export default {
components: {
Child: {
template: `
<div>
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
`,
},
},
};
</script>
自定义事件(子传父)
和vue 也是通过类似 自定义事件的方式 子组件创建事件,父组件实现,这样就可以子组件参数传递给父组件( 其实有点rpc的思想,rpc 服务定义接口 具体服务交给后端实现)
function EmitDemo() {
const [message , setMessage] = useState("")
// 首先父组件定义方法和实现
const handleClick = (message:string) => {
setMessage(message)
}
return (
<div style={{padding: "20px",
boxShadow: "0 0 10px rgba(0,0,0,0.1)",
border: "1px solid #ccc",
borderRadius: "5px",
}}>
<h1>Emit Demo</h1>
<span>来自子组件的消息父组件显示</span>
<br/>
<p>{message}</p>
<Demoson click={handleClick} ></Demoson>
</div>
)
}
function Demoson(props:{
click:(msg:string) => void,
})
{
const [inputValue, setinput] = useState("")
function changeinput(e) {
setinput(e.currentTarget.value);
}
return (
<div>
<h2>子组件</h2>
<span>
<input type="text" onChange={changeinput} value={inputValue}></input>
</span>
<button onClick={() => props.click(inputValue)}>点击出传递给父组件显示</button>
</div>
)
}
此时可以实现一个子组件传递给兄弟组件的逻辑了 ,子1 ->父组件->子2 ,就是先兄弟之间的传递
function EmitDemo() {
const [message , setMessage] = useState("")
// 首先父组件定义方法和实现
const handleClick = (message:string) => {
setMessage(message)
}
return (
<div style={{padding: "20px",
boxShadow: "0 0 10px rgba(0,0,0,0.1)",
border: "1px solid #ccc",
borderRadius: "5px",
}}>
<h1>Emit Demo</h1>
<span>来自子组件的消息父组件显示</span>
<br/>
<p>{message}</p>
<Demoson click={handleClick} ></Demoson>
{/*传递给兄弟组件*/}
<Brother message={message}></Brother>
</div>
)
}
function Demoson(props:{
click:(msg:string) => void,
})
{
const [inputValue, setinput] = useState("")
function changeinput(e) {
setinput(e.currentTarget.value);
}
return (
<div>
<h2>子组件</h2>
<span>
<input type="text" onChange={changeinput} value={inputValue}></input>
</span>
<button onClick={() => props.click(inputValue)}>点击出传递给父组件显示</button>
</div>
)
}
/**
* 子组件
* 演示兄弟组件的使用
* 子组件传值给了父组件 父组件传值个另一个子组件
* @constructor
*/
function Brother(props:{
message:string
}) {
return (<div style={{padding: "30px 20px",boxShadow: "0 0 10px rgba(0,0,0,0.1)",border: "1px solid blue",borderRadius: "5px"}}>
<h2>兄弟组件</h2>
<br/>
<p>{props.message}</p>
</div>)
}
上下文 (多层级传参)
虽然刚才实现了兄弟组件传值 ,但是那样太麻烦了 数据经历了 俩次传递,所以上下文对象就诞生了,和后端一些web框架(go:gin,java :spring)一样,一个请求的处理是有专门的链式处理的,在一开始设置上下文,全链的处理都可以拿到这个数据
类似vue 的provider 和imject
-
创建上下文对象
-
<上下文对象.Provider value={value}> 标签包裹 需要上下文环境的子对象
-
子组件 孙. 已经更下层级的组件都可以使用 const value = useContext(MyContext); // 使用 useContext Hook 来获取上下文值
// 主应用组件用来显示
function Container() {
return (
<>
{/*范围内的统计对象可以使用 A设置上下文 包裹bc*/}
<ComponentA>
<ComponentB />
<ComponentC></ComponentC>
</ComponentA>
</>
);
}
/**
* Context 是一个全局的共享对象,可以让组件之间共享数据,而不必显式地通过 props 传递。
* 它可以使得 props 在组件树中的任何位置都可以访问到,并且可以避免层层传递 props。
* 类似vue的provider 和inject
* 1.创建一个 Context 对象,并通过 createContext 函数创建。
* 2.使用 Provider 组件包装组件,并提供 Context 值。
* 3.使用 useContext Hook 来获取上下文值。
*/
// 1.创建一个 Context
const MyContext = createContext({
text: "",
number: 0,
component: <div>没有赋值成功</div>,
func: ()=>console.log("没有赋值成功"),
});
// 包装组件,提供 Context 值
function ComponentA(props:{ children: JSX.Element }) {
const value = {
text: "这是上下文包装对象!",
number: 42,
component: <div>This is a component passed through Context.</div>,
func: () => alert("上下文传递的函数!")
};
return (
//2.使用 Provider 组件包装组件,并提供 Context 值 并且将需要使用到上下文的对象插入插槽中
<MyContext.Provider value={value}>
{props.children}
</MyContext.Provider>
);
}
// 消费组件,使用上下文值
function ComponentB() {
const value = useContext(MyContext); // 使用 useContext Hook 来获取上下文值
return (
<div>
<p>{value.text}</p>
<p>{value.number}</p>
{value.component}
<button onClick={value.func}>Click me</button>
{/*孙级的对象也可以使用*/}
<ComponentC></ComponentC>
</div>
);
}function ComponentC() {
const value = useContext(MyContext); // 使用 useContext Hook 来获取上下文值
return (
<div>
<p>{value.text}</p>
<p>{value.number}</p>
{value.component}
<button onClick={value.func}>Click me</button>
</div>
);
}
获取dom元素
这个完全和vue一模一样
1.使用useref() api
2.在想获取的组件或者标签使用ref=api返回的对象,就可以获取
3.然后再函数内使用ref.current 获取当前dom 元素
/**
* 演示获取虚拟对象
* 和vue3的类似 使用useRef获取虚拟对象
* dom可用时 ref.current 指向真实dom对象
* dom不可用时 ref.current 指向undefined
*/
function Demo() {
const ref = useRef(null)
const [value, setValue] = useState("")
return (
<div>
<h1>Hello World</h1>
<input type="text" ref={ref} value={value} onKeyDown={()=>console.log("keydown")} onChange={e => setValue(e.target.value)}/>
<button onClick={() => {
if (ref.current) {
// 显示输入框的值
alert(ref.current.value);
// 输出后继续剧情聚焦避免用户还要点击输入框才能技术输入
ref.current.focus();
alert("输入数据"+value);
} else {
alert('DOM 元素未找到');
}
}}>获取DOM</button>
</div>
);
}
export default Demo;
自定义hook
自定义 Hook 是 React 中非常强大的一个特性,它允许你将组件逻辑抽取到一个独立的函数中,从而在多个组件中复用这部分逻辑。自定义 Hook 和普通的函数非常类似,唯一的区别是它的名称必须以
use
开头,并且可以调用其他 Hook(比如useState
、useEffect
等)。
创建自定义 Hook 的步骤
- 确定要复用的逻辑:通常情况下,你会发现某些组件中使用的逻辑可以复用,比如表单处理、API 请求、窗口尺寸变化等。
- 抽取逻辑到自定义 Hook:将这部分逻辑移到一个函数中,这个函数返回相关的状态和操作方法。
- 使用自定义 Hook:在需要的组件中调用这个自定义 Hook,并使用它提供的状态和方法。
官网演示的是检测网络连接 那么这里就做一个检查窗口大小的hook
useWindowSize
自定义 Hook
/**
* 自定义 Hook: 用于获取并跟踪窗口的宽度和高度。
* 使用了 `useState` 来存储窗口尺寸,`useEffect` 来监听窗口尺寸变化事件。
*/
function useWindowSize() {
// 1. 初始化宽度和高度的状态
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
// 2. 定义更新窗口尺寸的函数
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 3. 监听窗口尺寸变化事件
window.addEventListener('resize', handleResize);
// 4. 清除事件监听器
return () => {
window.removeEventListener('resize', handleResize);
};
}, [windowSize]); // 窗口对象 确保对象该改变数据实时更新
// 5. 返回窗口尺寸
return windowSize;
}
export default useWindowSize;
然后组件内进行使用
import useWindowSize from '../../hooks/useWindowSize.ts'; // 假设自定义 Hook 保存在 useWindowSize.js 文件中
/**
* 这个组件使用了自定义 Hook `useWindowSize` 来显示和跟踪窗口的尺寸。
*/
function WindowSizeComponent() {
// 1. 使用自定义 Hook 来获取窗口尺寸
const { width, height } = useWindowSize();
return (
<div style={{ padding: '20px', border: '1px solid #ccc', textAlign: 'center' }}>
<h2>当前窗口尺寸</h2>
<p>宽度: {width}px</p>
<p>高度: {height}px</p>
</div>
);
}
export default WindowSizeComponent;
还可以再写一个 axios的hook
import { useState, useEffect } from 'react';
import axios from 'axios';
/**
* 自定义 Axios Hook: 用于发起 HTTP 请求并处理数据的加载状态、错误和返回数据。
* @param {Object} config - Axios 请求配置,包括 method, url, data 等。
* @param {boolean} immediate - 是否立即发起请求,默认 true。
*/
const useAxios = (config, immediate = true) => {
const [data, setData] = useState(null); // 存储请求返回的数据
const [loading, setLoading] = useState(false); // 加载状态
const [error, setError] = useState(null); // 错误状态
const [trigger, setTrigger] = useState(immediate); // 是否立即触发请求
const fetchData = async () => {
setLoading(true);
try {
const response = await axios(config); // 通过 Axios 发起请求
setData(response.data); // 请求成功,保存返回的数据
setError(null); // 清除错误
} catch (err) {
setError(err.message || 'Something went wrong'); // 请求失败,保存错误信息
} finally {
setLoading(false); // 结束加载状态
}
};
useEffect(() => {
if (trigger) {
fetchData();
}
}, [trigger]); // 当 `trigger` 变化时发起请求
// 提供一个手动触发请求的方法
const refetch = () => setTrigger(true);
// 返回数据、加载状态、错误信息和重新请求的方法
return { data, loading, error, refetch };
};
export default useAxios;
使用的组件
import React, { useState } from 'react';
import useAxios from './useAxios';
function AxiosComponent() {
// 1. 配置请求
const config = {
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/posts/1',
};
// 2. 使用自定义 Hook
const { data, loading, error, refetch } = useAxios(config);
// 3. 处理加载状态、错误、数据展示
return (
<div style={{ padding: '20px' }}>
<h2>Axios Hook 请求演示</h2>
{loading && <p>加载中...</p>}
{error && <p>错误: {error}</p>}
{data && (
<div>
<h3>{data.title}</h3>
<p>{data.body}</p>
</div>
)}
<button onClick={refetch} style={{ marginTop: '20px' }}>
重新加载数据
</button>
</div>
);
}
export default AxiosComponent;
React Hooks
刚才用的useState ,UseRef ,UseContent 都属于hook 接下来说其他的hooks
建议直接看这个 官方文档 https://zh-hans.react.dev/reference/react/hooks
effect
useEffect
是 React 中的一个 Hook,用于在组件渲染后执行副作用操作。useEffect
有几种使用形式,根据依赖项的不同,它可以控制副作用执行的时机。接下来,我们将详细分析 useEffect
的多种使用方式
1. 无依赖项数组(每次渲染都执行)
如果 useEffect
中没有依赖项数组,意味着每次组件更新时,都会执行 useEffect
,即无论组件的任何状态或属性发生任何变化,useEffect
都会在每次重新渲染后运行。
useEffect(() => {
set("useEffect渲染完成" + new Date().toLocaleTimeString());
});
特点:
- 这种形式会在每次渲染完成后执行,包括初次挂载和之后的每次更新。
- 注意:这样会导致频繁调用,可能会影响性能,尤其是在组件频繁更新的情况下。
2. 空依赖项数组(只在组件挂载时执行)
当依赖项数组为空 []
时,useEffect
只会在组件**第一次渲染(挂载)**时执行一次,并且在组件卸载时清除(如果有清理函数)。
useEffect(() => {
set("useEffect渲染完成" + new Date().toLocaleTimeString());
}, []);
特点:
- 只会在组件初次渲染时执行一次(类似于
componentDidMount
)。用来做初始渲染 - 对性能更加友好,因为它不会在每次组件更新时都执行。
3. 依赖项数组(依赖特定变量变化时执行)
如果你提供了一个依赖项数组,useEffect
只会在数组中的依赖项发生变化时执行。你可以将状态或属性值放入依赖项数组中,这样只有这些值发生变化时,useEffect
才会运行。
useEffect(() => {
set("useEffect渲染完成" + new Date().toLocaleTimeString());
}, [count, name]);
特点:
- 只会在依赖项数组中的变量变化时执行(类似于
componentDidUpdate vue watch
)。 - 有效控制了副作用的执行时机,避免不必要的调用,提升性能。
4. 带清理函数的 useEffect
(处理副作用清理)
useEffect
还可以返回一个清理函数,这通常用于处理订阅、计时器或其他需要在组件卸载时清除的副作用。
useEffect(() => {
const timer = setInterval(() => {
set("useEffect渲染完成" + new Date().toLocaleTimeString());
}, 1000);
// 返回清理函数
return () => {
clearInterval(timer);
};
}, []);
特点:
- 当组件卸载或者依赖项变化时,清理函数会执行,用于清除副作用(类似于
componentWillUnmount
)。 - 常用于计时器、事件监听、网络请求等副作用的清理。
总结几种 useEffect
的使用方式:
- 不传依赖项:每次组件渲染时都会执行。
- 空依赖项数组
[]
:只在组件初次渲染时执行一次。 - 传递依赖项
[dep1, dep2]
:仅在依赖项发生变化时执行。 - 带清理函数:在组件卸载或依赖项更新时清理副作用。
在 React 中,如果你想通过 useEffect
监听对象或数组中的某个值、对象本身或数组本身的变化,有几种不同的方式可以实现。这取决于你是否希望监听单个属性或元素的变化,还是整个对象或数组的变化。
1. 监听对象中的某个值
你可以在依赖项数组中直接指定对象的某个属性。如果只想监听对象中的某个属性的变化,可以将该属性作为依赖项。
const [person, setPerson] = useState({ name: "Alice", age: 25 });
useEffect(() => {
console.log("person.name changed:", person.name);
}, [person.name]); // 只监听对象的 name 属性变化
2. 监听数组中的某个值
类似地,可以直接监听数组中的某个元素,通常会基于状态或其他条件动态指定依赖项。
const [numbers, setNumbers] = useState([1, 2, 3, 4]);
useEffect(() => {
console.log("numbers[0] changed:", numbers[0]);
}, [numbers[0]]); // 只监听数组第一个元素的变化
3. 监听整个对象
如果你想监听整个对象的变化,可以将整个对象作为依赖项。不过需要注意的是,如果对象是复杂的引用类型,即使对象的内部属性改变,React 可能不会重新触发 useEffect
,除非整个对象本身被替换。因此,更新对象时最好使用 浅拷贝 或 结构赋值,以确保 useEffect
能够监听到变化。
const [person, setPerson] = useState({ name: "Alice", age: 25 });
useEffect(() => {
console.log("Person object changed");
}, [person]); // 监听整个 person 对象的变化
// 示例:更新对象属性时,确保重新创建对象引用
const updateAge = () => {
setPerson(prev => ({ ...prev, age: prev.age + 1 }));
};
注意:
如果仅仅更新对象的某个属性而不替换整个对象(即直接修改引用类型的属性),不会触发 useEffect
重新运行。为了触发更新,对象需要重新创建。
4. 监听整个数组
同样的,监听整个数组也可以直接将数组作为依赖项。当数组中的内容发生变化(例如通过 setNumbers([...newNumbers])
更新数组时),会重新触发 useEffect
。
const [numbers, setNumbers] = useState([1, 2, 3, 4]);
useEffect(() => {
console.log("Numbers array changed:", numbers);
}, [numbers]); // 监听整个数组的变化
// 示例:更新数组时,确保创建新的数组引用
const addNumber = () => {
setNumbers(prev => [...prev, 5]);
};
5. 监听对象或数组中的深层属性
React 的 useEffect
默认不会对深层属性进行深度比较,只会对引用进行浅比较。如果你想监听对象或数组中的深层嵌套属性变化,简单的依赖项数组可能不会有效。这种情况下,可以考虑使用 JSON.stringify
进行深度比较(仅适合简单的场景),或者使用自定义的比较逻辑。
const [person, setPerson] = useState({ name: "Alice", age: 25, address: { city: "New York" } });
useEffect(() => {
console.log("Person's city changed:", person.address.city);
}, [JSON.stringify(person.address)]); // 深层监听 address 对象变化
演示代码
import {useEffect, useState} from "react";
function HookEffect() {
const style={
border: "1px solid #ccc",
borderRadius: "5px",
boxShadow: "0 0 10px rgba(0,0,0,0.3)",
padding: "20px",
transform: "translate(-50%, -50%)",
color: "red",
margin: "20px 20px",
fontSize: "30px"
}
const [message, set] = useState("演示effect")
const [count, setCount] = useState(0)
const [isShow, setIsShow] = useState(false)
//如果 useEffect 中没有依赖项数组,意味着每次组件更新时,都会执行 useEffect,即无论组件的状态或属性发生任何变化,useEffect 都会在每次重新渲染后运行
// useEffect(() => {
// set("useEffect渲染完成"+new Date().toLocaleTimeString())
// })
// useEffect(() => {
// set("任一数据改变 useEffect渲染完成"+new Date().toLocaleTimeString())
// },[])
useEffect(() => {
set("特定监听的数据让useEffect渲染完成")
},[count])
return (
<>
<div style={style}>HookEffect
<hr/>
<span>渲染内容</span>
<p>{message}</p>
<p>计数器:{count}</p>
<button onClick={() => setCount(count+1)}>计数器+1</button>
{isShow && <button onClick={() => setIsShow(false)}>卸载时钟</button>}
{!isShow && <button onClick={() => setIsShow(true)}>挂载时钟</button>}
</div>
<div>
{isShow && <Clock/>}
</div>
</>
)
}
const Clock = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date().toLocaleTimeString());
console.log(new Date().toLocaleTimeString())
}, 1000);
// 清理函数,组件卸载时清除计时器
return () => {
clearInterval(timer);
};
}, []); // 依赖项为空,意味着只在组件挂载时执行
return (
<div>
<h2>当前时间: {time}</h2>
</div>
);
};
export default HookEffect;
- useXXX的hook函数只能在组件中使用
- 并且不运行在for,if等逻辑判断里面执行,只能在top 顶层使用
- 监听对象中的某个值:可以直接将该值(如
person.name
)作为useEffect
的依赖项。 - 监听数组中的某个值:可以直接将该值(如
numbers[0]
)作为依赖项。 - 监听整个对象或数组:将整个对象或数组作为依赖项,并且在更新时创建新的对象或数组引用。
- 深层属性监听:可以通过
JSON.stringify
等技巧强制比较对象或数组的深层属性变化,但这并不是最佳方案,复杂场景下需要定制逻辑。
性能优化hook
当出现复杂计算结果的时候,比如cons agesum= 元素之和 ,由于响应式,每次有状态更新就会触发组件整体刷新,那么这个复杂结果也会更新,这样重复的计算 对性能会进行拖累,为此有一个类似计算属性的api 解决了问题
useMemo(()=>返回计算结果的回调函数,[监听对象列表])
缓存复杂计算的结果,只有第二个参数对象数组的状态发生改变才进行重新渲染
function Index() {
const ref = useRef(null);
const [students, setStudents] = useState<Student[]>(initialStudents);
const [newName, setName] = useState("");
const [newAge, setAge] = useState(0);
const [value, setValue] = useState(false);
// 复杂计算的示例函数
const computeExpensiveValue = (students: Student[]) => {
console.log("计算复杂值");
// 模拟复杂计算
return students.map(student => student.age * 2).reduce((a, b) => a + b, 0);
};
// 使用 useMemo 缓存复杂计算的结果,只有当 newname 或者回调参数变化时才进行调用对调 的到计算属性
//一般第二个参数就是回调参数的传参 这里是为了演示
const totalAge = useMemo(() => computeExpensiveValue(students), [newName]);
return (
<div className="container">
<h1>学生列表</h1>
{/* 显示缓存计算结果 */}
<p>学生总年龄(*2后的和):{totalAge}</p>
<div>
{students.map((item, index) => (
<div key={index}>{item.name} - {item.age}</div>
))}
</div>
<h2>添加新学生</h2>
<input
type="text"
placeholder="请输入姓名"
ref={ref}
value={newName}
onChange={(e) => setName(e.target.value)}
/>
<input
type="number"
placeholder="请输入年龄"
value={newAge}
onChange={(e) => setAge(Number(e.target.value))}
/>
<button
onClick={() => {
const newStudent = { name: newName, age: newAge };
ref.current.focus(); // 聚焦到输入框
setStudents([...students, newStudent]); // 使用扩展运算符创建新数组
setName(""); // 清空输入框
setAge(0); // 清空输入框
}}
>
添加学生
</button>
{/* 模拟组件的状态变化引发更新 */}
<span>状态变量:{value.toString()}</span>
<button onClick={() => setValue(!value)}>切换状态</button>
</div>
);
}
所以这里我点击切换状态刷新重新渲染以后复杂计算函数的回调,不会触发回调的日志,说明没有重新渲染,只有监听的属性改变时才会重新输出
子组件频繁渲染
下面这种情况,当一个父组件中调用了一个子组件的时候,父组件任意state更新 state 发生变化,会导致父组件更新页面重新渲染,而子组件虽然没有发生改变,但也会进行更新。简单的理解下,当一个页面内容非常复杂,模块非常多的时候,函数式组件会从头更新到尾,只要一处改变,所有的模块都会进行刷新,这种情况显然是没有必要的。
// 定义学生类型
type Student = {
name: string;
age: number;
};
// 初始学生列表
const initialStudents: Student[] = [
{ name: "John", age: 25 },
{ name: "Mary", age: 23 },
{ name: "Bob", age: 24 }
];
function Song() {
console.log("我是子组件进行渲染");
return <div>我是子组件</div>;
}
function Index() {
const ref = useRef(null);
const [students, setStudents] = useState<Student[]>(initialStudents);
const [newName, setName] = useState("");
const [newAge, setAge] = useState(0);
const [value, setValue] = useState(false);
// 复杂计算的示例函数
const computeExpensiveValue = (students: Student[]) => {
console.log("计算复杂值");
// 模拟复杂计算
return students.map(student => student.age * 2).reduce((a, b) => a + b, 0);
};
// 使用 useMemo 缓存复杂计算的结果,只有当 students 变化时才重新计算
const totalAge = useMemo(() => computeExpensiveValue(students), [students]);
return (
<div className="container">
<h1>学生列表</h1>
{/* 显示缓存计算结果 */}
<p>学生总年龄(*2后的和):{totalAge}</p>
<div>
{students.map((item, index) => (
<div key={index}>{item.name} - {item.age}</div>
))}
</div>
<h2>添加新学生</h2>
<input
type="text"
placeholder="请输入姓名"
ref={ref}
value={newName}
onChange={(e) => setName(e.target.value)}
/>
<input
type="number"
placeholder="请输入年龄"
value={newAge}
onChange={(e) => setAge(Number(e.target.value))}
/>
<button
onClick={() => {
const newStudent = { name: newName, age: newAge };
ref.current.focus(); // 聚焦到输入框
setStudents([...students, newStudent]); // 使用扩展运算符创建新数组
setName(""); // 清空输入框
setAge(0); // 清空输入框
}}
>
添加学生
</button>
{/* 模拟组件的状态变化引发更新 */}
<span>状态变量:{value.toString()}</span>
<button onClick={() => setValue(!value)}>切换状态</button>
<Song />
</div>
);
}
只要点击切换状态 那么子组件就会重新更新
优化 使用memo 此时只有传递的props:{} 属性发生变化子组件才进行更新
// 使用 React.memo 来优化 Song 组件
const Song = memo(() => {
console.log("我是子组件进行渲染");
return <div>我是子组件</div>;
});
// 使用 React.memo 包裹 Song 组件,并添加 props
const Song = memo(({ message }) => {
console.log("我是子组件进行渲染");
return <div>{message}</div>;
});
useCallback
缓存回调函数
useCallback返回的是缓存函数运行的结果
注意:这个函数是父组件传递子组件的一个函数,防止做无关的刷新,所以这个组件必须配合memo,否则不但不会提升性能
优化后
import React, { useState, useRef, useMemo, useCallback } from 'react';
// 定义学生类型
type Student = {
name: string;
age: number;
};
// 初始学生列表
const initialStudents: Student[] = [
{ name: "John", age: 25 },
{ name: "Mary", age: 23 },
{ name: "Bob", age: 24 }
];
// 使用 React.memo
const Song = memo(() => {
console.log("我是子组件进行渲染");
return <div>我是子组件</div>;
});
function Index() {
const ref = useRef(null);
const [students, setStudents] = useState<Student[]>(initialStudents);
const [newName, setName] = useState("");
const [newAge, setAge] = useState(0);
const [value, setValue] = useState(false);
// 模拟复杂计算函数
const computeExpensiveValue = useCallback((students: Student[]) => {
console.log("计算复杂值");
return students.map(student => student.age * 2).reduce((a, b) => a + b, 0);
}, []); // 依赖项为空,这个函数在组件挂载时只会创建一次
// 使用 useMemo 缓存复杂计算的结果,只有当 students 变化时才重新计算
const totalAge = useMemo(() => {
console.log("重新计算总年龄");
return computeExpensiveValue(students);
}, [students, computeExpensiveValue]); // 依赖项为 students 和 computeExpensiveValue
// 缓存的输入处理函数
const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
console.log("Name 变化");
setName(e.target.value);
}, []); // 依赖项为空,函数在挂载时只会创建一次 其他时候不创建函数实列 配合memo 保证子组件的函数随着子组件的挂载才进行创建
const handleAgeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
console.log("Age 变化");
setAge(Number(e.target.value));
}, []); // 依赖项为空
// 缓存的添加学生函数,依赖于 newName 和 newAge
const handleAddStudent = useCallback(() => {
console.log("添加学生");
const newStudent = { name: newName, age: newAge };
ref.current.focus(); // 聚焦到输入框
setStudents(prevStudents => [...prevStudents, newStudent]); // 使用扩展运算符创建新数组
setName(""); // 清空输入框
setAge(0); // 清空输入框
}, [newName, newAge]); // 依赖于 newName 和 newAge
return (
<div className="container">
<h1>学生列表</h1>
{/* 显示缓存计算结果 */}
<p>学生总年龄(*2后的和):{totalAge}</p>
<div>
{students.map((item, index) => (
<div key={index}>{item.name} - {item.age}</div>
))}
</div>
<h2>添加新学生</h2>
<input
type="text"
placeholder="请输入姓名"
ref={ref}
value={newName}
onChange={handleNameChange} // 使用缓存的输入处理函数
/>
<input
type="number"
placeholder="请输入年龄"
value={newAge}
onChange={handleAgeChange} // 使用缓存的输入处理函数
/>
<button onClick={handleAddStudent}>添加学生</button>
{/* 模拟状态变化 */}
<span>状态变量:{value.toString()}</span>
<button onClick={() => setValue(!value)}>切换状态</button>
<Song />
</div>
);
}
export default Index;
其他hooks
其他的hooks使用场景较少 官方都单独把他们分到一起
官方文档 https://zh-hans.react.dev/reference/react/hooks