1.数据驱动
useState
useState
是一个 React Hook,允许函数组件在内部管理状态。
组件通常需要根据交互更改屏幕上显示的内容,例如点击某个按钮更改值,或者输入文本框中的内容,这些值被称为状态值也就是(state)
使用方法
useState
接收一个参数,即状态的初始值,然后返回一个数组,其中包含两个元素:
- 该状态变量 当前的
state
,最初设置为你提供的 初始化state
。 - set 函数,它允许你在响应交互时将
state
更改为任何其他值。
ts
const [state, setState] = useState(initialState) // state 是状态变量,useState 是修改器
注意事项
useState
是一个 Hook,因此你只能在 组件的顶层
或自己的 Hook
中调用它。你不能在循环或条件语句中调用它。
在严格模式中,React 将 两次调用初始化函数
,以 帮你找到意外的不纯性。这只是开发时的行为,不影响生产
用法
基本数据类型
ts
// 基本数据类型
const [count, setCount] = useState(0); //数字 布尔值 null undefined 都可以直接赋值 一样的
const [str, setStr] = useState('aaa'); //
const updateData = () => {
setCount(count + 1); // 直接传值
setStr((pre) => pre + 'c'); // 函数式更新(推荐用于依赖上一次状态的情况)
};
数组
在React中你需要将数组视为只读的,不可以直接修改原数组,例如:不可以调用 arr.push()
arr.pop()
等方法。
下面是常见数组操作的参考表。当你操作 React state 中的数组时,你需要避免使用左列的方法,而首选右列的方法:
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) |
---|---|
添加元素 push,unshift | concat,[...arr] 展开语法(例子) |
删除元素 pop,shift,splice | filter,slice(例子) |
替换元素 splice,arr[i] = ... 赋值 | map(例子) |
排序 reverse,sort | 先将数组复制一份(例子) |
ts
const [arr, setArr] = useState([1, 2, 3]); // 数组
const updateArray = () => {
// 在React中你需要将数组视为只读的,不可以直接修改原数组,例如:不可以调用 arr.push() arr.pop() 等方法。
// 添加元素:避免使用push,unshift 推荐使用concat,[...arr] 展开语法
// arr.push(4); // 这样写视图不会更新,
setArr([...arr, 4]);
// 删除元素:避免使用pop,shift,splice 推荐使用filter,[...arr] 展开语法
setArr(arr.filter((item) => item !== 2));
// 替换元素:避免使用 splice,arr[i] = ... 赋值, 推荐使用map
setArr(arr.map((item) => (item === 2 ? 9 : item)));
// 排序、旋转等:避免使用 sort,reverse 推荐先将数组复制一份
const nextArr = [...arr];
nextArr.sort((a, b) => b - a);
setArr(nextArr);
// 指定位置插入元素
let startIndex = 0;
let endIndex = 2;
setArr([...arr.slice(startIndex, endIndex), 2.5, ...arr.slice(endIndex)]);
};
对象
useState可以接受一个函数,可以在函数里面编写逻辑,初始化值,注意这个只会执行一次,更新的时候就不会执行了。
在使用setObject的时候,可以使用Object.assign合并对象 或者 ... 合并对象,不能单独赋值,不然会覆盖原始对象。
tsx
let [obj, setObj] = useState(() => {
return {
name: '张三',
age: 18,
};
});
// useState可以接受一个函数,可以在函数里面编写逻辑,初始化值,注意这个只会执行一次,更新的时候就不会执行了。
const updateObj = () => {
// 在使用setObject的时候,可以使用Object.assign合并对象 或者 ... 合并对象,不能单独赋值,不然会覆盖原始对象。
// 不要像下面这样改变一个对象:
// setObj({
// name: '张三',
// });
setObj({
...obj,
name: '李四',
});
//setObject(Object.assign({}, obj, { age: 26 })) 第二种写法
};
函数
React 只在初次渲染时保存初始状态,后续渲染时将其忽略。
tsx
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos());
// ...
尽管 createInitialTodos()
的结果仅用于初始渲染,但你仍然在每次渲染时调用此函数。如果它创建大数组或执行昂贵的计算,这可能会浪费资源。
为了解决这个问题,你可以将它 作为初始化函数传递给 useState
:
tsx
function TodoList() {
const [todos, setTodos] = useState(createInitialTodos);
// ...
请注意,你传递的是 createInitialTodos
函数本身 ,而不是 createInitialTodos()
调用该函数的结果。如果将函数传递给 useState
,React 仅在初始化期间调用它。
React 在开发模式下可能会调用你的 初始化函数 两次,以验证它们是否是 纯函数。
更新机制
异步机制
useState set函数是异步更新的
tsx
const [index, setIndex] = useState(0);
const heandleClick = () => {
setIndex(1); // 异步代码
console.log(index); // 此时会打印0,因为是同步代码
};
此时index应该打印1,但是还是0,因为我们正常编写的代码是同步的,所以会先执行,而set函数是异步的所以后执行,这么做是为了性能优化,因为我们要的是结果而不是过程。
内部机制
当我们多次以相同的操作更新状态时,React 会进行比较,如果值相同,则会屏蔽后续的更新行为。自带防抖
的功能,防止频繁的更新。
tsx
import { useState } from "react"
function App() {
let [index, setIndex] = useState(0)
const heandleClick = () => {
setIndex(index + 1) //1
setIndex(index + 1) //1
setIndex(index + 1) //1
console.log(index,'index')
}
return (
<>
<h1>Index:{index}</h1>
<button onClick={heandleClick}>更改值</button>
</>
)
}
export default App
结果是1并不是3,因为setIndex(index + 1)
的值是一样的,后续操作被屏蔽掉了,阻止了更新。
为了解决这个问题,你可以向setIndex
传递一个更新函数,而不是一个状态。
tsx
import { useState } from "react"
function App() {
let [index, setIndex] = useState(0)
// 按照惯例,通常将待定状态参数命名为状态变量名称的第一个字母,例如 prevIndex 或者其更清楚的名称。
const heandleClick = () => {
setIndex(prevIndex => prevIndex + 1) //1
setIndex(prevIndex => prevIndex + 1) //2
setIndex(prevIndex => prevIndex + 1) //3
}
return (
<>
<h1>Index:{index}</h1>
<button onClick={heandleClick}>更改值</button>
</>
)
}
export default App
- index => index + 1 将接收 0 作为待定状态,并返回 1 作为下一个状态。
- index => index + 1 将接收 1 作为待定状态,并返回 2 作为下一个状态。
- index => index + 1 将接收 2 作为待定状态,并返回 3 作为下一个状态。
现在没有其他排队的更新,因此 React 最终将存储 3 作为当前状态。
useReducer
useReducer
是一个 React Hook,它允许你向组件里面添加一个 reducer。
使用方法
tsx
const [state, dispatch] = useReducer(reducer, initialArg, init?)
参数:
reducer
是一个处理函数,用于更新状态, reducer 里面包含了两个参数,第一个参数是state
,第二个参数是action
。reducer
会返回一个新的state
。initialArg
是state
的初始值。init
是一个可选的函数,用于初始化state
,如果编写了init函数,则默认值使用init函数的返回值,否则使用initialArg
。
返回值:
useReducer 返回一个由两个值组成的数组:
-
当前的 state:初次渲染时,它是 init(initialArg) 或 initialArg (如果没有 init 函数)。
-
dispatch 函数:用于更新 state 并触发组件的重新渲染。
tsx
import { useReducer } from 'react';
//根据旧状态进行处理 oldState,处理完成之后返回新状态 newState
//reducer 只有被dispatch的时候才会被调用 刚进入页面的时候是不会执行的
//oldState 任然是只读的
function reducer(oldState, action) {
// ...
return newState;
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42,name:'张三' });
// ...
计数器案例
初始状态 (initialState):
tsx
// initialArg 是 state 的初始值。
const initialState = { count: 0, name: '张三' };
type Stage = typeof initialState;
初始化函数init:
tsx
// init 是一个可选的函数,用于初始化 state,如果编写了init函数,则默认值使用init函数的返回值,否则使用initialArg。
const init = () => {
initialState.count = 10; // 调用初始方法,设置count初始值为0
return initialState;
};
reducer 函数:
tsx
interface Action {
type: 'increment' | 'decrement';
}
// reducer 是一个处理函数,用于更新状态, reducer 里面包含了两个参数,第一个参数是 state,第二个参数是 action。reducer 会返回一个新的 state。
const reducer = (stage: Stage, action: Action) => {
switch (action.type) {
case 'add':
return { ...stage, count: stage.count++ };
case 'sub':
return { ...stage, count: stage.count-- };
default:
throw new Error(); //抛出错误处理未预期的action类型
}
};
- reducer 是一个用来根据不同的 action 来更新状态的纯函数。
- 它接收当前状态 (state) 和一个动作对象 (action),根据 action.type 来决定如何更新 state。
- 如果 action.type 是 'increment',则 count 增加 1;如果是 'decrement',则 count 减少 1。
- 如果 action.type 不匹配任何已定义的情况,则抛出一个错误。 App 组件:
tsx
const App = () => {
const [stage, dispatch] = useReducer(reducer, initialState, init);
return (
<>
<div>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<span>{stage.count}</span>
<span>{stage.name}</span>
</div>
</>
);
};
export default App;
- 当点击 "-" 按钮时,调用 dispatch({ type: 'decrement' }),使 count 减少。
- 当点击 "+" 按钮时,调用 dispatch({ type: 'increment' }),使 count 增加。
useSyncExternalStore
useSyncExternalStore 是 React 18 引入的一个 Hook,用于从外部存储(例如状态管理库、浏览器 API 等)获取状态并在组件中同步显示。这对于需要跟踪外部状态的应用非常有用。
场景
- 订阅外部 store 例如(redux,Zustand
德语
) - 订阅浏览器API 例如(online,storage,location)等
- 抽离逻辑,编写自定义hooks
- 服务端渲染支持
用法
tsx
const res = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
- subscribe:用来订阅数据源的变化,接收一个回调函数,在数据源更新时调用该回调函数。
- getSnapshot:获取当前数据源的快照(当前状态)。
- getServerSnapshot?:在服务器端渲染时用来获取数据源的快照。
返回值:该 res 的当前快照,可以在你的渲染逻辑中使用
tsx
const subscribe = (callback: () => void) => {
// 订阅
callback()
return () => {
// 取消订阅
}
}
const getSnapshot = () => {
return data
}
const res = useSyncExternalStore(subscribe, getSnapshot)
案例
1.订阅浏览器Api 实现自定义hook(useStorage)
我们实现一个useStorage Hook,用于订阅 localStorage 数据。这样做的好处是,我们可以确保组件在 localStorage 数据发生变化时,自动更新同步。
实现代码
我们将创建一个 useStorage Hook,能够存储数据到 localStorage,并在不同浏览器标签页之间同步这些状态。此 Hook 接收一个键值参数用于存储数据的键名,还可以接收一个默认值用于在无数据时的初始化。
在 hooks/useStorage.ts 中定义 useStorage Hook:
ts
import { useSyncExternalStore } from 'react';
/**
*
* @param key 存储到localStorage 的key
* @param defaultValue 默认值
*/
export const useStorage = (key: any, defaultValue: any) => {
const subscribe = (callback: () => void) => {
// 订阅storage事件
window.addEventListener('storage', callback);
return () => {
// 取消订阅storage事件
window.removeEventListener('storage', callback);
};
};
//从localStorage中获取数据 如果读不到返回默认值
const getSnapshot = () => {
return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : defaultValue;
};
//修改数据
const setStore = (value: any) => {
localStorage.setItem(key, JSON.stringify(value));
window.dispatchEvent(new StorageEvent('storage')); //手动触发storage事件
};
//返回数据
const res = useSyncExternalStore(subscribe, getSnapshot);
return [res, setStore];
};
在 App.tsx 中,我们可以直接使用 useStorage,来实现一个简单的计数器。值会存储在 localStorage 中,并且在刷新或其他标签页修改数据时自动更新。
jsx
import { useStorage } from '../hooks/useStorage';
export const App = () => {
const [count, setVal] = useStorage('count', 1);
return (
<>
<div>
<button onClick={() => setVal(count + 1)}>加一</button>
<button onClick={() => setVal(count - 1)}>减一</button>
<span>{count}</span>
</div>
</>
);
};
export default App;
效果演示
-
值的持久化:点击按钮增加 val,页面刷新后依然会保留最新值。
-
跨标签页同步:在多个标签页打开该应用时,任意一个标签页修改 val,其他标签页会实时更新,保持同步状态。
2. 订阅history实现路由跳转
实现一个简易的useHistory Hook,获取浏览器url信息 + 参数
ts
import { useSyncExternalStore } from 'react';
//history api实现push,replace页面跳转,监听history变化
export const useHistory = () => {
const subscribe = (callback: () => void) => {
// 订阅浏览器api 监听history变化
// history底层:popstate
// hash底层: hashchange
window.addEventListener('popstate', callback);
window.addEventListener('hashchange', callback);
return () => {
// 取消订阅storage事件
window.removeEventListener('popstate', callback);
window.removeEventListener('hashchange', callback);
};
// popstate只能监听浏览器前进后退按钮的点击事件,不能监听pushState,replaceState的变化,需要手动触发
};
const getSnapshot = () => {
return [window.location.href];
};
const push = (url: string) => {
window.history.pushState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate')); //手动触发storage事件
};
const replace = (url: string) => {
window.history.replaceState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate')); //手动触发storage事件
};
const res = useSyncExternalStore(subscribe, getSnapshot);
return [res, push, replace] as const; // 将数组字面量转换为 只读元组 类型
};
使用 useHistory Hook
让我们在组件中使用这个 useHistory Hook,实现基本的前进、后退操作以及程序化导航。
tsx
import { useHistory } from '../hooks/useHistory';
export const App = () => {
const [history, push, replace] = useHistory();
return (
<>
<div>
<button onClick={() => push('/AA')}>跳转</button>
<button onClick={() => replace('/CCC')}>替换</button>
<span>{history}</span>
</div>
</>
);
};
export default App;
效果演示
- history:这是 useHistory 返回的当前路径值。每次 URL 变化时,useSyncExternalStore 会自动触发更新,使 history 始终保持最新路径。
- push 和 replace:点击"跳转"按钮调用 push("/AA"),会将 /AA 推入历史记录;点击"替换"按钮调用 replace("/CCC"),则会将当前路径替换为 /CCC。
注意事项
如果 getSnapshot
返回值不同于上一次,React 会重新渲染组件。这就是为什么,如果总是返回一个不同的值,会进入到一个无限循环,并产生这个报错。
ts
Uncaught (in promise) Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
ts
function getSnapshot() {
return myStore.todos; //object
}
这种写法每次返回了对象的引用,即使这个对象没有改变,React 也会重新渲染组件。
如果你的 store 数据是可变的,getSnapshot
函数应当返回一个它的不可变快照。这意味着 确实 需要创建新对象,但不是每次调用都如此。而是应当保存最后一次计算得到的快照,并且在 store 中的数据不变的情况下,返回与上一次相同的快照。如何决定可变数据发生了改变则取决于你的可变 store。
ts
function getSnapshot() {
if (myStore.todos !== lastTodos) {
// 只有在 todos 真的发生变化时,才更新快照
lastSnapshot = { todos: myStore.todos.slice() };
lastTodos = myStore.todos;
}
return lastSnapshot;
}