安装:
yarn create react-app reatc-lesson --template typescript
yarn create 创建一个react-app的应用 项目名称 typescript 的模板
react-app 官方地址
https://create-react-app.bootcss.com/docs/adding-typescript
react 语法文档
https://zh-hans.react.dev/learn#writing-markup-with-jsx
语法
基础语法
组件函数
1、必须使用大驼峰命名;
2、return 之前可以定义组件使用的数据;
3、使用一对大括号即可使用定义对象的属性;
4、reactNode 不支持直接渲染布尔值,布尔值用来条件渲染处理或者将其转成字符串来使用。
5、reactNode 不支持直接渲染对象,需要将其转为字符串才能直接渲染。
5、ReactNode 类型用来表示可以在React 组件中渲染的任何内容的一种类型,可以直接渲染:字符串、数字、元素或者或者包含这些类型的数组。
//return () 用来存放html组件的
function MyBanner(){
//return 之前可以定义组件使用的数据
const user = {
name: 'Joe',
}
{/* */} //注释语法
{/* 返回组件的根元素*/}
return(
<h1 className>Hello MyBanner {user.name}</h1>
)
(/* 也可以不使用括号返回 */)
return <h1 className>Hello MyBanner {user.name}</h1>
}
注释
语法: {/* */}
{/* */}
添加class名称
只能使用小驼峰命名的属性名:className 来指定一个css 的class,使用方式跟class一样
<img className="avatar" />
.avatar {
border-radius: 50%;
}
根据数据渲染视图
将数据放在元素标签中,放到一对大括号里,例如:{user.name}
return (
<h1>
{user.name}
</h1>
)
还可以将定义的数据放到元素属性上,但是必须使用大括号 而非引号。例如, className={avatar} 将avatar 字符串传递给 className,作为css的class,而非通过变量传递给className。但 src={user.imageUrl} 会读取js 的 user.imageUrl 这个变量,然后将其读取的值作为 src 属性传递
return (
<img className='avatar' src={user.imageUrl} />
)
一个组件中返回多个元素
1、在React语法中,要去一个组件的返回值只能有一个根元素。
2、使用div 包裹多个元素是一种常见的方法,但有时会导致不必要的DOM层次结构。
解决:引入 <></> 作为一种更简洁的方法。
<></> 是React 中的一种称为 Fragment 的语法。它是一种用于在组件中返回多个元素而不需要创建额外DOM元素的简洁方式
return (
<>
<h1>{user.name}</h1>
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
color:'red'
width: user.imageSize,
height: user.imageSize
}}
/>
</>
);
添加style样式
语法:style={{}} ,是style={} 大括号内的一个普通 {} 对象。当样式依赖 js 变量时,可以使用 style属性
return (
<>
<h1>{user.name}</h1>
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
color:'red'
width: user.imageSize,
height: user.imageSize
}}
/>
</>
);
使用style样式的方式
function MyComponent(){
const styles = {
color:'red',
fontSize:'16px',
fontWeight:'bold'
}
return (<div style={styles}>这是一个文本</div>;)
}
{/* 使用css模块化文件 */}
import styles from './styles.module.css';
function MyComponent() {
return (<div className={styles.myClass}>这是一个文本</div>)
}
条件渲染
React没有特殊语句来编写条件语句,使用的就是普通的 js 代码。例如:if
const user = {
name: 'Joe',
age: 32,
isAdmin: false,
isBanned: true,
}
let content;
if(user.isAdmin){
content = <h2>Welcome, {user.name}!</h2>
}else {
content = <h2>You are not an admin.</h2>
}
或者通过组件:三目元算符
<div>{user.isBanned ? (<MyBanner />) : (<MyButton />)}</div>
又或者是 if 引入组件
let content;
if (isLoggedIn) {
content = <MyBanner />
} else {
content = <MyBanner2 />;
}
return (
<div>
{content}
</div>
);
又或者是 &&
<div>
{isLoggedIn && <AdminPanel />}
</div>
渲染列表
也是依赖js特性,例如for循环 和 map函数来渲染组件。
写法1:组件外循环
const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
]
{/* 注意 li 里有一个key属性。对应列表每一个元素,都应该传递一个字符串或数字给key,用于在其他 兄弟节点中唯一标识该元素,key是什么数据跟vue循环的key是一样的 */}
const listItem = user.products.map(item => <li key={item.id}>{item.title}</li>)
<ul>{listItems}</ul>
写法2,在组件内循环
<ul>
{
user.products.map((item,index) => (
<li key={index}>{item.title}</li>
))
}
</ul>
写法3:带样式
const itemList = user.products.map(item =>
<li
key={item.id}
style={{color: item.id % 2 === 0 ? 'red' : 'blue'}}
>
{item.title}
</li>
)
<ul>
{itemList}
</ul>
for循环,没其他写法
const itemList2 = []
for(let i = 0; i < user.products.length; i++){
itemList2.push(
<li key={i}>
{user.products[i].title}
</li>
)
}
<ul>
{itemList2}
</ul>
响应事件
基本使用
也就是点击事件咯,语法是 onClick={函数}
function App() {
function handleClick (aaa :any){
{/* 在默认情况,事件监听器的参数aaa 是一个事件对象(通常命名event,我现在命名aaa),这个事件包含事件类型、目标元素等 */}
console.log('clicked',aaa)
console.log(aaa.target) {/* 获取目标元素*/}
}
return (
<div className="App">
{/* 基础写法1 */}
<button onClick={handleClick}>点击响应事件</button>
{/* 内联事件函数处理 */}
<button onClick={function handleClick() {
alert('hello')
}}>OK1</button>
{/* 简洁箭头函数 */}
<button onClick={() => {
alert('你点击了我!');
}}>
</div>
);
}
事件监听传参
使用箭头函数,在事件监听中使用箭头函数来传递参数。在箭头函数中,可以访问事件对象(入event)以及传递给事件监听的其他参数
{/* 函数定义 */}
const handleClick2 =(aaa :Number) =>{
console.log('clicked',aaa)
return aaa
}
或者
function handleClick2 (aaa :Number){
console.log('clicked',aaa)
return aaa
}
return (
<button onClick={() => handleClick2(2)}>点击传值</button>
)
使用bind方法:
通过bind方法,可以绑定参数并创建一个新的函数,该函数将在事件触发是被调用
{/* 函数定义 */}
const handleClick2 =(aaa :Number) =>{
console.log('clicked',aaa)
return aaa
}
或者
function handleClick2 (aaa :Number){
console.log('clicked',aaa)
return aaa
}
return (
<button onClick={handleClick2.bind(null,2)}>点击传值</button>
)
错误陷阱
错误1
传递事件处理函数的函数应该是直接传递,而非直接调用。
这个示例中,handleClick 作为一个 onClick 事件处理函数传递。这会让React 记住,并且只在点击按钮的时候调用 传递的函数。
// 传递一个函数(正确写法)
<button onClick={handleClick}></button>
// 调用一个函数(错误写法)
<button onClick={handleClick()}></button>
错误2
// 传递一个函数(正确)【alert 定义内联事件函数,点击的时候触发】
<button onClick="{() => alert('...')}"></button>
// 调用一个函数(错误)【这个 alert 在组件渲染时触发,还不是在点击时触发】
<button onClick="{alert('...')}"></button>
其他常见响应事件
1、onChange 表单元素值发生变化触发
当表单元素的值发生变化时触发,比如输入框的文本内容发生变化。
import React,{useState} from 'react';
function App() {
const [person, setPerson] = useState({
name:'',
age:0
})
function inputUpChange (event: React.ChangeEvent<HTMLInputElement>){
const {name,value} = event.target
console.log(name,value) //打印 nanme属性名,value 输入值
setPerson({...person, [name]: value}) //设置值
}
return (
<div>
<h4>当前信息{JSON.stringify(person)}</h4>
<input type="text" name='name' value={person.name} onChange={inputUpChange} />
<input type="text" name='age' value={person.age} onChange={inputUpChange} />
</div>
);
}
2、onSubmit 表单提交时触发
点击按钮时触发 form表单 提交函数
submitUserInfo
注意:在表单上使用 onSubmit 事件,并没有阻止默认行为,它将触发表单的默认提交行为,导致页面刷新。
import React,{useState} from 'react';
function App() {
const [person, setPerson] = useState({
name:'',
age:0
})
function submitUserInfo(){
console.log('submit',user)
}
return (
<div>
<h4>当时onSubmit 信息</h4>
<form onSubmit={submitUserInfo}>
<input type="text" name='name' value={person.name} onChange={inputUpChange} />
<button type='submit'>点击触发submit事件</button>
</form>
</div>
);
}
为了阻止默认行为,可以在onSubmit 事件处理函数中调用
event.preventDefault()
方法。将阻止表单的默认提交行为,从而避免页面刷新。
下面的代码,在用户点击提交按钮时候,
submitHandle
函数将被调用,并且e.preventDefault()
将阻止表单的默认提交行为,从而避免页面刷新。可以在submitHandle
函数中执行提交表单的逻辑。
在没有使用
preventDefault
的情况下,打印的对象和数组无法展开的,因为在提交后表单的默认行为会导致刷新
import React,{useState} from 'react';
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
]
function StatesFormBox() {
const [iibb,setIibb] = useState(initialList)
function submitHandle(e: any){
e.preventDefault()
console.log(e)
console.log(666,iibb)
}
function aa(){
setIibb([{ id: 3, title: 'Terracotta Army', seen: false }])
}
return (
<div>
{/* <h1>State Form</h1> */}
<form onSubmit={submitHandle}>
<button type='submit'>点击按钮提交</button>
</form>
</div>
)
}
export default StatesFormBox;
3、onMouseEnter 当鼠标移入元素时触发
import React,{useState} from 'react';
function App() {
function handleMouseEnter(){
console.log('鼠标移入元素了')
}
return (
<div style={{width:'100px',height:'100px',border:'1px solid green'}} onMouseEnter={handleMouseEnter}>
<h4>鼠标进入</h4>
</div>
);
}
4、onMouseLeave 当鼠标移出元素时触发
import React,{useState} from 'react';
function App() {
function handleMouseLeave(){
console.log('鼠标移出元素了')
}
return (
<div style={{width:'100px',height:'100px',border:'1px solid green'}} onMouseLeave={handleMouseLeave}>
<h4>鼠标移出</h4>
</div>
);
}
5、onKeyDown 当按下键盘上的任何信息时触发
使用方法更上面类型
<input type="text" onKeyDown={handleKeyDown} />
6、onKeyUp 当释放键盘上的任意键时触发
<input type="text" onKeyUp={handleKeyUp} />
7、onFocus 当元素获取焦点时触发
<input type="text" onFocus={handleFocus} />
8、onBlur 当元素失去焦点时触发
<input type="text" onBlur={handleBlur} />
9、onScroll 当前元素滚动时触发
<div onScroll={handleScroll}>滚动时触发</div>
子组件接收父组件的child,类型vue的v-text
import React,{useState} from 'react';
import Gallery from './Gallery';
function App() {
function clickHandle(num: number, num2: number){
console.log(num + num2)
return num + num2
}
function MyButton4({onClick, children}:{onClick:(num:number,num2:number)=>number,children:string}){
return (
<div>
{/* 渲染会显示 <button onClick={()=> onClick(2,2)}>我是传递</button> 的按钮 */}
<button onClick={()=> onClick(2,2)}>{children}</button>
</div>
)
}
return (
<div>
<MyButton4 onClick={clickHandle}>我是传递</MyButton4>
</div>
);
}
更新界面
就是更改数据,更新视图,数据驱动视图
1、从useState 中获得两样东西:当前的state(count),以及更新值的函数(setCount)。也可以起任何名字,但是惯例会像这样:[something, setSomething] 这样命名
2、第一次显示,count 的值默认为0,因为你
import React from 'react';
import { useState } from 'react';
优化后一行搞到:
import React,{useState} from 'react';
function App() {
{/* 从useState 中获得两样东西:当前的state(count),以及更新值的函数(setCount)。 */}
const [count, setCount] = useState(0); {/* 默认值0*/}
{/* 自定义命名 */}
// 声明一个num的状态变量,并初始化为 2
const [num, setNum] = useState(2); {/* 默认值2 */}
function updateClick(){
setCount(count + 1)
}
function updateNum(){
setNum(num + 1)
}
return (
<div className="App">
<button onClick={updateClick}>点击 {count} 了</button>
<button onClick={updateNum}>点击了num值{num}了</button>
</div>
);
}
Hook
再React中,以 use 开头的函数都被称为 Hook。 useState 是React 提供的内置 Hook 函数。
Hook 比普通函数更为严格。只能在组件(或者其他Hook)的顶层 调用 Hook。如果要在一个条件或者循环中使用 useState,需要在新的组件并在内部使用它。
注意
Hooks ---->以
use
开头的函数 -----> 只能在组件活自定义 Hook的最顶部调用。 不能在条件语句、循环语句或者其他嵌套函数内调用 Hook。Hook是函数,但将其视为关于组件需求的无条件声明。
//useState 的唯一参数是 state 变量的 初始值。在这个例子中,index的初始值被 useState(0) 设置为0
const [index, setIndex] = useState(0)
渲染步骤
- **组件进行第一次渲染。**因为你将
0
作为index
的初始值传递给useState
,它将返回[0, setIndex]
。React 记住0
是最新的 state值。 - 你更新了state。 当点击按钮时,调用
setIndex(index +1)
。index
是0
,所以它是setIndex(1)
。这告诉 React 现在记住index
是1
并触发下一次渲染。 - 组件进行第二次渲染。 React 仍然看到
useState(0)
, 但是因为 React 记住了你将index
设置为了1
,它将返回[1, setIndex]
。 - 以此类推
组件共享
在子组件里使用父组件传的方法和变量数据
import React,{useState} from 'react';
import Gallery from './Gallery';
// {person,size} 组件使用时的属性,要一一对应,对接收的值类型验证
function MyButton1({person,size}:{person:Object,size:number}) {
return (
<button>按钮一号:{JSON.stringify(person)}---{size > 1 ? 1 : 2}</button>
)
}
// onClick 是组件使用的属性名,冒红后面的对象是对这个函数的描述的类型解析
function MyButton2({onClick}:{onClick:() => void}) {
return (
<button onClick={onClick}>按钮二{num}号</button>
)
}
//接收一个属性,对函数执行的时候参数和返回值的要去
function MyButton3({onClick}:{onClick:(num:number,num2:number)=>number}){
return (
<div>
<button onClick={()=> onClick(2,2)}>按钮三</button>
</div>
)
}
function App() {
const [num, setNum] = useState(2);
return (
<div>
<MyButton1
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
<MyButton2 onClick={updateNum} />
<MyButton3 onClick={clickHandle} />
</div>
);
}
组件
定义组件
function Profile(){
return (
<img
src='https://www......'
/>
)
}
组件的导入导出
export 居然导出,export default 默认导出
function Profile(){
return (
<img
src='https://www......'
/>
)
}
// export 居然导出
export default function Gellery(){
return (
<h1>open111</h1>
<Profile />
)
}
import 组件 from ‘组件文件地址’ =》 import Gallery from ‘./Gallery’
Gallery.tsx 导出
function Profile() {
return (
<img
src="https://i.imgur.com/QIrZWGIs.jpg"
alt="Alan L. Hart"
/>
);
}
export default function Gallery(){
return (
<>
<h1>开始了</h1>
<Profile />
<Profile />
</>
)
}
app.tsx 导入
引入过程中,import Gallery from ‘./Gallery’; ,后缀.jsx添加与否都能正常使用。
import Gallery from './Gallery';
function App() {
return (
<div>
<Gallery />
<Gallery />
</div>
);
}
导入导出注意点
从一个文件中导出和导入多个文件
//用具名方式导出
export function Profile(){
//****
}
//接着,具名导入的方式,从文件到当前组件文件中(用大括号)
import {Profile} from './Gallery.tsx'
//渲染
export default function App() {
return <Profile />;
}
//用默认导出的方式
export default function Gallery() {
return (
<section>
<h1>了不起的科学家们</h1>
</section>
);
}
// 导入 默认导出的组件
import Gallery from './Gallery.tsx';
//渲染
export default function App() {
return <Gallery />;
}
嵌套组组件
组件里可以渲染其他组件,但是 请不要嵌套定义组件的定义。下面这段代码 非常慢,并且还会导致bug产生
export default function Gallery() {
// 🔴 永远不要在组件中定义组件
function Profile() {
// ...
}
// ...
}
正常使用
export default function Gallery() {
// ...
}
// ✅ 在顶层声明组件
function Profile() {
// ...
}
组件记忆:双向绑定
组件通常需要通过 交互更改屏幕上显示的呢绒。输入表单 应该更新输入字段,点击轮播图上的 “下一个”应该更改显示的图片,点击 “购买” 应该将商品放入购物车。组件需要 “记住” 某些东西:当前输入值、当前图片、购物车等。值React中,这种组件特有的记忆称为 state。
普通的变量的值改变时,更新变量的值时,组件没有出现数据驱动视图
普通变量无法驱动改变视图
点击按钮,变量的值更新了,但是视图没有变化。
注意
updateAgeHandle()
事件处理函数整个更新局部变量 age,有两个原因使得视图没有更新1、**局部变量无法在多长渲染中持久保持。**当React 再次渲染这个组件时,会从头开始渲染,不会考虑之前对局部变量的任何更改。
2、**更改局部变量不会触发渲染。**React 没有意识到它需要使用新数据再次渲染组件。
function App() {
const user = {
name: 'Joe',
age: 32,
}
function updateAgeHandle(){
user.age += 1
console.log('age',user.age)
}
return (
<div>
<h4>当前年龄:{user.age}</h4>
<button onClick={updateAgeHandle}>更新年龄</button>
</div>
);
}
方案
要使用新数据更新组件,需要做两件事
1、保留 渲染之间的数据。
2、触发 React 使用新数据来渲染组件(重新渲染)
解决
useState Hook 提供了这两个解决功能
1、State 变量 用于保存渲染间的数据。
2、State setter 函数 更新变量并触发 React 再次渲染。
实现
// 要添加 state 变量,先从文件顶部导入 useState
import {useState} form 'react'
// 然后 将局部变量定义的代码换成state 变量
//替换后的 index 是一个state变量,setIndex 是对应的 setter 函数。
let index = 0; 【将其修改为】====>>> const [index, setIndex] = useState(0) //初始变量0
//函数中触发
function updateAgeHandle(){
setIndex(index + 1)
}
State 是隔离且私有的
State 是屏幕上组件实例内部的状态。也就是说,**如果你渲染同一个组件两次,每次副本都会有完全隔离的 state!**其中一个组件的state不会音响另外一个。
State 定义对象
看了这么多state 变量的定义,还是不太明白对象数据驱动视图怎么弄。
如下实现 表单输入更新
import React,{useState} from 'react';
function App() {
const [person, setPerson] = useState({
name:'',
age:0
})
function inputUpChange (event: React.ChangeEvent<HTMLInputElement>){
const {name,value} = event.target
console.log(name,value) //打印 nanme属性名,value 输入值
setPerson({...person, [name]: value}) //设置值
}
return (
<div>
<h4>当前信息{JSON.stringify(person)}</h4>
<input type="text" name='name' value={person.name} onChange={inputUpChange} />
<input type="text" name='age' value={person.age} onChange={inputUpChange} />
</div>
);
}
state 中更新数组
当操作 React state 中数组是时,你需要避免使用左列的方法,而首选右列的方法
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) | |
---|---|---|
添加元素 | push ,unshift | concat ,[...arr] 展开语法(例子) |
删除元素 | pop ,shift ,splice | filter ,slice (例子) |
替换元素 | splice ,arr[i] = ... 赋值 | map (例子) |
排序 | reverse ,sort | 先将数组复制一份(例子) |
添加元素
import React,{useState} from 'react'
function ArrayDomState() {
const [arr, setArr] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
function add1ArrHandle() {
setArr([...arr,11])
}
function add2ArrHandle() {
setArr(arr.concat(11))
}
return (
<>
{/* 添加元素 */}
<div>
<p>array 数据:{JSON.stringify(arr)}</p>
<button onClick={add1ArrHandle}>state 扩展运算符[...arr] 添加 state 数组数据</button>
</div>
<div>
<p>array 数据:{JSON.stringify(arr)}</p>
<button onClick={add2ArrHandle}>使用 concat 添加 state 数组数据</button>
</div>
</>
)
}
export default ArrayDomState
删除元素
import React,{useState} from 'react'
function ArrayDomState() {
const [arr, setArr] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
function delete1ArrHandle() {
const newArr = arr.filter((item) => item !== 1)
setArr(newArr)
}
function delete2ArrHandle() {
const newArr = [...arr]
newArr.splice(1, 1);
setArr(newArr)
}
return (
<>
{/* 删除元素 */}
<div>
<p>arr 删除元素{JSON.stringify(arr)}</p>
<button onClick={delete1ArrHandle}>state 使用 filter 删除数组数据</button>
</div>
<div>
<p>arr 删除元素{JSON.stringify(arr)}</p>
<button onClick={delete2ArrHandle}>state 使用 splice 删除数组数据</button>
</div>
</>
)
}
export default ArrayDomState
转换数组
这种方式就是转换数组,就是使用 setState() 方法来更新组件的state,从而实现对数据的转换操作。
const newArr = [...arr]
newArr.splice(1, 1);
// 使用新的数组进行重渲染
setArr(newArr)
替换数组中的元素
import React,{useState} from 'react'
function ArrayDomState() {
const [arrs, setArrs] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
function arraysDomHandle() {
const updateArr = arrs.map((item) => {
if(item === 3){
return 333333333
} else {
return item
}
});
setArrs(updateArr)
}
return (
<div>
<div>
<p>array 数据:{JSON.stringify(arrs)}</p>
<button onClick={arraysDomHandle}>点击替换数组中的元素</button>
</div>
</div>
)
}
export default ArrayDomState
向数组中间插入元素
向数组特定位置插入一个元素,这个位置既不在数组开头也不在数组末尾。
import React,{useState} from 'react'
function ArrayDomState()
const [arrs, setArrs] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
function insertArrHandle(){
const insertIndex = 5;
const newArr = [
...arrs.slice(0, insertIndex),
333333333,
...arrs.slice(insertIndex)
]
setArrs(newArr)
}
return (
<div>
{/* 向数组中插入元素 */}
<div>
<p>array 数据:{JSON.stringify(arrs)}</p>
<button onClick={insertArrHandle}>点击向数组中插入元素</button>
</div>
</div>
)
}
export default ArrayDomState
其他更改数组的情况
总有些事情,是仅靠展开运算符和
map()
或者filter()
等不会直接修改原值的方法能做到的。例如翻转数组,或者数组排序,而 javaScript 中的 reverse() 和 sort() 方法会改变原数组,所以不能直接使用她们。解决:先拷贝这个数组,然后再改变拷贝数组的值。
import React,{useState} from 'react'
function ArrayDomState()
const [arrs, setArrs] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
function changeArrHandle(){
const newArr = [...arrs];
newArr[0] = 333333333;
newArr.reverse()
setArrs(newArr)
}
return (
<div>
{/* 其他改变数组的情况 */}
<div>
<p>array 数据:{JSON.stringify(arrs)}</p>
<button onClick={changeArrHandle}>点击改变数组中的元素</button>
</div>
</div>
)
}
export default ArrayDomState
问题:
在上面的代码中,虽然使用
[...arrs]
展开运算符创建了一份数组的拷贝值。当有了拷贝值后,就可以使用newArr.reverse()
或者newArr.sort()
这样修改原数组的方法。 甚至可以通过newArr[0] = 333333333
这样的方式对特定元素进行赋值。
但是,这种拷贝方式,只能适用于 基础类型的数组,不适用对象数组的元素。 原因大家应该都知道,这种解构的方式是浅拷贝,新数组种的对象依然与原始对象数组的原始的内存地址。因此,如果你修改了拷贝数组内部的某个对象。
//虽然 nextList 和 list 是两个不同的数组,nextList[0] 和 list[0] 却指向了同一个对象。因此,通过改变 nextList[0].name,list[0].name 的值也会被改变
const nextList = [...list]
nextList[0].name = 'tom'
setList(nextList)
更新数组对象的元素
import React,{useState} from 'react'
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
]
function ArrayDomState() {
const [myList, setMyList] = useState(initialList)
function handleToggleMyList(artworkId :number, nextSeen: any) {
setMyList(myList.map((item) => {
if(item.id === artworkId){
// 创建包含变更的 新对象
return {...item, title: nextSeen}
}else {
return item
}
}))
}
return (
<div>
{/* 更新数组内部对象的值 */}
<div>
<p>更新数据{JSON.stringify(myList)}</p>
<button onClick={() => handleToggleMyList(1,'修改咯')}>更新对象数组</button>
</div>
</div>
)
}
export default ArrayDomState
使用Immer 编写简单的更新
import React,{useState} from 'react'
import { useImmer} from 'use-immer'
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
]
function ArrayDomState() {
const [myList2, setMyList2] = useImmer(initialList)
function handleToggleMyList2(artworkId :number, nextSeen: any) {
setMyList2(draft => {
console.log('draft',JSON.stringify(draft))
const artwork = draft.find((item) => item.id === artworkId)
console.log('artwork', JSON.stringify(artwork))
if(artwork) artwork.title = nextSeen
console.log('artwork222',JSON.stringify(artwork))
})
}
return (
<div>
{/* 使用Immer 编写更加简洁的更新逻辑 */}
<div>
<p>使用 Immer 编写:{JSON.stringify(myList2)}</p>
<button onClick={() => handleToggleMyList2(2,'Immer')}>使用Immer点击</button>
</div>
</div>
)
}
export default ArrayDomState
想修改对象数组的值,还得先拷贝一份。
使用Immer 时,类似
artwork.seen = nextSeen
这种会产生 mutation的语法不会再有任何问题了:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
状态管理
React 状态管理是指在React 应用中有效地管理和共享组件之间的状态。 React 也提供了一些内置的状态管理:例如 使用组件本地的状态( state) 和属性 ( props ),以及使用上下文 ( context )进行状态共享。
随着应用不断变大,应用变得更加复杂,这些内置的状态管理机制可能会变得不够灵活或难以维护。沉余或者重复的状态往往是缺陷的根源。 为了解决这个问题,通常会使用 Redux、Mobx 或者 React Context API 等
使用State 状态相应输入
在react种,不用直接从代码层面上修改UI,不用编写诸如 “禁用按钮”、“启用按钮”、“显示成功消息” 等命令。只需要描述组件在不同状态(“初始状态”、“输入状态”、“成功状态”)下希望展示的UI,然后根据用户输入触发状态变更。
使用React 编写的反馈表单,根据 status 这个状态变量来决定显示提交按钮以及 是否显示成功消息
import React,{useState} from 'react';
function StatesFormBox() {
const [age, setAge] = useState('')
const [error, setError] = useState('')
const [status, setStatus] = useState('typind')
if(status === 'success'){
return <div>Success</div>
}
async function submitHandle(e: any){
e.preventDefault()
setStatus('loading')
try {
await submitForm(age)
setStatus('success')
} catch(error) {
console.log(error)
setStatus('typind')
setError('error')
}
}
function setAgeHandle(e:any){
setAge(e.target.value)
}
return (
<div>
{/* <h1>State Form</h1> */}
<div>{age}==={error}----{status}</div>
<form onSubmit={submitHandle}>
<textarea value={age} onChange={setAgeHandle}></textarea>
<button type='submit'>点击按钮提交</button>
</form>
</div>
)
}
function submitForm(age: string){
return new Promise((resolve,reject) => {
setTimeout(() => {
console.log(age,age === '18')
if(age !== '18') {
reject(new Error('年龄不正确'))
}else{
resolve('提交成功')
}
},2000)
})
}
export default StatesFormBox;
在组件共享状态
import React,{useState} from 'react';
function Panel({title,children,isActive,onShow}:{title:string,children:string,isActive:boolean,onShow:()=>void}){
return (
<>
<h3>{title}</h3>
{
isActive ? (<p>children</p>) :(<button onClick={onShow}>显示</button>)
}
</>
)
}
function StatesFormBoxShare() {
const [activeIndex, setActiveIndex] = useState(0)
function setActiveHandle(value:number){
setActiveIndex(value)
}
return(
<>
<Panel title='标题' isActive={activeIndex === 0} onShow={() => setActiveHandle(0)}>112313</Panel>
<Panel title='标题二' isActive={activeIndex === 1} onShow={() => setActiveHandle(1)}>22222</Panel>
</>
)
}
export default StatesFormBoxShare
useReducer 的使用
在hooks中提供了 useReducer 功能,可以增强 ReducerDemo 函数提供类似 Redux的的功能。
useReducer 能接受一个 reducer 函数 作为参数,reducer 接受两个参数,一个是state 另外一个是action。然后返回一个状态 count 和 dispath,count 是返回状态中的值,而 dispath 是一个可以发布事件来更新 state 的。
基本使用
import React,{useReducer} from 'react'
export default function ReducerDemo() {
const [count, dispath] = useReducer((state,action)=> {
//...
}, 0);
return (
<div>
<h1 className="title">{count}</h1>
</div>
)
}
要点
reducers 不应该包含异步请求、定时器、或者任何副作用(对组件外部有影响的操作),应该以不可变值的方式去更新对象和数组
修改对象
下面就是useReducer 更新的使用。
事件处理程序只通过派发 action 来 指定 发生了什么,而 reducer 函数通过 响应 actions 来决定 状态如何更新
import React,{useReducer} from 'react'
//(1)初始的数据
const initInfoData = {
name : '张三',
age : 18,
sex : '男',
}
//(2) 定义组件
function UseReducerBox (){
//(3) useReducer 接受一个reducer参数:【reducerFun 自定义函数,自定义的这个reducer函数reducerFun 接受两个参数:一个是state 另一个是action。】;useReducer 接受的第二个参数:【initInfoData 就是初始的 state 数据,就是初始数据】
// useReducer 返回一个状态 count:【userInfo】和 dispath:【setUserInfo】,userInfo 是返回状态中的值,而 setUserInfo 是一个可以发布事件来触发更新state的
// count 和dispath 是官方示例的命名
const [userInfo, setUserInfo] = useReducer(reducerFun,initInfoData)
//(7) 点击函数 触发 发布事件来更新state 的。
function handleClick(){
//触发发布更新后,useReducer 第一个参数就会执行了。
setUserInfo('edit')
}
//(5)定义组件
return (
<>
<h5>useReducer 修改对象</h5>
<div>{JSON.stringify(userInfo)}</div>
{/*(6) 触发点击*/}
<button onClick={() => handleClick()}>点击设置</button>
</>
)
}
//(3) 定义 useReducer 的第一个reducer参数,接收两个参数 一个是 state 一个是action
function reducerFun(state :any, action :any){
//(4) state 当前状态下的数据,action为接收 setUserInfo 这个更新state的参数
console.log(state, action) // 打印:{name: '张三', age: 18, sex: '男'} 'edit'
//这个if可以不用
if(action === 'edit'){
//返回修改后状态数据
return {
name : '李四',
age : 20,
sex : '女'
}
}
}
export default UseReducerBox
useState 和 useReducer 的对比
代码体积 :
通常,在使用
useState
的时候,开始的时候只需要写少量的代码。 而useReducer
必须提前编写reducer
函数和需要调度的 actions。 但是在多个事件处理程序以相似的方式修改 state 的时候,useReducer
可以减少代码量。
可读性:
状态更新逻辑足够简单的时候 useState 的可读性还可以,但是一旦逻辑变动复杂起来,就会使得代码变得臃肿难以阅读。这种情况下,useReducer 可以将状态更新逻辑和 事件处理程序分离。
可调试性:
使用 useState 出现问题,必须单步执行更多代码。而使用 useReducer 的时候,可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么更新(来自哪个action)。如果所有的action都没问题,就知道问题出在 reducer本身的逻辑了。
区分:
useState 是React 中 最简单的状态管理方法。使用简单的对象来存储状态,并提供两个方法来访问和更新状态
useReducer 提供了一种更加复杂的状态管理方法。使用一个reducer 函数来处理状态更新,并提供一个 dispatch() 方法来触发状态更新。
useState
关键区别在于如何处理状态更新。 useState 使用简单的对象来存储状态。这使得它很好使用,但也可能导致性能问题,因为每次状态更新都会重新渲染组件。
useReducer
使用一个reducer函数 来处理状态更新。这使得可以更有效地处理复杂的状态更新,因为可以避免不必要的重新渲染。但是 useReducer 也更复杂,需要更多的学习和理解才能使用。
大多数情况下 useState 是足够来管理简单的状态。但是,如果需要处理复杂的状态更新,则 useReducer 可能是更好的选择
实验Immer 简化 reducers
这与平常的 state 中 修改对象和数组一样,可以使用Immer 库来简化 reducer。useImmerReducer 让可以通过 push 或者 arr[ i ] = 来修改state
import React,{} from 'react'
import { useImmerReducer } from 'use-immer'
const initInfoData = {
name : '张三',
age : 18,
sex : '男',
}
function ImmerReducerBox (){
const [userInfo, setUserInfo] = useImmerReducer(reducerFun,initInfoData)
function handleClick(obj: any){
setUserInfo(obj)
}
return (
<>
<h5>ImmerReducerBox 简化 useReducer 对象</h5>
<div>{JSON.stringify(userInfo)}</div>
<button onClick={() => handleClick({type:'setName',payload:'大豆'})}>点击设置姓名</button>
<button onClick={() => handleClick({type:'setAge',payload:'19'})}>点击设置年龄</button>
<button onClick={() => handleClick({type:'setSex',payload:'女'})}>点击设置性别</button>
</>
)
}
function reducerFun(draft: any,action :any){
switch(action.type){
case 'setName':
draft.name = action.payload
break
case 'setAge':
draft.age = action.payload
break
case 'setSex':
draft.sex = action.payload
break
default:
break
}
}
export default ImmerReducerBox
使用 Context 深层次传递参数
描述
通常使用 props 将信息从父组件传递到子组件。但是,如果必须通过许多中间件向下传递props,或者在应用中的许多组件需要相同信息,传递props 会变得十分冗长和不变。Context 允许父组件向其下层无论多深的任何组件提供信息,无需通过props 显示传递
props 传递带来的问题
props 传递 是将数据通过 UI 树显式传递到 子组件的好方法。
但是当需要在组件树深层递参数以及需要在组件间复用相同的参数时,传递 props 就会变得很麻烦。最近的根节点的父组件可能离需要的组件很远,状态提升 到太高的层级会导致 逐层传递 props 的情况
Context的基本使用
import React,{createContext,useContext} from 'react'
const initInfoData = {
name : '张三',
age : 18,
sex : '男',
}
function Childrens() {
return (
<>
<div>第一个子元素</div>
<Childrens2 />
</>
)
}
function Childrens2() {
const aaa = useContext(conTextS)
return (
<div>第二个子元素{JSON.stringify(aaa)}</div>
)
}
const conTextS = createContext(initInfoData)
function ContextBox (){
return (
<>
<div>1232222</div>
<Childrens />
</>
)
}
export default ContextBox