React常见Hooks使用

1.数据驱动

useState

useState 是一个 React Hook,允许函数组件在内部管理状态。

组件通常需要根据交互更改屏幕上显示的内容,例如点击某个按钮更改值,或者输入文本框中的内容,这些值被称为状态值也就是(state)

使用方法

useState 接收一个参数,即状态的初始值,然后返回一个数组,其中包含两个元素:

  1. 该状态变量 当前的 state,最初设置为你提供的 初始化 state
  2. 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
  1. index => index + 1 将接收 0 作为待定状态,并返回 1 作为下一个状态。
  2. index => index + 1 将接收 1 作为待定状态,并返回 2 作为下一个状态。
  3. index => index + 1 将接收 2 作为待定状态,并返回 3 作为下一个状态。

现在没有其他排队的更新,因此 React 最终将存储 3 作为当前状态。

useReducer

useReducer 是一个 React Hook,它允许你向组件里面添加一个 reducer

使用方法

tsx 复制代码
const [state, dispatch] = useReducer(reducer, initialArg, init?)

参数:

  1. reducer 是一个处理函数,用于更新状态, reducer 里面包含了两个参数,第一个参数是 state,第二个参数是 actionreducer 会返回一个新的 state
  2. initialArgstate 的初始值。
  3. 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 等)获取状态并在组件中同步显示。这对于需要跟踪外部状态的应用非常有用。

场景

  1. 订阅外部 store 例如(redux,Zustand德语)
  2. 订阅浏览器API 例如(online,storage,location)等
  3. 抽离逻辑,编写自定义hooks
  4. 服务端渲染支持

用法

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;
}
相关推荐
@大迁世界7 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路16 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug19 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213821 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中43 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全
前端工作日常1 小时前
我学习到的AG-UI的概念
前端