第三五章 逃生艙口-序

逃生艙口

你的一些組件可能需要控制和同步 React 之外的系統(tǒng)。 例如,您可能需要使用瀏覽器 API 聚焦輸入,播放和暫停未使用 React 實(shí)現(xiàn)的視頻播放器,或者連接并收聽(tīng)來(lái)自遠(yuǎn)程服務(wù)器的消息。 在本章中,您將學(xué)習(xí)讓您“走出去”React 并連接到外部系統(tǒng)的逃生艙口。 大多數(shù)應(yīng)用程序邏輯和數(shù)據(jù)流不應(yīng)依賴這些功能。

本章內(nèi)容預(yù)告

  • 如何在不重新渲染的情況下“記住”信息
  • 如何訪問(wèn) React 管理的 DOM 元素
  • 如何將組件與外部系統(tǒng)同步
  • 如何從組件中刪除不必要的效果
  • Effect 的生命周期與組件的生命周期有何不同
  • 如何防止某些值重新觸發(fā) Effects
  • 如何減少 Effect 重新運(yùn)行的頻率
  • 如何在組件之間共享邏輯

使用 ref 引用值

當(dāng)你想讓一個(gè)組件“記住”一些信息,但又不想讓這些信息觸發(fā)新的渲染時(shí),你可以使用 ref:

const ref = useRef(0);

與狀態(tài)一樣,refs 在重新渲染之間由 React 保留。 但是,設(shè)置狀態(tài)會(huì)重新渲染組件。 更改 ref 不會(huì)! 您可以通過(guò) ref.current 屬性訪問(wèn)該 ref 的當(dāng)前值。

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

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

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

ref 就像 React 不跟蹤的組件的秘密口袋。 例如,您可以使用 refs 來(lái)存儲(chǔ)超時(shí) ID、DOM 元素和其他不影響組件渲染輸出的對(duì)象。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀使用 ref 引用值了解如何使用 refs 來(lái)記住信息。

使用 ref 操作 DOM

React 會(huì)自動(dòng)更新 DOM 以匹配您的渲染輸出,因此您的組件不需要經(jīng)常操作它。 然而,有時(shí)您可能需要訪問(wèn)由 React 管理的 DOM 元素——例如,聚焦一個(gè)節(jié)點(diǎn),滾動(dòng)到它,或者測(cè)量它的大小和位置。 在 React 中沒(méi)有內(nèi)置的方法來(lái)做這些事情,所以你需要一個(gè) DOM 節(jié)點(diǎn)的引用。 例如,單擊按鈕將使用 ref 聚焦輸入:

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

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

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀使用 Refs 操作 DOM 以了解如何訪問(wèn)由 React 管理的 DOM 元素。

使用Effect同步

一些組件需要與外部系統(tǒng)同步。 例如,您可能希望根據(jù) React 狀態(tài)控制非 React 組件、設(shè)置服務(wù)器連接或在組件出現(xiàn)在屏幕上時(shí)發(fā)送分析日志。 與讓您處理特定事件的事件處理程序不同,Effects 讓您在渲染后運(yùn)行一些代碼。 使用它們將你的組件與 React 之外的一些系統(tǒng)同步。

多次按下播放/暫停鍵,看看視頻播放器如何與 isPlaying 屬性值保持同步:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

許多 Effects 也需要自己“清理”。 例如,如果你的 Effect 建立了一個(gè)到聊天服務(wù)器的連接,它應(yīng)該返回一個(gè)清理函數(shù)來(lái)告訴 React 如何斷開(kāi)你的組件與該服務(wù)器的連接:
App.js

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

chat.js

export function createConnection() {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('? Connecting...');
    },
    disconnect() {
      console.log('? Disconnected.');
    }
  };
}

在開(kāi)發(fā)中,React 將立即運(yùn)行并額外清理一次 Effect。 這就是為什么您會(huì)看到兩次打印“? Connecting...”的原因。 這確保您不會(huì)忘記實(shí)現(xiàn)清理功能。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀使用Effect同步以了解如何將組件與外部系統(tǒng)同步。

你可能不需要Effect

Effects 是 React 范式的逃生通道。 它們讓你“走出”React 并將你的組件與一些外部系統(tǒng)同步。 如果不涉及外部系統(tǒng)(例如,如果您想在某些屬性或狀態(tài)更改時(shí)更新組件的狀態(tài)),則不需要 Effect。 刪除不必要的 Effects 將使您的代碼更易于理解、運(yùn)行速度更快并且更不容易出錯(cuò)。

有兩種常見(jiàn)情況不需要 Effects:

  • 您不需要 Effects 來(lái)轉(zhuǎn)換數(shù)據(jù)以進(jìn)行渲染。
  • 您不需要 Effects 來(lái)處理用戶事件。

例如,您不需要 Effect 來(lái)根據(jù)其他狀態(tài)調(diào)整某些狀態(tài):

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // ?? Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

相反,在渲染時(shí)盡可能多地計(jì)算:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ? Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

但是,您確實(shí)需要 Effects 才能與外部系統(tǒng)同步。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀您可能不需要效果以了解如何刪除不必要的效果。

React Effect的生命周期

Effect 與組件有不同的生命周期。 組件可以掛載、更新或卸載。 Effect 只能做兩件事:開(kāi)始同步某些東西,然后停止同步它。 如果你的 Effect 依賴于隨時(shí)間變化的props和狀態(tài),這個(gè)循環(huán)可能會(huì)發(fā)生多次。

此效果取決于 roomId 屬性的值。 屬性是反應(yīng)值,這意味著它們可以在重新渲染時(shí)改變。 請(qǐng)注意,在您更新 roomId 后,Effect 會(huì)重新同步(并重新連接到服務(wù)器):

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

const serverUrl = 'https://localhost:1234';

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

  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

React 提供了一個(gè) linter 規(guī)則來(lái)檢查您是否正確指定了 Effect 的依賴項(xiàng)。 如果您忘記在上述示例的依賴項(xiàng)列表中指定 roomId,linter 會(huì)自動(dòng)找到該錯(cuò)誤。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀反應(yīng)事件的生命周期,了解 Effect 的生命周期與組件的生命周期有何不同。

將事件與Effect分開(kāi)

建設(shè)中...
本節(jié)描述了一個(gè)尚未添加到 React 中的實(shí)驗(yàn)性 API,因此您還不能使用它。

事件處理程序僅在您再次執(zhí)行相同的交互時(shí)重新運(yùn)行。 與事件處理程序不同,如果 Effects 讀取的某些值(如 prop 或狀態(tài)變量)與上次渲染時(shí)的值不同,則 Effects 會(huì)重新同步。 有時(shí),您需要兩種行為的混合:一個(gè) Effect 重新運(yùn)行以響應(yīng)某些值而不是其他值。

Effects 中的所有代碼都是反應(yīng)式的。 如果它讀取的某些反應(yīng)值由于重新渲染而發(fā)生變化,它將再次運(yùn)行。 例如,如果 roomId 或主題在交互后發(fā)生更改,此 Effect 將重新連接到聊天:
App.js

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}

chat.js

export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}

notifications.js

import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';

export function showNotification(message, theme) {
  Toastify({
    text: message,
    duration: 2000,
    gravity: 'top',
    position: 'right',
    style: {
      background: theme === 'dark' ? 'black' : 'white',
      color: theme === 'dark' ? 'white' : 'black',
    },
  }).showToast();
}

這并不理想。 僅當(dāng) roomId 已更改時(shí),您才想重新連接到聊天。 切換主題不應(yīng)該重新連接到聊天! 將代碼閱讀主題從 Effect 移到 Event 函數(shù)中:
App.js

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}

chat.js

export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}

事件函數(shù)中的代碼不是反應(yīng)性的,因此更改主題不再使您的效果重新連接。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀將事件與Effect分開(kāi)了解如何防止某些值重新觸發(fā) Effects。

刪除 Effect 依賴項(xiàng)

當(dāng)您編寫(xiě) Effect 時(shí),linter 將驗(yàn)證您是否已將 Effect 讀取的每個(gè)反應(yīng)值(如 props 和 state)包含在 Effect 的依賴項(xiàng)列表中。 這確保您的 Effect 與組件的最新道具和狀態(tài)保持同步。 不必要的依賴項(xiàng)可能會(huì)導(dǎo)致您的 Effect 運(yùn)行過(guò)于頻繁,甚至?xí)斐蔁o(wú)限循環(huán)。 刪除它們的方式取決于具體情況。

例如,此 Effect 取決于每次編輯輸入時(shí)都會(huì)重新創(chuàng)建的選項(xiàng)對(duì)象:

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

您不希望每次開(kāi)始在聊天中輸入消息時(shí)聊天都重新連接。 要解決此問(wèn)題,請(qǐng)?jiān)?Effect 中移動(dòng)選項(xiàng)對(duì)象的創(chuàng)建,以便 Effect 僅依賴于 roomId 字符串:

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

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

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

請(qǐng)注意,您并沒(méi)有通過(guò)編輯依賴項(xiàng)列表來(lái)刪除選項(xiàng)依賴項(xiàng)。 那是錯(cuò)誤的。 相反,您更改了周?chē)拇a,這樣依賴性就變得不必要了。 您可以將依賴項(xiàng)列表視為您的 Effect 代碼使用的所有反應(yīng)值的列表。 您不會(huì)有意選擇要放在該列表中的內(nèi)容。 該列表描述了您的代碼。 要更改依賴項(xiàng)列表,請(qǐng)更改代碼。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀刪除 Effect 依賴項(xiàng)了解如何減少 Effect 重新運(yùn)行的頻率。

通過(guò)自定義 Hooks 重用邏輯

React 帶有內(nèi)置的 Hook,例如 useState、useContext 和 useEffect。 有時(shí),您會(huì)希望有一個(gè) Hook 用于某些更具體的目的:例如,獲取數(shù)據(jù)、跟蹤用戶是否在線或連接到聊天室。 為此,您可以根據(jù)應(yīng)用程序的需要?jiǎng)?chuàng)建自己的 Hook。

在此示例中,usePointerPosition 自定義 Hook 跟蹤光標(biāo)位置,而 useDelayedValue 自定義 Hook 返回一個(gè)“滯后”您傳遞的值一定毫秒數(shù)的值。 將光標(biāo)移到沙盒預(yù)覽區(qū)域上以查看跟隨光標(biāo)移動(dòng)的點(diǎn)軌跡:
App.js

import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';

export default function Canvas() {
  const pos1 = usePointerPosition();
  const pos2 = useDelayedValue(pos1, 100);
  const pos3 = useDelayedValue(pos2, 200);
  const pos4 = useDelayedValue(pos3, 100);
  const pos5 = useDelayedValue(pos3, 50);
  return (
    <>
      <Dot position={pos1} opacity={1} />
      <Dot position={pos2} opacity={0.8} />
      <Dot position={pos3} opacity={0.6} />
      <Dot position={pos4} opacity={0.4} />
      <Dot position={pos5} opacity={0.2} />
    </>
  );
}

function Dot({ position, opacity }) {
  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity,
      transform: `translate(${position.x}px, ${position.y}px)`,
      pointerEvents: 'none',
      left: -20,
      top: -20,
      width: 40,
      height: 40,
    }} />
  );
}

usePointerPosition.js

import { useState, useEffect } from 'react';

export function usePointerPosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
  }, []);
  return position;
}

useDelayedValue.js

import { useState, useEffect } from 'react';

export function useDelayedValue(value, delay) {
  const [delayedValue, setDelayedValue] = useState(value);

  useEffect(() => {
    setTimeout(() => {
      setDelayedValue(value);
    }, delay);
  }, [value, delay]);

  return delayedValue;
}

您可以創(chuàng)建自定義 Hooks,將它們組合在一起,在它們之間傳遞數(shù)據(jù),并在組件之間重用它們。 隨著您的應(yīng)用程序的增長(zhǎng),您將減少手動(dòng)編寫(xiě)的 Effects,因?yàn)槟鷮⒛軌蛑赜媚呀?jīng)編寫(xiě)的自定義 Hooks。 React 社區(qū)也維護(hù)了很多優(yōu)秀的自定義 Hooks。

準(zhǔn)備好學(xué)習(xí)這個(gè)主題了嗎?

閱讀通過(guò)自定義 Hooks 重用邏輯了解如何在組件之間共享邏輯。

下一步是什么?

前往使用 Refs 引用值開(kāi)始逐頁(yè)閱讀本章!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文太長(zhǎng),謹(jǐn)慎閱讀。本文原文:一份完整的useEffect指南原文檔: A Complete Guide to u...
    xiaohesong閱讀 10,592評(píng)論 7 23
  • 前言 這篇文章旨在總結(jié) React Hooks 的使用技巧以及在使用過(guò)程中需要注意的問(wèn)題,其中會(huì)附加一些問(wèn)題產(chǎn)生的...
    袋鼠云數(shù)棧前端閱讀 437評(píng)論 0 1
  • 前言 React Fiber 不是一個(gè)新的東西,但在前端領(lǐng)域是第一次廣為認(rèn)知的應(yīng)用。幾年前全新的Fiber架構(gòu)讓剛...
    這個(gè)前端不太冷閱讀 5,719評(píng)論 2 8
  • 1. 引言 工具型文章要跳讀,而文學(xué)經(jīng)典就要反復(fù)研讀。如果說(shuō) React 0.14 版本帶來(lái)的各種生命周期可以類比...
    黃子毅閱讀 2,234評(píng)論 0 10
  • React Hooks Hook 是 16.8新增特性。 hooks 優(yōu)勢(shì) 能優(yōu)化類組件的三大問(wèn)題 能在無(wú)需修改組...
    Vincent_cy閱讀 869評(píng)論 0 0

友情鏈接更多精彩內(nèi)容