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>
  );
});
相关推荐
你挚爱的强哥16 分钟前
【sgCreateCallAPIFunctionParam】自定义小工具:敏捷开发→调用接口方法参数生成工具
前端·javascript·vue.js
喝旺仔la21 分钟前
Element 表格相关操作
javascript·vue.js·elementui
米老鼠的摩托车日记24 分钟前
【vue element-ui】关于删除按钮的提示框,可一键复制
前端·javascript·vue.js
forwardMyLife24 分钟前
element-plus的菜单组件el-menu
javascript·vue.js·elementui
猿饵块1 小时前
cmake--get_filename_component
java·前端·c++
好多吃的啊1 小时前
背景图鼠标放上去切换图片过渡效果
开发语言·javascript·ecmascript
大表哥61 小时前
在react中 使用redux
前端·react.js·前端框架
Passion不晚1 小时前
打造民国风格炫酷个人网页:用HTML和CSS3传递民国风韵
javascript·html·css3
十月ooOO1 小时前
【解决】chrome 谷歌浏览器,鼠标点击任何区域都是 Input 输入框的状态,能看到输入的光标
前端·chrome·计算机外设
qq_339191141 小时前
spring boot admin集成,springboot2.x集成监控
java·前端·spring boot