React Hook 指南(下)

useRef

用法

useRef 主要用于在函数组件中访问和操作 DOM 元素 ,以及保留某些值的引用,以避免它们在组件重新渲染时被重置

创建 ref 对象:使用 useRef 创建一个 ref 对象,可以像这样初始化:initialValue 是可选的,通常用于初始化 ref 的值。

ini 复制代码
const myRef = useRef(initialValue);

访问 ref 的值:通过 myRef.current属性访问 ref 的值。这个属性会始终包含对 ref 对象当前值的引用。

ini 复制代码
const element = myRef.current; // 访问 DOM 元素的引用

案例

通过useRef绑定DOM

ini 复制代码
// 1.用法一:通过useRef来绑定DOM
const titleRef = useRef();
const inputRef = useRef();

然后在对应的JSX元素处进行绑定

xml 复制代码
<h2 ref={titleRef}>标题</h2>
<input type="text" ref={inputRef} />

此时就可以编写一些函数来访问和操作DOM,例如下方的打印出DOM信息以及获取文本输入框的焦点

scss 复制代码
function showTitleDom() {
  console.log(titleRef.current);
}
function getInput() {
  inputRef.current.focus();
}

通过按钮绑定事件,用于查看标题dom和获取文本框焦点

scss 复制代码
<button onClick={(e) => showTitleDom()}>查看标题dom</button>
<button onClick={(e) => getInput()}>获取文本框焦点</button>

useImperativeHandle

forwardRef用法

forwardRef将函数组件内部的DOM元素暴露给父组件

forwardRef(render) ,forwardRef接受一个render函数用于渲染子组件

render函数的形式通常如下:

javascript 复制代码
forwardRef((props, ref) => {
  // 在组件内部使用 ref
  // ...
  return <div ref={ref}>Hello, World!</div>;
});

因此forwardRef可以创建一个接受 ref 作为参数的子组件。父组件可以通过 ref访问子组件内部的 DOM 元素

可以举一个具体的例子,页面的具体构成如下

这是一个通过forwardRef包裹的子组件Home

javascript 复制代码
const Home = memo(
  forwardRef((props, ref) => {
    return (
      <div>
        <h3>Home组件</h3>
        <input type="text" ref={ref} />
      </div>
    );
  })
);

在父组件中,就可以通过将生成的ref实例传递给子组件,以在父组件中访问子组件Home的DOM元素。因此整个使用过程可以理解为,使用forwardRef将ref转发给子组件,子组件获取到父组件创建的ref,绑定到自己的某个DOM元素上

ini 复制代码
const App2 = memo(() => {
  const inputRef = useRef();
  useEffect(() => {
    console.log(inputRef.current);
  }, []);
  return (
    <div>
      <h2>App2组件</h2>

      <Home ref={inputRef} />
    </div>
  );
});

useImperativeHandle用法

上一小节提到的forwardRef可以实现将子组件的DOM元素暴露给父组件,但是有些情况下不希望完全将DOM元素完全暴露给父组件,而仅仅是希望父组件可以执行特定的操作,而不是任意操作DOM。这种场景可以考虑使用useImperativeHandle


useImperativeHandle 允许定义在组件外部可访问的实例值或方法,并将它们暴露给父组件或外部代码

useImperativeHandle 接受两个参数:

  • 第一个参数是 ref 对象
  • 第二个参数是一个函数,这个函数返回一个对象,其中包含子组件要暴露给外部的属性和方法。

useImperativeHandle会将传入的ref和第二个参数返回的对象进行绑定,因此父组件在使用ref.current时,实际调用的是第二个参数返回的对象

案例

例如想要定义一个子组件,展示一个文本框。并且想要为父组件提供 文本框聚焦、以及修改文本框内容的两个方法

useImperativeHandle第二个参数传入回调函数,其返回的对象就是对应传入ref的current

javascript 复制代码
const Home = memo(
  forwardRef((props, ref) => {
    const textRef = useRef();
    useImperativeHandle(ref, () => {
      // 暴露给父组件的操作,全放在这个对象里
      return {
        focus() {
          textRef.current.focus();
        },
        changeContent() {
          textRef.current.value = Math.random();
        },
      };
    });
    return (
      <div>
        <h2>Home组件</h2>
        <input type="text" ref={textRef} />
      </div>
    );
  })
);

因此可以在父组件中调用上述定义的focus和setValue方法。

为子组件传入inputRef,那么inputRef.current中就包含了focus方法和changeContent方法。

javascript 复制代码
const App3 = memo(() => {
  const inputRef = useRef();
  console.log(inputRef.current);
  function handleFocus() {
    inputRef.current.focus();
  }
  function handleContent() {
    inputRef.current.changeContent();
  }
  return (
    <div>
      <h2>App组件</h2>
      <Home ref={inputRef} />
      <button onClick={(e) => handleFocus()}>文本框聚焦</button>
      <button onClick={(e) => handleContent()}>修改文本框内容</button>
    </div>
  );
});

useLayoutEffect

useLayoutEffect 的基本用法与 useEffect 类似,都接受两个参数:一个回调函数和一个依赖数组。当依赖数组中的值发生变化时,useLayoutEffect 的回调函数会被调用。

useLayoutEffect与useEffect类似,区别在于:

  • useEffect会在浏览器执行绘制之后异步触发
  • useLayoutEffect会在渲染的内容更新到DOM之前执行,阻塞DOM更新

useLayoutEffect意义是可以在 DOM 更新之前执行某些操作,通常用于需要立即获取或计算 DOM 元素属性的情况。可以防止闪烁或不一致的 UI 渲染问题。

案例

当点击按钮时,会将count置为0,在案例中,我们假设count是不可以为0的。因此需要保证count为0时设置为别的数值,这时可以用useLayoutEffect在将count绘制在浏览器之前进行修改。

scss 复制代码
const App = memo(() => {
  const [count, setCount] = useState(10);
  //   在count渲染到屏幕前,发现count为0则进行修改
  useLayoutEffect(() => {
    if (count === 0) {
      setCount(Math.random() + 10);
    }
  }, [count]);
  return (
    <div>
      <h2>count: {count}</h2>
      <button onClick={(e) => setCount(0)}>设置count</button>
    </div>
  );
});

需要注意的是,如果此时考虑使用useEffect进行count修改,则会出现屏幕闪烁问题,即先出现0,再更换为别的数值

scss 复制代码
useEffect(() => {
  if (count === 0) {
    setCount(Math.random() + 10);
  }
}, [count]);

自定义hook

自定义hook本质是一种封装可重用逻辑的方式。以便不同组件都可以共享该逻辑。

自定义 Hook 的名称通常以 "use" 开头,在自定义hook内部,可以使用React内置hook,也可以编写自己创建的函数。

抽取Context

假如某业务需要在多个组件都获取同样的Context信息,那么可以考虑将获取context的逻辑封装成一个hook

javascript 复制代码
import { useContext } from "react";
import { ThemeContext, UserContext } from "../../04_useContext的使用/context";
function useFetchContext() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
  return [user, theme];
}
export default useFetchContext;

那么在所有组件都可以通过一个hook函数来获取到需要的context信息

监听窗口滚动位置

编写一个自定义hook获取页面滚动的水平和垂直位置,使用这个自定义 Hook,可以在任何组件中获取到页面的滚动位置,而无需在每个组件中重复编写滚动事件的监听逻辑。

scss 复制代码
import { useEffect, useState } from "react";

function useScrollPosition() {
  const [scrollX, setScrollX] = useState(0);
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    function handleScroll() {
      // console.log(window.scrollX, "-", window.scrollY);
      setScrollX(window.scrollX);
      setScrollY(window.scrollY);
    }
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  return [scrollX, scrollY];
}
export default useScrollPosition;

Redux Hooks

useSelector

useSelector是 React 函数组件中访问 Redux store 中的状态的一种方式,使用时需要为useSelector传入一个回调函数,就可以从 Redux store 中选择(select)某些数据进行渲染或其他操作。

基本用法如下:

首先要确保所编写的函数组件位于 Redux Provider 的内部 ,这样它才能访问到 Redux store。因此往往在使用Redux的时候先用Provider包裹App组件。

在以下案例中,我们获取到了state状态中counter子模块的count属性值。并且注意我的写法,这种方式传入的回调函数,返回值是一个对象,比较适合从redux store中取出多个数据。

(state) => ({ count: state.counter.count,})

javascript 复制代码
import React, { memo } from "react";
import {  useSelector } from "react-redux";
const App = memo((props) => {
  // 获取redux store中的数据
  const { count } = useSelector((state) => ({
    count: state.counter.count,
  }));
  return (
    <div>
      <h2>当前计数:{count}</h2>
    </div>
  );
});
export default App;

useSelector浅层比较

在下列应用场景中,子组件Home中使用useSelector获取到message,并进行渲染,父组件同时渲染count。子组件Home通过memo进行包裹。

javascript 复制代码
const Home = memo((props) => {
  const { message } = useSelector((state) => ({
    message: state.counter.message,
  }));
  console.log("Home render");
  return (
    <div>
      <h2>Home message:{message}</h2>
    </div>
  );
});

如果更改count,会导致父组件重新渲染,然而Home组件也会被重新渲染。但是Home组件被memo包裹,如果props没更新的话,是应该不会重新渲染的。这是由于useSelector监听state的变化,由于count发生更新,导致state发生变化。即使Home组件中仅仅使用了state中的message,仍然会重新渲染。

因此可以考虑为useSelector传入shallowEqual,shallowEqual 是 React Redux 提供的浅层比较函数,它会比较对象的属性值,只有当属性值发生实际变化时才认为对象发生了变化。使用 shallowEqual 可以确保只有当 message 属性的内容发生变化时,才会触发 Home 组件的重新渲染,而不受 count 属性的变化影响。

优化之后,更新count的值,仅仅父组件进行重新渲染,子组件Home不会渲染

useDispatch

useDispatch 是 React Redux 提供的 hook,用于在函数组件中获取 Redux store 的 dispatch 函数,从而可以在组件中派发(dispatch)Redux actions。

以下案例就通过useDispatch获取dispatch函数,并在特定的事件(比如按钮点击)发生时,派发相应的 Redux action。

javascript 复制代码
import React, { memo } from "react";

import { incrementAction, decrementAction } from "./store/modules/counter";
import { useDispatch, useSelector } from "react-redux";
const App = memo((props) => {
  // 获取redux store中的数据
  const { count } = useSelector((state) => ({
    count: state.counter.count,
  }));

  const dispatch = useDispatch();
  function handleCount(num, isAdd) {
    if (isAdd) {
      dispatch(incrementAction(num));
    } else dispatch(decrementAction(num));
  }
  return (
    <div>
      <h2>当前计数:{count}</h2>
      <button onClick={(e) => handleCount(1, true)}>+1</button>
      <button onClick={(e) => handleCount(10, false)}>-10</button>
    </div>
  );
});
export default App;

useTransition

用法

官网文档这么描述该hook,useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。

React中,transition是一个特定的渲染状态,React对处于transition状态的更新操作进行优化处理。useTransition 的作用是实现在进行比较耗时的更新状态操作时不阻塞UI的交互。

很多状态更新操作涉及大量计算或数据处理,往往比较耗费时间,会阻塞用户界面。使用useTransition管理这部分操作后,界面在进行状态更新的同时仍然能够响应用户的交互操作

可以理解为useTransition可以将部分任务的更新优先级变低,可以稍后进行更新。useTransition 返回一个由两个元素组成的数组:

  • isPending:是否存在待处理的 transition(过渡)。如果 isPending 的值为 true,表示当前存在尚未完成的过渡任务
  • startTransition 函数,使用 startTransition 函数将某些状态更新操作标记为 transition。通过调用 startTransition 函数,React 在处理状态为transition的任务时会进行特殊的优化。

简单的演示案例如下,setData(newData);是一个非常耗时的更新操作,执行过程中,界面的UI交互会变得很卡顿,因此可以将该部分内容包裹在startTransition函数中,并且通过isPending来控制渲染,更新操作未完成时显示Loading组件。

javascript 复制代码
import React, { useState, useTransition } from 'react';

function MyComponent() {
  const [isPending, startTransition] = useTransition();
  const [data, setData] = useState(null);

  const fetchData = () => {
    startTransition(() => {
      // 比较耗时的更新操作
      setData(newData);
    });
  };

  return (
    <div>
      <button onClick={fetchData}>Fetch Data</button>
      {isPending ? <LoadingSpinner /> : <DataComponent data={data} />}
    </div>
  );
}

以下是对该hook的进一步说明,以论述useTransition的作用。

  • 标记为 transition 的状态更新将会被其他状态更新打断。例如,在 transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,然后再重新启动对图表组件的渲染工作。
  • 如果希望某个状态更新操作被 useTransition 所追踪,那么应该使用 useState 返回的 setter 函数(通常形如 setSomeState)来进行状态更新。

案例

当前网页渲染10000个公司名的列表,并且在上方有一个输入框,当输入内容时,会查找包含关键词的公司名。例如输入ot,会显示包含ot的公司名

当删除t时,由于数据量过大,在删除t时输入框的交互会变的卡顿,因为 渲染新的公司列表输入框的交互操作会同时进行,这时可以考虑采用useTransition进行优化

setCompanyNames(filterCompanyNames)操作放在setTransition内部,更新公司列表的相关逻辑会标记为transition状态,此时输入框相关的操作会打断更新公司列表操作。

pending会标示状态为transition的任务是否完成,如果没完成则会显示Loading效果

javascript 复制代码
const App = memo(() => {
  const [companyNames, setCompanyNames] = useState(arrayData);
  const [pending, setTransition] = useTransition();
  function valueChangeHandle(event) {
    const keyword = event.target.value;
    // 将一部分比较复杂的逻辑延后执行
    setTransition(() => {
      // 筛选出包含关键词的数组项
      const filterCompanyNames = arrayData.filter((item) =>
        item.includes(keyword)
      );
      setCompanyNames(filterCompanyNames);
    });
  }
  return (
    <div>
      <input type="text" onInput={(e) => valueChangeHandle(e)} />
      <h2>公司列表 {pending && <span>pending...</span>}</h2>
      <ul>
        {companyNames.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});

export default App;

useDeferredValue

用法

useDeferredValue提供一种延迟更新 UI 的机制。当有一个新值需要渲染时,React 会在后台异步地进行渲染,确保首先使用旧值进行渲染,避免阻塞用户界面。然后,当渲染完成后,React 再使用新值进行一次渲染,保持 UI 的同步性。

  • 参数 value: 可以将任何类型的值传递给 useDeferredValue。这个值是要在 UI 中延迟更新的数据。
  • 返回值: 返回值在组件的初始渲染期间与传递的初始值相同。但是在组件更新时,React 会有两次渲染尝试:
    • 第一次尝试(返回旧值): React 首先尝试使用之前的(旧的)值进行重新渲染,因此在这个阶段 useDeferredValue 返回的值仍然是旧的。
    • 第二次尝试(返回更新后的值): 在后台,React 使用你提供的新值进行另一个重新渲染。在这个阶段,useDeferredValue 返回的值将会是更新后的值。

基本用法如下:

javascript 复制代码
const App = memo(() => {
  const [companyNames, setCompanyNames] = useState(arrayData);
  // 设置延迟更新的值
  const deferCompanyNames = useDeferredValue(companyNames);
  
  return (
    <div>
      <h2>公司列表 </h2>
      <ul>
        {deferCompanyNames.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});

export default App;

补充:

useDeferredValue 本身不会引起任何固定的延迟。一旦 React 完成原始的重新渲染,它会立即开始使用新的延迟值处理后台重新渲染。

任何由事件(例如用户输入)引起的更新都会中断后台重新渲染,并被优先处理。这确保了用户的输入和交互在应用中的体验是即时的,不会因为延迟而感觉迟钝。

案例

一个页面展示10000条公司列表,在文本框输入关键词,会进行列表更新。由于数量过多,在更改关键词时输入操作会卡顿,因此可以考虑将要渲染的列表数据通过useDeferredValue包装,那么发生文本框输入事件时,就会中断后台渲染公司列表数据,提升交互体验。

javascript 复制代码
const App = memo(() => {
  const [companyNames, setCompanyNames] = useState(arrayData);
  const deferCompanyNames = useDeferredValue(companyNames);
  function valueChangeHandle(event) {
    const keyword = event.target.value;
    // 筛选出包含关键词的数组项
    const filterCompanyNames = arrayData.filter((item) =>
      item.includes(keyword)
    );
    setCompanyNames(filterCompanyNames);
  }
  return (
    <div>
      <input type="text" onInput={(e) => valueChangeHandle(e)} />
      <h2>公司列表 </h2>
      <ul>
        {deferCompanyNames.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});
相关推荐
旧味清欢|7 分钟前
关注分离(Separation of Concerns)在前端开发中的实践演进:从 XMLHttpRequest 到 Fetch API
javascript·http·es6
热爱编程的小曾24 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin36 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
-代号95271 小时前
【JavaScript】十四、轮播图
javascript·css·css3
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
QTX187302 小时前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员