第6期 - 小桥流水

封面图来源于苏州平江路,感受真正的小桥流水人家。

寒山寺

技术分享-hook相关

useContext、useState、use

useContext 跨组件深层次传值, 读取和订阅组件中的 context use 读取类似于 Promise 或 context 的资源的值。会在数据获取到后重新渲染组件, 可在条件语句中调用。

useContext: React 是单向数据流,如果组件之间有嵌套引用的关系如 A 引用 B,B 引用 C,像套娃一样的关系, 在没有状态管理的情况下,只能通过 props 一层一层的进行传递。实现组件值的传递,不需要经过中间组件 Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。总是在调用它的组件 上面 寻找最近的 provider

import { createContext, useContext } from 'react';

const ThemeContext = createContext(null);
// 在 MyApp 顶层包装了 ThemeContext.Provider 使得 Form 内可以使用 ThemeContext 值
export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}

// 更改 Context 值
const ThemeContext = createContext('light');

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <>
      <ThemeContext.Provider value={theme}>
        <Form />
      </ThemeContext.Provider>
      <Button onClick={() => {
        setTheme(theme === 'dark' ? 'light' : 'dark');
      }}>
        Toggle theme
      </Button>
    </>
  )
}

use:

  1. 使用 use 读取 context, 即异步组件内容的读取
  2. 将数据从服务器流式传递给客户端
"use client";
import { use, Suspense } from "react";

const ThemeContext = createContext(null); // 模拟假设组件内容
function Panel({ title, children }) {
  const theme = use(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}
export function MessageContainer({ messagePromise }) {
  return (
    <Suspense fallback={<p>⌛Downloading message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

useEffect

useEffect 返回 undefined, 只能在 组件的顶层 或自己的 Hook 中调用。返回一个 清理函数,初次挂载会执行内含的函数,当依赖项发生变化时,先执行 return 的内容进行上一次的清理,再执行内含的函数

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
   // 内含的函数
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
    // 清理函数
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]); // 依赖项
}

useMemo、useCallback

useMemo 缓存定义的函数计算的结果,当依赖项没有发生变化则取上一次值,变化则重新执行 useCallback 缓存函数。返回一个函数,依赖发生变化执行传入函数,否则拿到上一次传入的函数缓存

useCallback 用法意义:

  1. 将其作为 props 传递给包装在 [memo] 中的组件。如果 props 未更改,则希望跳过重新渲染。缓存允许组件仅在依赖项更改时重新渲染。
  2. 传递的函数可能作为某些 Hook 的依赖
  3. 避免不必要地更新 Effect
import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
  const product = useData('/product/' + productId);

  const requirements = useMemo(() => { //调用函数并缓存结果
    return computeRequirements(product);
  }, [product]);

  const handleSubmit = useCallback((orderDetails) => { // 缓存函数本身,避免重复渲染
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  // 与 useEffect 套用
const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // ✅ 仅当 roomId 更改时更改

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // ✅ 仅当 createOptions 更改时更改

  return (
    <div className={theme}>
      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
    </div>
  );
}

useDeferredValue、useLayoutEffect

useDeferredValue 延迟更新 UI,中断重新渲染,如果 value 有新的更新,React 会从头开始重新启动后台渲染。即 在数据加载完成前,页面 UI 暂停更新,将会使用原来的旧值(如:用户在输入框中的输入速度比接收延迟值的图表重新渲染的速度快,那么图表只会在用户停止输入后重新渲染)

useLayoutEffect 在浏览器重新绘制屏幕之前进行布局测量

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
    console.log('Measured tooltip height: ' + height);
  }, []);

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

与防抖节流有什么区别? 防抖:用户停止输入一段时间后更新 节流:每隔一段时间进行更新 实际上,我认为没太大区别,不过在 react 项目中,可以直接使用进行优化。上手简单,开箱即用

自定义 hook

理解为抽取一个公共的函数方法,在需要的地方进行调用即可。自定义 Hook 共享的只是状态逻辑而不是状态本身。对 Hook 的每个调用完全独立于对同一个 Hook 的其他调用

function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  }

useRef

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);
  let ref = useRef(0); // 缓存值

  // 获取dom对应的ref进行操作
  function handleClick() {
    inputRef.current.focus();
  }

  function handleClick2() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>

      <button onClick={handleClick2}>
        Click me!
      </button>
    </>
  );
}

// 避免创建重复
function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }