WHAT - React 学习系列(七)- Escape hatches

Overview

Some of your components may need to control and synchronize with systems outside of React. For example, you might need to focus an input using the browser API, play and pause a video player implemented without React, or connect and listen to messages from a remote server.

Referencing values with refs
When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref. A ref is like a secret pocket of your component that React doesn’t track. For example, you can use refs to store timeout IDs, DOM elements, and other objects that don’t impact the component’s rendering output.

Manipulating the DOM with refs
Sometimes you might need access to the DOM elements managed by React—for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a ref to the DOM node.

For example, clicking the button will focus the input using a 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>
    </>
  );
}

Synchronizing with external systems with Effects
Some components need to synchronize with external systems.

For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen.

Unlike event handlers, which let you handle particular events, Effects let you run some code after rendering. Use them to synchronize your component with a system outside of React.



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

Many Effects also “clean up” after themselves.For example, an Effect that sets up a connection to a chat server should return a cleanup function that tells React how to disconnect your component from that server.

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

In development, React will immediately run and clean up your Effect one extra time. This is why you see “Connecting…” printed twice. This ensures that you don’t forget to implement the cleanup function.

Effects are an escape hatch from the React paradigm. They let you “step outside” of React and synchronize your components with some external system.

You Might Not Need An Effect
If there is no external system involved (for example, if you want to update a component’s state when some props or state change), you shouldn’t need an Effect.
Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.
There are two common cases in which you don’t need Effects:

  • You don’t need Effects to transform data for rendering.
  • You don’t need Effects to handle user events.

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

Lifecycle of reactive effects
Effects have a different lifecycle from components.
Components may mount, update, or unmount.
An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it.
This cycle can happen multiple times if your Effect depends on props and state that change over time.

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

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

This Effect depends on the value of the roomId prop. Props are reactive values, which means they can change on a re-render.

React provides a linter rule to check that you’ve specified your Effect’s dependencies correctly. If you forget to specify roomId in the list of dependencies in the above example, the linter will find that bug automatically.
When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component.

Separating events from Effects: useEffectEvent
Event handlers only re-run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if any of the values they read, like props or state, are different than during last render. Sometimes, you want a mix of both behaviors: an Effect that re-runs in response to some values but not others?

All code inside Effects is reactive. It will run again if some reactive value it reads has changed due to a re-render. For example, this Effect will re-connect to the chat if either roomId or theme have changed:

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

This is not ideal. You want to re-connect to the chat only if the roomId has changed. Move the code reading theme out of your Effect into an Effect Event:

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

Code inside Effect Events isn’t reactive, so changing the theme no longer makes your Effect re-connect.

Removing Effect dependencies
Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop.
For example, this Effect depends on the options object which gets re-created every time you edit the input:

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

Error - 9:9 - The ‘options’ object makes the dependencies of useEffect Hook (at line 18) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of ‘options’ in its own useMemo() Hook.

To fix this problem, move creation of the options object inside the Effect so that the Effect only depends on the roomId string:

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

Reusing logic with custom Hooks

Sometimes, you’ll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room.
To do this, you can create your own Hooks for your application’s needs.

You can create custom Hooks, compose them together, pass data between them, and reuse them between components. As your app grows, you will write fewer Effects by hand because you’ll be able to reuse custom Hooks you already wrote.

There are also many excellent custom Hooks maintained by the React community.

Referencing values with refs

When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref.

You can access the current value of that ref through the ref.current property. This value is intentionally mutable, meaning you can both read and write to it.

Unlike state, ref is a plain JavaScript object with the current property that you can read and modify.

When a piece of information is used for rendering, keep it in state. When a piece of information is only needed by event handlers and changing it doesn’t require a re-render, using a ref may be more efficient.

  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);
  // …
  function handleStart() {
    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
    //…
  }
···

Differences between refs and state?
https://react.dev/learn/referencing-values-with-refs#differences-between-refs-and-state

How does useRef work inside?
You can imagine that inside of React, useRef is implemented like this:
```javascript
function useRef(initialValue) {
	const [ref, unused] = useState({ current: initialValue });
	return ref;
}

Note how the state setter is unused in this example. It is unnecessary because useRef always needs to return the same object!
You can think of it as a regular state variable without a setter.

state acts like a snapshot for every render and doesn’t update synchronously. But when you mutate the current value of a ref, it changes immediately

ref.current = 5;
console.log(ref.current); // 5

If you’re familiar with object-oriented programming, refs might remind you of instance fields—but instead of this.something you write somethingRef.current.

When to use refs?
Typically, you will use a ref when your component needs to “step outside” React and communicate with external APIs—often a browser API that won’t impact the appearance of the component. Here are a few of these rare situations:

  • Storing timeout IDs
  • Storing and manipulating DOM elements, which we cover on the next page
  • Storing other objects that aren’t necessary to calculate the JSX.
    However, the most common use case for a ref is to access a DOM element.
    When you pass a ref to a ref attribute in JSX, like <div ref={myRef}>, React will put the corresponding DOM element into myRef.current. Once the element is removed from the DOM, React will update myRef.current to be null.

Best practices for refs?

  1. Treat refs as an escape hatch
  2. Don’t read or write ref.current during rendering

Manipulating the DOM with refs

sometimes you might need access to the DOM elements managed by React—for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a ref to the DOM node.

const myRef = useRef(null);
<div ref={myRef}>

You can then access this DOM node from your event handlers and use the built-in browser APIs defined on it.

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

How to manage a list of refs using a ref callback?
sometimes you might need a ref to each item in the list, and you don’t know how many you will have.
One solution is to pass a function to the ref attribute. This is called a ref callback. React will call your ref callback with the DOM node when it’s time to set the ref, and with null when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref by its index or some kind of ID.
This example shows how you can use this approach to scroll to an arbitrary node in a long list:

import { useRef, useState } from "react";

export default function CatFriends() {
  const itemsRef = useRef(null);
  const [catList, setCatList] = useState(setupCatList);

  function scrollToCat(cat) {
    const map = getMap();
    const node = map.get(cat);
    node.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToCat(catList[0])}>Tom</button>
        <button onClick={() => scrollToCat(catList[5])}>Maru</button>
        <button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
	        // The ref callback on every list item takes care to update the Map
                const map = getMap();
                if (node) {
     		  // Add to the Map
                  map.set(cat, node);
                } else {
 		  // Remove from the Map
                  map.delete(cat);
                }
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

In this example, itemsRef doesn’t hold a single DOM node. Instead, it holds a Map from item ID to a DOM node.

This example shows another approach for managing the Map with a ref callback cleanup function.

<li
  key={cat.id}
  ref={node => {
    const map = getMap();
    // Add to the Map
    map.set(cat, node);

    return () => {
      // Remove from the Map
      map.delete(cat);
    };
  }}
>

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
This happens because by default React does not let a component access the DOM nodes of other components.

Accessing another component’s DOM nodes?React.forwardRef()?
Instead, components that want to expose their DOM nodes have to opt in to that behavior. A component can specify that it “forwards” its ref to one of its children. Here’s how MyInput can use the forwardRef API:

<MyInput ref={inputRef} />
//…
const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

完整代码:

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

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

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

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

In design systems, it is a common pattern for low-level components like buttons, inputs, and so on, to forward their refs to their DOM nodes.
On the other hand, high-level components like forms, lists, or page sections usually won’t expose their DOM nodes to avoid accidental dependencies on the DOM structure.

Exposing a subset of the API with an imperative handle?
In the above example, MyInput exposes the original DOM input element. This lets the parent component call focus() on it. However, this also lets the parent component do something else—for example, change its CSS styles. In uncommon cases, you may want to restrict the exposed functionality. You can do that with useImperativeHandle.

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

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

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

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

Here, realInputRef inside MyInput holds the actual input DOM node. However, useImperativeHandle instructs React to provide your own special object as the value of a ref to the parent component.

When React attaches the refs?
In React, every update is split in two phases:

  1. During render, react calls your components to figure out what should be on the screen.
  2. During commit, react applies changes to the DOM.
    During the first render, the DOM nodes have not yet been created, so ref.current will be null. And during the rendering of updates, the DOM nodes haven’t been updated yet. So it’s too early to read them.
    React sets ref.current during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes.

Flushing state updates synchronously with flushSync?
You can force React to update (“flush”) the DOM synchronously.

flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

This will instruct React to update the DOM synchronously right after the code wrapped in flushSync executes.

Best practices for DOM manipulation with refs?
https://react.dev/learn/manipulating-the-dom-with-refs#best-practices-for-dom-manipulation-with-refs
However, if you try to modify the DOM manually, you can risk conflicting with the changes React is making?

Synchronizing with external systems with Effects

Effects let you run some code after rendering so that you can synchronize your component with some system outside of React.

Before getting to Effects, you need to be familiar with two types of logic inside React components:

  • Rendering code (introduced in Describing the UI)
  • Event handlers (introduced in Adding Interactivity)
    Sometimes this isn’t enough.

Consider a ChatRoom component that must connect to the chat server whenever it’s visible on the screen. Connecting to a server is not a pure calculation (it’s a side effect) so it can’t happen during rendering. However, there is no single particular event like a click that causes ChatRoom to be displayed.

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event.

Effects run at the end of a commit after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library).

Keep in mind that Effects are typically used to “step out” of your React code and synchronize with some external system. This includes browser APIs, third-party widgets, network, and so on. Wrap the side effect with useEffect to move it out of the rendering calculation.

How to write an Effect?
https://react.dev/learn/synchronizing-with-effects

Most Effects should only re-run when needed rather than after every render. For example, a fade-in animation should only trigger when a component appears. Connecting and disconnecting to a chat room should only happen when the component appears and disappears, or when the chat room changes.

Some Effects need to specify how to stop, undo, or clean up whatever they were doing. For example, “connect” needs “disconnect”, “subscribe” needs “unsubscribe”, and “fetch” needs either “cancel” or “ignore”.

By default, Effects run after every render. This is why code like this will produce an infinite loop:

const [count, setCount] = useState(0);
useEffect(() => {
  setCount(count + 1);
});

Notice that you can’t “choose” your dependencies. You will get a lint error if the dependencies you specified don’t match what React expects based on the code inside your Effect. This helps catch many bugs in your code.

useEffect(() => {
  // This runs after every render
});

useEffect(() => {
  // This runs only on mount (when the component appears)
}, []);

useEffect(() => {
  // This runs on mount *and also* if either a or b have changed since the last render
}, [a, b]);

下面来看一个经典的例子:

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
  }, []);
  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.');
    }
  };
}

为什么控制台会打印两次 ✅ Connecting…?Why does it happen?

Imagine the ChatRoom component is a part of a larger app with many different screens. The user starts their journey on the ChatRoom page. The component mounts and calls connection.connect(). Then imagine the user navigates to another screen—for example, to the Settings page. The ChatRoom component unmounts. Finally, the user clicks Back and ChatRoom mounts again. This would set up a second connection—but the first connection was never destroyed! As the user navigates across the app, the connections would keep piling up.
Bugs like this are easy to miss without extensive manual testing. To help you spot them quickly, in development React remounts every component once immediately after its initial mount.
Seeing the “✅ Connecting…” log twice helps you notice the real issue: your code doesn’t close the connection when the component unmounts.

To fix the issue, return a cleanup function from your Effect:

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

React will call your cleanup function each time before the Effect runs again, and one final time when the component unmounts (gets removed). Let’s see what happens when the cleanup function is implemented:

✅ Connecting...
❌ Disconnected.
✅ Connecting...

This is the correct behavior in development. There’s an extra connect/disconnect call pair because React is probing your code for bugs in development.
In production, you would only see “✅ Connecting…” printed once. Remounting components only happens in development to help you find Effects that need cleanup.

how to handle the Effect firing twice in dev?

  1. Controlling non-React widgets
  2. Subscribing to events
  3. Triggering animations
  4. Fetching data https://react.dev/learn/synchronizing-with-effects#fetching-data
  5. Sending analytics https://react.dev/learn/synchronizing-with-effects#sending-analytics

Fetching Data !!! What are good alternatives to data fetching in effects?
https://react.dev/learn/synchronizing-with-effects#what-are-good-alternatives-to-data-fetching-in-effects

  • If you use a framework, use its built-in data fetching mechanism. Modern React frameworks have integrated data fetching mechanisms that are efficient and don’t suffer from the above pitfalls.
  • Otherwise, consider using or building a client-side cache. Popular open source solutions include React Query, useSWR, and React Router 6.4+.
  • You can build your own solution too, in which case you would use Effects under the hood, but add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).

Framework?https://react.dev/learn/start-a-new-react-project#production-grade-react-frameworks

React Query?https://tanstack.com/query/latest
useSWR?https://swr.vercel.app/
React Router 6.4+?https://beta.reactrouter.com/en/main/start/overview

Effects from each render are isolated from each other. If you’re curious how this works, you can read about closures.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

You Might Not Need An Effect

Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.

How to remove unnecessary Effects?
https://react.dev/learn/you-might-not-need-an-effect#how-to-remove-unnecessary-effects

  1. Updating state based on props or state
  2. Caching expensive calculations. useMemo!
  3. Resetting all state when a prop changes
  4. Adjusting some state when a prop changes
  5. Sharing logic between event handlers
  6. Sending a post request
  7. Chains of computations
  8. Initializing the application
  9. Notifying parent components about state changes
  10. Passing data to the parent
  11. Subscribing to an external store. useSyncExternalStore!
  12. Fetching data. Race condition!

Caching expensive calculations. useMemo!
How to tell if a calculation is expensive?

console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');

If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation. As an experiment, you can then wrap the calculation in useMemo to verify whether the total logged time has decreased for that interaction or not:

console.time('filter array');
const visibleTodos = useMemo(() => {
  return getFilteredTodos(todos, filter); // Skipped if todos and filter haven't changed
}, [todos, filter]);
console.timeEnd('filter array');

Keep in mind that your machine is probably faster than your users’ so it’s a good idea to test the performance with an artificial slowdown. For example, Chrome offers a CPU Throttling option for this.
https://developer.chrome.com/blog/new-in-devtools-61/#throttling

useMemo won’t make the first render faster. It only helps you skip unnecessary work on updates.

Resetting all state when a prop changes

Instead, you can tell React that each user’s profile is conceptually a different profile by giving it an explicit key.

<Profile
  userId={userId}
  key={userId}
/>

By passing userId as a key to the Profile component, you’re asking React to treat two Profile components with different userId as two different components that should not share any state.

Adjusting some state when a prop changes

Sometimes, you might want to reset or adjust a part of the state on a prop change, but not all of it. This List component receives a list of items as a prop, and maintains the selected item in the selection state variable. You want to reset the selection to null whenever the items prop receives a different array:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

Every time the items change, the List and its child components will render with a stale selection value at first. Then React will update the DOM and run the Effects. Finally, the setSelection(null) call will cause another re-render of the List and its child components, restarting this whole process again.

Instead, adjust the state directly during rendering:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

Storing information from previous renders like this can be hard to understand, but it’s better than updating the same state in an Effect.
https://react.dev/reference/react/useState#storing-information-from-previous-renders

In the above example, setSelection is called directly during a render. React will re-render the List immediately after it exits with a return statement. React has not rendered the List children or updated the DOM yet, so this lets the List children skip rendering the stale selection value.

Although this pattern is more efficient than an Effect, most components shouldn’t need it either. No matter how you do it, adjusting state based on props or other state makes your data flow more difficult to understand and debug. So Always check whether you can reset all state with a key or calculate everything during rendering instead.

For example, instead of storing (and resetting) the selected item, you can store the selected item ID:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

Sharing logic between event handlers

When you’re not sure whether some code should be in an Effect or in an event handler, ask yourself why this code needs to run. Use Effects only for code that should run because the component was displayed to the user. example, the notification should appear because the user pressed the button, not because the page was displayed! Delete the Effect and put the shared logic into a function called from both event handlers.

When you choose whether to put some logic into an event handler or an Effect, the main question you need to answer is what kind of logic it is from the user’s perspective. If this logic is caused by a particular interaction, keep it in the event handler. If it’s caused by the user seeing the component on the screen, keep it in the Effect.

Chains of computations

Sometimes you might feel tempted to chain Effects that each adjust a piece of state based on other state:

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    } else {
      setCard(nextCard);
    }
  }

  // ...

One problem is that it is very inefficient: the component (and its children) have to re-render between each set call in the chain. In the example above, in the worst case (setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render) there are three unnecessary re-renders of the tree below.

In this case, it’s better to calculate what you can during rendering, and adjust the state in the event handler:

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ Calculate what you can during rendering
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // ✅ Calculate all the next state in the event handler
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }

  // ...

But, in some cases, you can’t calculate the next state directly in the event handler. For example, imagine a form with multiple dropdowns where the options of the next dropdown depend on the selected value of the previous dropdown. Then, a chain of Effects is appropriate because you are synchronizing with network.

import React, { useState, useEffect } from 'react';
import { Select } from 'antd';
import axios from 'axios';

const { Option } = Select;

const DynamicDropdownForm = () => {
    const [firstDropdownOptions, setFirstDropdownOptions] = useState([]);
    const [secondDropdownOptions, setSecondDropdownOptions] = useState([]);
    const [thirdDropdownOptions, setThirdDropdownOptions] = useState([]);

    const [selectedFirstOption, setSelectedFirstOption] = useState('');
    const [selectedSecondOption, setSelectedSecondOption] = useState('');
    const [selectedThirdOption, setSelectedThirdOption] = useState('');

    useEffect(() => {
        // Simulate fetching options from the server
        axios.get(`https://api.example.com/options/first`)
            .then(response => {
                setFirstDropdownOptions(response.data);
            })
            .catch(error => {
                console.error('Error fetching first dropdown options:', error);
            });
    }, []);

    useEffect(() => {
        // Fetch options for the second dropdown based on selectedFirstOption
        if (selectedFirstOption) {
            axios.get(`https://api.example.com/options/second/${selectedFirstOption}`)
                .then(response => {
                    setSecondDropdownOptions(response.data);
                })
                .catch(error => {
                    console.error('Error fetching second dropdown options:', error);
                });
        } else {
            setSecondDropdownOptions([]);
        }
    }, [selectedFirstOption]);

    useEffect(() => {
        // Fetch options for the third dropdown based on selectedSecondOption
        if (selectedSecondOption) {
            axios.get(`https://api.example.com/options/third/${selectedSecondOption}`)
                .then(response => {
                    setThirdDropdownOptions(response.data);
                })
                .catch(error => {
                    console.error('Error fetching third dropdown options:', error);
                });
        } else {
            setThirdDropdownOptions([]);
        }
    }, [selectedSecondOption]);

    const handleFirstDropdownChange = (value) => {
        setSelectedFirstOption(value);
        setSelectedSecondOption(''); // Reset second dropdown when first dropdown changes
        setSelectedThirdOption(''); // Reset third dropdown when first dropdown changes
    };

    const handleSecondDropdownChange = (value) => {
        setSelectedSecondOption(value);
        setSelectedThirdOption(''); // Reset third dropdown when second dropdown changes
    };

    const handleThirdDropdownChange = (value) => {
        setSelectedThirdOption(value);
    };

    return (
        <div>
            <Select
                value={selectedFirstOption}
                onChange={handleFirstDropdownChange}
                style={{ width: 200, marginBottom: 16 }}
            >
                {firstDropdownOptions.map(option => (
                    <Option key={option.value} value={option.value}>{option.label}</Option>
                ))}
            </Select>

            <Select
                value={selectedSecondOption}
                onChange={handleSecondDropdownChange}
                style={{ width: 200, marginBottom: 16 }}
                disabled={!selectedFirstOption}
            >
                {secondDropdownOptions.map(option => (
                    <Option key={option.value} value={option.value}>{option.label}</Option>
                ))}
            </Select>

            <Select
                value={selectedThirdOption}
                onChange={handleThirdDropdownChange}
                style={{ width: 200 }}
                disabled={!selectedSecondOption}
            >
                {thirdDropdownOptions.map(option => (
                    <Option key={option.value} value={option.value}>{option.label}</Option>
                ))}
            </Select>
        </div>
    );
};

export default DynamicDropdownForm;

Initializing the application

Some logic should only run once when the app loads. You might be tempted to place it in an Effect in the top-level component:

function App() {
  // 🔴 Avoid: Effects with logic that should only ever run once
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

However, you’ll quickly discover that it runs twice in development. This can cause issues—for example, maybe it invalidates the authentication token because the function wasn’t designed to be called twice.

Although it may not ever get remounted in practice in production, following the same constraints in all components makes it easier to move and reuse code. If some logic must run once per app load rather than once per component mount, add a top-level variable to track whether it has already executed:

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

You can also run it during module initialization and before the app renders:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
   // ✅ Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

Code at the top level runs once when your component is imported—even if it doesn’t end up being rendered. To avoid slowdown or surprising behavior when importing arbitrary components, don’t overuse this pattern.

Keep app-wide initialization logic to root component modules like App.js or in your application’s entry point.

Passing data to the parent

In React, data flows from the parent components to their children. When you see something wrong on the screen, you can trace where the information comes from by going up the component chain until you find which component passes the wrong prop or has the wrong state.

When child components update the state of their parent components in Effects, the data flow becomes very difficult to trace.

Subscribing to an external store

Sometimes, your components may need to subscribe to some data outside of the React state. This data could be from a third-party library or a built-in browser API.

Since this data can change without React’s knowledge, you need to manually subscribe your components to it. This is often done with an Effect, for example:

function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

Here, the component subscribes to an external data store (in this case, the browser navigator.onLine API). Since this API does not exist on the server (so it can’t be used for the initial HTML), initially the state is set to true. Whenever the value of that data store changes in the browser, the component updates its state.

Although it’s common to use Effects for this, React has a purpose-built Hook for subscribing to an external store that is preferred instead. Delete the Effect and replace it with a call to useSyncExternalStore:

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function useOnlineStatus() {
  // ✅ Good: Subscribing to an external store with a built-in Hook
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

This approach is less error-prone than manually syncing mutable data to React state with an Effect. Typically, you’ll write a custom Hook like useOnlineStatus() above so that you don’t need to repeat this code in the individual components.

Fetching data

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 Avoid: Fetching without cleanup logic
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

the code above has a bug. Imagine you type “hello” fast. Then the query will change from “h”, to “he”, “hel”, “hell”, and “hello”. This will kick off separate fetches, but there is no guarantee about which order the responses will arrive in. For example, the “hell” response may arrive after the “hello” response. Since it will call setResults() last, you will be displaying the wrong search results. This is called a “race condition”: two different requests “raced” against each other and came in a different order than you expected.
https://en.wikipedia.org/wiki/Race_condition

To fix the race condition, you need to add a cleanup function to ignore stale responses:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

This ensures that when your Effect fetches data, all responses except the last requested one will be ignored.

Handling race conditions is not the only difficulty with implementing data fetching. You might also want to think about caching responses (so that the user can click Back and see the previous screen instantly), how to fetch data on the server (so that the initial server-rendered HTML contains the fetched content instead of a spinner), and how to avoid network waterfalls (so that a child can fetch data without waiting for every parent). Solving them is not trivial, which is why modern frameworks provide more efficient built-in data fetching mechanisms than fetching data in Effects.

  1. Race conditions
  2. Caching responses
  3. Fetch data on the server
  4. Avoid network waterfalls

If you don’t use a framework (and don’t want to build your own) but would like to make data fetching from Effects more ergonomic, consider extracting your fetching logic into a custom Hook like in this example:

function SearchResults({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}

You’ll likely also want to add some logic for error handling and to track whether the content is loading.

The fewer raw useEffect calls you have in your components, the easier you will find to maintain your application.

Lifecycle of reactive effects

Effects have a different lifecycle from components.

Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time.

The lifecycle of an Effect?
Every React component goes through the same lifecycle:

  • A component mounts when it’s added to the screen.
  • A component updates when it receives new props or state, usually in response to an interaction.
  • A component unmounts when it’s removed from the screen.

An Effect describes how to synchronize an external system to the current props and state.

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

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

Intuitively, you might think that React would start synchronizing when your component mounts and stop synchronizing when your component unmounts. However, this is not the end of the story!

Sometimes, it may also be necessary to start and stop synchronizing multiple times while the component remains mounted? Imagine this ChatRoom component receives a roomId prop that the user picks in a dropdown. Recall that your ChatRoom component has received a new value for its roomId prop. It used to be “general”, and now it is “travel”. React needs to re-synchronize your Effect to re-connect you to a different room. Every time after your component re-renders with a different roomId, your Effect will re-synchronize. Finally, when the user goes to a different screen, ChatRoom unmounts. Now there is no need to stay connected at all. React will stop synchronizing your Effect one last time and disconnect you from the “travel” chat room.
Let’s recap everything that’s happened from the ChatRoom component’s perspective:

  1. ChatRoom mounted with roomId set to “general”
  2. ChatRoom updated with roomId set to “travel”
  3. ChatRoom unmounted

Previously, you were thinking from the component’s perspective. Instead, always focus on a single start/stop cycle at a time. It shouldn’t matter whether a component is mounting, updating, or unmounting. All you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as it’s needed.

React verifies that your Effect can re-synchronize by forcing it to do that immediately in development. This might remind you of opening a door and closing it an extra time to check if the door lock works. React starts and stops your Effect one extra time in development to check you’ve implemented its cleanup well.

Each Effect represents a separate synchronization process?
Resist adding unrelated logic to your Effect only because this logic needs to run at the same time as an Effect you already wrote. For example, let’s say you want to send an analytics event when the user visits the room.

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

But imagine you later add another dependency to this Effect that needs to re-establish the connection. If this Effect re-synchronizes, it will also call logVisit(roomId) for the same room, which you did not intend. Logging the visit is a separate process from connecting. Write them as two separate Effects:

function ChatRoom({ roomId }) {
  useEffect(() => {
    logVisit(roomId);
  }, [roomId]);

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    // ...
  }, [roomId]);
  // ...
}

Each Effect in your code should represent a separate and independent synchronization process.
On the other hand, if you split up a cohesive piece of logic into separate Effects, the code may look “cleaner” but will be more difficult to maintain. This is why you should think whether the processes are same or separate, not whether the code looks cleaner.

All values inside the component (including props, state, and variables in your component’s body) are reactive. Any reactive value can change on a re-render, so you need to include reactive values as Effect’s dependencies. In other words, Effects “react” to all values from the component body.
Can global or mutable values be dependencies?

  1. Mutable values (including global variables) aren’t reactive. A mutable value like location.pathname can’t be a dependency. Instead, you should read and subscribe to an external mutable value with useSyncExternalStore.
    https://react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store
  2. A mutable value like ref.current or things you read from it also can’t be a dependency. The ref object returned by useRef itself can be a dependency, but its current property is intentionally mutable.

In some cases, React knows that a value never changes even though it’s declared inside the component. For example, the set function returned from useState and the ref object returned by useRef are stable—they are guaranteed to not change on a re-render. Stable values aren’t reactive, so you may omit them from the list. Including them is allowed: they won’t change, so it doesn’t matter.

当在 React 中使用 useStateuseRef 钩子时,它们返回的值在组件重新渲染时是稳定的,即保证不会改变。这种稳定性对于某些场景非常有用,特别是在处理事件处理程序、动画控制、以及其他需要持久化状态或引用的情况下。

useState

useState 返回一个包含当前状态值和更新状态值的数组,如下所示:

import React, { useState } from 'react';

function ExampleComponent() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1); // setCount 是稳定的,不会改变
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default ExampleComponent;

在上面的例子中,setCount 是一个稳定的函数,即使在组件重新渲染时,它也仍然是同一个函数引用。这意味着你可以安全地将 setCount 作为事件处理程序传递给子组件,而不必担心它在每次重新渲染时都会发生变化。

useRef

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(通常是 null)。它具有在组件重新渲染时保持不变的特性:

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

function ExampleComponent() {
  const refContainer = useRef(null);

  useEffect(() => {
    console.log(refContainer.current); // refContainer 是稳定的,不会改变
  }, []);

  return <div ref={refContainer}>Example Component</div>;
}

export default ExampleComponent;

在上面的例子中,refContainer 是一个稳定的对象,即使在组件重新渲染时,它也保持不变。这使得它非常适合用来引用 DOM 元素、存储持久化数据或者进行其他需要在多次渲染之间共享的任务。

总结,在使用 useStateuseRef 时,它们返回的值在组件重新渲染时保持稳定,不会发生变化。这使得在处理一些需要保持状态或引用不变的情况下非常方便,例如事件处理、动画控制、以及其他涉及到持久化状态的场景。

The linter is your friend, but its powers are limited. The linter only knows when the dependencies are wrong. It doesn’t know the best way to solve each case. If the linter suggests a dependency, but adding it causes a loop, it doesn’t mean the linter should be ignored. You need to change the code inside (or outside) the Effect so that that value isn’t reactive and doesn’t need to be a dependency.

If you have an existing codebase, you might have some Effects that suppress the linter like this:

useEffect(() => {
  // ...
  // 🔴 Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Separating events from Effects: useEffectEvent

Things get more tricky when you want to mix reactive logic with non-reactive logic.

For example, imagine that you want to show a notification when the user connects to the chat. You read the current theme (dark or light) from the props so that you can show the notification in the correct color:

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

When the roomId changes, the chat re-connects as you would expect. But since theme is also a dependency, the chat also re-connects every time you switch between the dark and the light theme. That’s not great!
In other words, you don’t want this line to be reactive, even though it is inside an Effect (which is reactive): showNotification('Connected!', theme);. You need a way to separate this non-reactive logic from the reactive Effect around it.

Use a special Hook called useEffectEvent to extract this non-reactive logic out of your Effect:
https://react.dev/reference/react/experimental_useEffectEvent

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]); // ✅ All dependencies declared
  // ...

You can think of Effect Events as being very similar to event handlers. The main difference is that event handlers run in response to a user interactions, whereas Effect Events are triggered by you from Effects.

Effect Events let you “break the chain” between the reactivity of Effects and code that should not be reactive.

Effect Events let you fix many patterns where you might be tempted to suppress the dependency linter.
https://react.dev/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events

Limitations of Effect Events?
https://react.dev/learn/separating-events-from-effects#limitations-of-effect-events
Effect Events are very limited in how you can use them:

  • Only call them from inside Effects.
  • Never pass them to other components or Hooks.

Removing Effect dependencies

Reactive values include props and all variables and functions declared directly inside of your component. Since roomId is a reactive value, you can’t remove it from the dependency list.

To remove a dependency, “prove” to the linter that it doesn’t need to be a dependency.

For example, you can move roomId out of your component to prove that it’s not reactive and won’t change on re-renders:

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore

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

If you want to change the dependencies, change the surrounding code first.

If you have an existing codebase, you might have some Effects that suppress the linter like this:

useEffect(() => {
  // ...
  // 🔴 Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

When dependencies don’t match the code, there is a very high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on.
Instead, use the techniques below.

Every time you adjust the Effect’s dependencies to reflect the code, look at the dependency list. Does it make sense for the Effect to re-run when any of these dependencies change? Sometimes, the answer is “no”:

  • You might want to re-execute different parts of your Effect under different conditions.
  • You might want to only read the latest value of some dependency instead of “reacting” to its changes.
  • A dependency may change too often unintentionally because it’s an object or a function.
    To find the right solution, you’ll need to answer a few questions about your Effect. Let’s walk through them.
  1. Should this code move to an event handler?
  2. Is your Effect doing several unrelated things?
  3. Are you reading some state to calculate the next state?
  4. Do you want to read a value without “reacting” to its changes?
  5. Does some reactive value change unintentionally? This problem only affects objects and functions. In JavaScript, each newly created object and function is considered distinct from all the others. It doesn’t matter that the contents inside of them may be the same! Object and function dependencies can make your Effect re-synchronize more often than you need. This is why, whenever possible, you should try to avoid objects and functions as your Effect’s dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them.
    https://react.dev/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally

Reusing logic with custom Hooks

React comes with several built-in Hooks like useState, useContext, and useEffect.

Sometimes, you’ll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room. You might not find these Hooks in React, but you can create your own Hooks for your application’s needs.

https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-sharing-logic-between-components

When you extract logic into custom Hooks, you can hide the gnarly details of how you deal with some external system or a browser API. The code of your components expresses your intent, not the implementation.

React applications are built from components. Components are built from Hooks.

You must follow these naming conventions:

  1. React component names must start with a capital letter, like StatusBar and SaveButton. React components also need to return something that React knows how to display, like a piece of JSX.
  2. Hook names must start with use followed by a capital letter, like useState (built-in) or useOnlineStatus (custom). Hooks may return arbitrary values.

Custom Hooks let you share stateful logic, not state itself !!
https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-let-you-share-stateful-logic-not-state-itself

function StatusBar() {
  const isOnline = useOnlineStatus();
  // ...
}

function SaveButton() {
  const isOnline = useOnlineStatus();
  // ...
}

It works the same way as before you extracted the duplication:

function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    // ...
  }, []);
  // ...
}

These are two completely independent state variables and Effects!

Custom Hooks let you share stateful logic but not state itself. Each call to a Hook is completely independent from every other call to the same Hook. When you need to share the state itself between multiple components, lift it up and pass it down instead.

Passing reactive values between Hooks?

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

Passing event handlers to custom Hooks? useEffectEvent!!

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
  const onMessage = useEffectEvent(onReceiveMessage);

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    connection.on('message', (msg) => {
      onMessage(msg);
    });
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // ✅ All dependencies declared
}

When to use custom Hooks?
https://react.dev/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks

You don’t need to extract a custom Hook for every little duplicated bit of code. Some duplication is fine.

However, whenever you write an Effect, consider whether it would be clearer to also wrap it in a custom Hook.

For example, consider a ShippingForm component that displays two dropdowns: one shows the list of cities, and another shows the list of areas in the selected city. You might start with some code that looks like this:

function ShippingForm({ country }) {
  const [cities, setCities] = useState(null);
  // This Effect fetches cities for a country
  useEffect(() => {
    let ignore = false;
    fetch(`/api/cities?country=${country}`)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setCities(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [country]);

  const [city, setCity] = useState(null);
  const [areas, setAreas] = useState(null);
  // This Effect fetches areas for the selected city
  useEffect(() => {
    if (city) {
      let ignore = false;
      fetch(`/api/areas?city=${city}`)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setAreas(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [city]);

  // ...

Although this code is quite repetitive, it’s correct to keep these Effects separate from each other. They synchronize two different things, so you shouldn’t merge them into one Effect. Instead, you can simplify the ShippingForm component above by extracting the common logic between them into your own useData Hook:

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    if (url) {
      let ignore = false;
      fetch(url)
        .then(response => response.json())
        .then(json => {
          if (!ignore) {
            setData(json);
          }
        });
      return () => {
        ignore = true;
      };
    }
  }, [url]);
  return data;
}

function ShippingForm({ country }) {
  const cities = useData(`/api/cities?country=${country}`);
  const [city, setCity] = useState(null);
  const areas = useData(city ? `/api/areas?city=${city}` : null);
  // ...

Extracting a custom Hook makes the data flow explicit.

By “hiding” your Effect inside useData, you also prevent someone working on the ShippingForm component from adding unnecessary dependencies to it

With time, most of your app’s Effects will be in custom Hooks!!

Keep your custom Hooks focused on concrete high-level use cases?
https://react.dev/learn/reusing-logic-with-custom-hooks#keep-your-custom-hooks-focused-on-concrete-high-level-use-cases

  • Start by choosing your custom Hook’s name.
  • Avoid creating and using custom “lifecycle” Hooks that act as alternatives and convenience wrappers for the useEffect API itself.
  • If you’re writing an Effect, start by using the React API directly. Then, you can (but don’t have to) extract custom Hooks for different high-level use cases.
    A good custom Hook makes the calling code more declarative by constraining what it does. For example, useChatRoom(options) can only connect to the chat room, while useImpressionLog(eventName, extraData) can only send an impression log to the analytics.

Custom Hooks help you migrate to better patterns?
With time, the React team’s goal is to reduce the number of the Effects in your app to the minimum by providing more specific solutions to more specific problems.
Wrapping your Effects in custom Hooks makes it easier to upgrade your code when these solutions become available.

This is another reason for why wrapping Effects in custom Hooks is often beneficial:

  1. You make the data flow to and from your Effects very explicit.
  2. You let your components focus on the intent rather than on the exact implementation of your Effects.
  3. When React adds new features, you can remove those Effects without changing any of your components.

This will keep your components’ code focused on the intent, and let you avoid writing raw Effects very often.

Many excellent custom Hooks are maintained by the React community.

Effects let you connect React to external systems. The more coordination between Effects is needed (for example, to chain multiple animations), the more it makes sense to extract that logic out of Effects and Hooks completely
Then, the code you extracted becomes the “external system”. This lets your Effects stay simple because they only need to send messages to the system you’ve moved outside React.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值