額外機制

進階

您的某些元件可能需要控制 React 以外的系統並與之同步。例如,您可能需要使用瀏覽器 API 將焦點放在輸入框上、播放和暫停非 React 實作的影片播放器,或者連線到遠端伺服器並監聽訊息。在本章中,您將學習讓您可以「跳出」React 並連接到外部系統的額外機制。您的大部分應用程式邏輯和資料流不應依賴這些功能。

使用 refs 參考值

當您希望元件「記住」某些資訊,但您不希望該資訊觸發新的渲染時,您可以使用ref

const ref = useRef(0);

與狀態類似,refs 在重新渲染之間由 React 保留。但是,設定狀態會重新渲染元件。更改 ref 不會!您可以透過 ref.current 屬性存取該 ref 的目前值。

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 來儲存逾時 IDDOM 元素,以及其他不會影響元件渲染輸出的物件。

準備好學習這個主題了嗎?

閱讀使用 Refs 參考值 以了解如何使用 refs 來記住資訊。

閱讀更多

使用 refs 操作 DOM

React 會自動更新 DOM 以符合您的渲染輸出,因此您的元件通常不需要操作它。但是,有時您可能需要存取 React 管理的 DOM 元素,例如,將焦點放在節點上、捲動到它,或者測量它的大小和位置。React 中沒有內建的方法可以執行這些操作,因此您需要 DOM 節點的 ref。例如,點擊按鈕將使用 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>
    </>
  );
}

準備好學習這個主題了嗎?

閱讀使用 Refs 操作 DOM 以了解如何存取 React 管理的 DOM 元素。

閱讀更多

使用 Effects 同步

某些元件需要與外部系統同步。例如,您可能希望根據 React 狀態控制非 React 元件、設定伺服器連線,或者在元件出現在螢幕上時傳送分析記錄。與允許您處理特定事件的事件處理常式不同,Effects 允許您在渲染後執行一些程式碼。使用它們將您的元件與 React 以外的系統同步。

按幾次播放/暫停,看看影片播放器如何與 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 應傳回一個清理函式,該函式告訴 React 如何將您的元件與該伺服器斷開連線

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>;
}

在開發過程中,React 會立即額外執行並清理您的 Effect 一次。這就是為什麼您會看到 "✅ 連線中..." 列印兩次的原因。這可確保您不會忘記實作清理函式。

準備好學習這個主題了嗎?

閱讀使用 Effects 同步 以了解如何將元件與外部系統同步。

閱讀更多

你可能不需要 Effect

Effect 是 React 範式中的一個逃生出口。它們允許你「跳出」React 並將你的元件與某些外部系統同步。如果沒有涉及外部系統(例如,如果你想在某些 props 或 state 改變時更新元件的 state),你應該不需要 Effect。移除不必要的 Effect 將使你的程式碼更容易理解、執行速度更快且更不容易出錯。

有兩種常見的情況下你不需要 Effect

  • 你不需要 Effect 來轉換渲染用的資料。
  • 你不需要 Effect 來處理使用者事件。

例如,你不需要 Effect 來根據其他 state 調整某些 state

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]);
// ...
}

相反地,盡可能在渲染時進行計算

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

然而,你*確實*需要 Effect 來與外部系統同步。

準備好學習這個主題了嗎?

閱讀你可能不需要 Effect 瞭解如何移除不必要的 Effect。

閱讀更多

反應式 Effect 的生命週期

Effect 的生命週期與元件不同。元件可以掛載、更新或卸載。Effect 只能做兩件事:開始同步某些東西,以及稍後停止同步它。如果你的 Effect 依賴於隨著時間推移而變化的 props 和 state,則此週期可能會發生多次。

此 Effect 依賴於 roomId prop 的值。Props 是*反應式值*,這表示它們可以在重新渲染時更改。請注意,如果 roomId 改變,Effect 會*重新同步*(並重新連接到伺服器)。

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

const serverUrl = 'https://127.0.0.1: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 提供了一個程式碼檢查規則來檢查你是否已正確指定 Effect 的依賴項。如果你忘記在上述範例的依賴項列表中指定 roomId,程式碼檢查器會自動找到該錯誤。

準備好學習這個主題了嗎?

閱讀反應式事件的生命週期 瞭解 Effect 的生命週期與元件的生命週期有何不同。

閱讀更多

將事件與 Effect 分離

建構中

本節描述一個在穩定版 React 中**尚未發布的實驗性 API**。

事件處理程式僅在你再次執行相同互動時才會重新執行。與事件處理程式不同,如果 Effect 讀取的任何值(例如 props 或 state)與上次渲染時不同,Effect 就會重新同步。有時,你希望混合使用這兩種行為:一個響應某些值而不是其他值的 Effect。

Effect 內的所有程式碼都是*反應式的*。如果它讀取的一些反應式值由於重新渲染而發生更改,它將再次執行。例如,如果 roomIdtheme 發生更改,此 Effect 將重新連接到聊天室

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

const serverUrl = 'https://127.0.0.1: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'} 
      />
    </>
  );
}

這並不理想。你只想在 roomId 改變時才重新連接到聊天室。切換 theme 不應該重新連接到聊天室!將讀取 theme 的程式碼從你的 Effect 移到*Effect 事件*中

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://127.0.0.1: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'} 
      />
    </>
  );
}

Effect 事件內的程式碼不是反應式的,因此更改 theme 不會再使你的 Effect 重新連接。

準備好學習這個主題了嗎?

閱讀將事件與 Effect 分離 以瞭解如何防止某些值重新觸發 Effect。

閱讀更多

移除 Effect 依賴項

當你編寫 Effect 時,程式碼檢查器會驗證你是否已將 Effect 讀取的每個反應式值(例如 props 和 state)包含在 Effect 的依賴項列表中。這可確保你的 Effect 與元件的最新 props 和 state 保持同步。不必要的依賴項可能會導致你的 Effect 執行過於頻繁,甚至造成無限迴圈。移除它們的方式取決於具體情況。

例如,此 Effect 依賴於每次編輯輸入時都會重新建立的 options 物件

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

const serverUrl = 'https://127.0.0.1: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} />
    </>
  );
}

你不希望每次在該聊天室中開始輸入訊息時都重新連接到聊天室。要解決此問題,請將 options 物件的建立移到 Effect 內,以便 Effect 僅依賴於 roomId 字串

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

const serverUrl = 'https://127.0.0.1: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} />
    </>
  );
}

請注意,你不是從編輯依賴項列表開始移除 options 依賴項。那樣是錯誤的。相反地,你更改了周圍的程式碼,使依賴項變得*不必要*。將依賴項列表視為你的 Effect 程式碼使用的所有反應式值的列表。你不應該刻意選擇要在此列表中放置什麼。列表描述了你的程式碼。要更改依賴項列表,請更改程式碼。

準備好學習這個主題了嗎?

閱讀移除 Effect 依賴項 以瞭解如何減少 Effect 的重新執行頻率。

閱讀更多

使用自訂 Hooks 重複使用邏輯

React 內建了一些 Hooks,例如 useStateuseContextuseEffect。有時,您會希望有一個 Hook 能夠處理更特定的用途:例如,擷取資料、追蹤使用者是否在線上,或連線到聊天室。為此,您可以根據應用程式的需求建立自己的 Hooks。

在此範例中,自訂 Hook usePointerPosition 會追蹤游標位置,而自訂 Hook useDelayedValue 會傳回一個值,該值會「延遲」您傳遞的值一定的毫秒數。將游標移到沙盒預覽區域,即可看到一串移動的點跟隨著游標。

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(pos4, 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,
    }} />
  );
}

您可以建立自訂 Hooks、將它們組合在一起、在它們之間傳遞資料,並在元件之間重複使用它們。隨著應用程式的成長,您將會減少手動編寫 Effects 的次數,因為您將能夠重複使用已編寫的自訂 Hooks。React 社群也維護許多優秀的自訂 Hooks。

準備好學習這個主題了嗎?

閱讀使用自訂 Hooks 重複使用邏輯,以了解如何在元件之間共用邏輯。

閱讀更多

下一步是什麼?

前往使用 Refs 參考值,開始逐頁閱讀本章!