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.

Bạn sẽ được học

  • What custom Hooks are, and how to write your own
  • How to reuse logic between components
  • How to name and structure your custom Hooks
  • When and why to extract custom Hooks

Custom Hooks: Sharing logic between components

Imagine you’re developing an app that heavily relies on the network (as most apps do). You want to warn the user if their network connection has accidentally gone off while they were using your app. How would you go about it? It seems like you’ll need two things in your component:

  1. A piece of state that tracks whether the network is online.
  2. An Effect that subscribes to the global online and offline events, and updates that state.

This will keep your component synchronized with the network status. You might start with something like this:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

Try turning your network on and off, and notice how this StatusBar updates in response to your actions.

Now imagine you also want to use the same logic in a different component. You want to implement a Save button that will become disabled and show “Reconnecting…” instead of “Save” while the network is off.

To start, you can copy and paste the isOnline state and the Effect into SaveButton:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

Verify that, if you turn off the network, the button will change its appearance.

These two components work fine, but the duplication in logic between them is unfortunate. It seems like even though they have different visual appearance, you want to reuse the logic between them.

Extracting your own custom Hook from a component

Imagine for a moment that, similar to useState and useEffect, there was a built-in useOnlineStatus Hook. Then both of these components could be simplified and you could remove the duplication between them:

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progress saved');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}

Although there is no such built-in Hook, you can write it yourself. Declare a function called useOnlineStatus and move all the duplicated code into it from the components you wrote earlier:

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

At the end of the function, return isOnline. This lets your components read that value:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

Verify that switching the network on and off updates both components.

Now your components don’t have as much repetitive logic. More importantly, the code inside them describes what they want to do (use the online status!) rather than how to do it (by subscribing to the browser events).

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.

Hook names always start with use

React applications are built from components. Components are built from Hooks, whether built-in or custom. You’ll likely often use custom Hooks created by others, but occasionally you might write one yourself!

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, like earlier on the page). Hooks may return arbitrary values.

This convention guarantees that you can always look at a component and know where its state, Effects, and other React features might “hide”. For example, if you see a getColor() function call inside your component, you can be sure that it can’t possibly contain React state inside because its name doesn’t start with use. However, a function call like useOnlineStatus() will most likely contain calls to other Hooks inside!

Note

If your linter is configured for React, it will enforce this naming convention. Scroll up to the sandbox above and rename useOnlineStatus to getOnlineStatus. Notice that the linter won’t allow you to call useState or useEffect inside of it anymore. Only Hooks and components can call other Hooks!

Tìm hiểu sâu

Should all functions called during rendering start with the use prefix?

No. Functions that don’t call Hooks don’t need to be Hooks.

If your function doesn’t call any Hooks, avoid the use prefix. Instead, write it as a regular function without the use prefix. For example, useSorted below doesn’t call Hooks, so call it getSorted instead:

// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
return items.slice().sort();
}

This ensures that your code can call this regular function anywhere, including conditions:

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ It's ok to call getSorted() conditionally because it's not a Hook
displayedItems = getSorted(items);
}
// ...
}

You should give use prefix to a function (and thus make it a Hook) if it uses at least one Hook inside of it:

// ✅ Good: A Hook that uses other Hooks
function useAuth() {
return useContext(Auth);
}

Technically, this isn’t enforced by React. In principle, you could make a Hook that doesn’t call other Hooks. This is often confusing and limiting so it’s best to avoid that pattern. However, there may be rare cases where it is helpful. For example, maybe your function doesn’t use any Hooks right now, but you plan to add some Hook calls to it in the future. Then it makes sense to name it with the use prefix:

// ✅ Good: A Hook that will likely use some other Hooks later
function useAuth() {
// TODO: Replace with this line when authentication is implemented:
// return useContext(Auth);
return TEST_USER;
}

Then components won’t be able to call it conditionally. This will become important when you actually add Hook calls inside. If you don’t plan to use Hooks inside it (now or later), don’t make it a Hook.

Custom Hooks let you share stateful logic, not state itself

In the earlier example, when you turned the network on and off, both components updated together. However, it’s wrong to think that a single isOnline state variable is shared between them. Look at this code:

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! They happened to have the same value at the same time because you synchronized them with the same external value (whether the network is on).

To better illustrate this, we’ll need a different example. Consider this Form component:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

There’s some repetitive logic for each form field:

  1. There’s a piece of state (firstName and lastName).
  2. There’s a change handler (handleFirstNameChange and handleLastNameChange).
  3. There’s a piece of JSX that specifies the value and onChange attributes for that input.

You can extract the repetitive logic into this useFormInput custom Hook:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

Notice that it only declares one state variable called value.

However, the Form component calls useFormInput two times:

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

This is why it works like declaring two separate state variables!

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. This is why the two sandboxes above are completely equivalent. If you’d like, scroll back up and compare them. The behavior before and after extracting a custom Hook is identical.

When you need to share the state itself between multiple components, lift it up and pass it down instead.

Passing reactive values between Hooks

The code inside your custom Hooks will re-run during every re-render of your component. This is why, like components, custom Hooks need to be pure. Think of custom Hooks’ code as part of your component’s body!

Because custom Hooks re-render together with your component, they always receive the latest props and state. To see what this means, consider this chat room example. Change the server URL or the chat room:

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

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

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

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

When you change serverUrl or roomId, the Effect “reacts” to your changes and re-synchronizes. You can tell by the console messages that the chat re-connects every time that you change your Effect’s dependencies.

Now move the Effect’s code into a custom Hook:

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

This lets your ChatRoom component call your custom Hook without worrying about how it works inside:

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

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

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

This looks much simpler! (But it does the same thing.)

Notice that the logic still responds to prop and state changes. Try editing the server URL or the selected room:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

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

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

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

Notice how you’re taking the return value of one Hook:

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

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

and pass it as an input to another Hook:

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

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

Every time your ChatRoom component re-renders, it passes the latest roomId and serverUrl to your Hook. This is why your Effect re-connects to the chat whenever their values are different after a re-render. (If you ever worked with audio or video processing software, chaining Hooks like this might remind you of chaining visual or audio effects. It’s as if the output of useState “feeds into” the input of the useChatRoom.)

Passing event handlers to custom Hooks

Under Construction

This section describes an experimental API that has not yet been released in a stable version of React.

As you start using useChatRoom in more components, you might want to let components customize its behavior. For example, currently, the logic for what to do when a message arrives is hardcoded inside the Hook:

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

Let’s say you want to move this logic back to your component:

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

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
// ...

To make this work, change your custom Hook to take onReceiveMessage as one of its named options:

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

This will work, but there’s one more improvement you can do when your custom Hook accepts event handlers.

Adding a dependency on onReceiveMessage is not ideal because it will cause the chat to re-connect every time the component re-renders. Wrap this event handler into an Effect Event to remove it from the dependencies:

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
}

Now the chat won’t re-connect every time that the ChatRoom component re-renders. Here is a fully working demo of passing an event handler to a custom Hook that you can play with:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

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

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });

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

Lưu ý rằng bạn không còn cần phải biết cách useChatRoom hoạt động để sử dụng nó. Bạn có thể thêm nó vào bất kỳ component nào khác, truyền bất kỳ tùy chọn nào khác và nó sẽ hoạt động theo cùng một cách. Đó là sức mạnh của Custom Hook.

Khi nào nên sử dụng Custom Hook

Bạn không cần phải trích xuất một Custom Hook cho mọi đoạn code trùng lặp nhỏ. Một số trùng lặp là chấp nhận được. Ví dụ: trích xuất một Hook useFormInput để bọc một lệnh gọi useState duy nhất như trước đó có lẽ là không cần thiết.

Tuy nhiên, bất cứ khi nào bạn viết một Effect, hãy cân nhắc xem liệu việc bọc nó trong một Custom Hook có rõ ràng hơn không. Bạn không nên cần Effect quá thường xuyên, vì vậy nếu bạn đang viết một Effect, điều đó có nghĩa là bạn cần “bước ra ngoài React” để đồng bộ hóa với một số hệ thống bên ngoài hoặc để làm điều gì đó mà React không có API tích hợp sẵn. Việc bọc nó vào một Custom Hook cho phép bạn truyền đạt chính xác ý định của mình và cách dữ liệu luân chuyển qua nó.

Ví dụ: hãy xem xét một component ShippingForm hiển thị hai dropdown: một hiển thị danh sách các thành phố và một hiển thị danh sách các khu vực trong thành phố đã chọn. Bạn có thể bắt đầu với một số code trông như thế này:

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]);

// ...

Mặc dù code này khá lặp đi lặp lại, việc giữ các Effect này tách biệt nhau là đúng. Chúng đồng bộ hóa hai thứ khác nhau, vì vậy bạn không nên hợp nhất chúng thành một Effect. Thay vào đó, bạn có thể đơn giản hóa component ShippingForm ở trên bằng cách trích xuất logic chung giữa chúng vào Hook useData của riêng bạn:

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

Bây giờ bạn có thể thay thế cả hai Effect trong các component ShippingForm bằng các lệnh gọi đến useData:

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

Việc trích xuất một Custom Hook làm cho luồng dữ liệu trở nên rõ ràng. Bạn đưa url vào và bạn nhận được data ra. Bằng cách “ẩn” Effect của bạn bên trong useData, bạn cũng ngăn ai đó làm việc trên component ShippingForm thêm các dependency không cần thiết vào nó. Theo thời gian, hầu hết các Effect của ứng dụng của bạn sẽ nằm trong Custom Hook.

Tìm hiểu sâu

Tập trung Custom Hook của bạn vào các trường hợp sử dụng cấp cao cụ thể

Bắt đầu bằng cách chọn tên cho Custom Hook của bạn. Nếu bạn gặp khó khăn trong việc chọn một cái tên rõ ràng, điều đó có thể có nghĩa là Effect của bạn quá gắn liền với phần còn lại của logic component của bạn và chưa sẵn sàng để được trích xuất.

Lý tưởng nhất là tên Custom Hook của bạn phải đủ rõ ràng để ngay cả một người không viết code thường xuyên cũng có thể đoán được Custom Hook của bạn làm gì, nó nhận gì và nó trả về gì:

  • useData(url)
  • useImpressionLog(eventName, extraData)
  • useChatRoom(options)

Khi bạn đồng bộ hóa với một hệ thống bên ngoài, tên Custom Hook của bạn có thể mang tính kỹ thuật hơn và sử dụng biệt ngữ dành riêng cho hệ thống đó. Điều đó tốt miễn là nó rõ ràng đối với một người quen thuộc với hệ thống đó:

  • useMediaQuery(query)
  • useSocket(url)
  • useIntersectionObserver(ref, options)

Tập trung Custom Hook vào các trường hợp sử dụng cấp cao cụ thể. Tránh tạo và sử dụng Custom Hook “vòng đời” hoạt động như các giải pháp thay thế và trình bao bọc tiện lợi cho chính API useEffect:

  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

Ví dụ: Hook useMount này cố gắng đảm bảo một số code chỉ chạy “khi mount”:

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

// 🔴 Tránh: sử dụng Custom Hook "vòng đời"
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔴 Tránh: tạo Custom Hook "vòng đời"
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
}

Custom Hook “vòng đời” như useMount không phù hợp với mô hình React. Ví dụ: ví dụ code này có một lỗi (nó không “phản ứng” với các thay đổi roomId hoặc serverUrl), nhưng trình lint sẽ không cảnh báo bạn về điều đó vì trình lint chỉ kiểm tra các lệnh gọi useEffect trực tiếp. Nó sẽ không biết về Hook của bạn.

Nếu bạn đang viết một Effect, hãy bắt đầu bằng cách sử dụng trực tiếp API React:

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

// ✅ Tốt: hai Effect thô được phân tách theo mục đích

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

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

Sau đó, bạn có thể (nhưng không bắt buộc) trích xuất Custom Hook cho các trường hợp sử dụng cấp cao khác nhau:

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

// ✅ Tuyệt vời: Custom Hook được đặt tên theo mục đích của chúng
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

Một Custom Hook tốt làm cho code gọi trở nên khai báo hơn bằng cách hạn chế những gì nó làm. Ví dụ: useChatRoom(options) chỉ có thể kết nối với phòng chat, trong khi useImpressionLog(eventName, extraData) chỉ có thể gửi nhật ký hiển thị đến phân tích. Nếu API Custom Hook của bạn không hạn chế các trường hợp sử dụng và rất trừu tượng, về lâu dài, nó có khả năng gây ra nhiều vấn đề hơn là giải quyết.

Custom Hook giúp bạn di chuyển sang các pattern tốt hơn

Effect là một “lối thoát hiểm”: bạn sử dụng chúng khi bạn cần “bước ra ngoài React” và khi không có giải pháp tích hợp tốt hơn cho trường hợp sử dụng của bạn. Theo thời gian, mục tiêu của nhóm React là giảm số lượng Effect trong ứng dụng của bạn xuống mức tối thiểu bằng cách cung cấp các giải pháp cụ thể hơn cho các vấn đề cụ thể hơn. Việc bọc Effect của bạn trong Custom Hook giúp bạn dễ dàng nâng cấp code của mình hơn khi các giải pháp này có sẵn.

Hãy quay lại ví dụ này:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

Trong ví dụ trên, useOnlineStatus được triển khai với một cặp useStateuseEffect. Tuy nhiên, đây không phải là giải pháp tốt nhất có thể. Có một số trường hợp đặc biệt mà nó không xem xét. Ví dụ: nó giả định rằng khi component mount, isOnline đã là true, nhưng điều này có thể sai nếu mạng đã ngoại tuyến. Bạn có thể sử dụng API navigator.onLine của trình duyệt để kiểm tra điều đó, nhưng việc sử dụng trực tiếp nó sẽ không hoạt động trên máy chủ để tạo HTML ban đầu. Tóm lại, code này có thể được cải thiện.

React bao gồm một API chuyên dụng có tên là useSyncExternalStore để giải quyết tất cả những vấn đề này cho bạn. Đây là Hook useOnlineStatus của bạn, được viết lại để tận dụng API mới này:

import { useSyncExternalStore } from 'react';

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

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

Lưu ý rằng bạn không cần phải thay đổi bất kỳ component nào để thực hiện việc di chuyển này:

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

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

Đây là một lý do khác tại sao việc bọc Effect trong Custom Hook thường có lợi:

  1. Bạn làm cho luồng dữ liệu đến và đi từ Effect của bạn rất rõ ràng.
  2. Bạn cho phép các component của bạn tập trung vào ý định hơn là vào việc triển khai chính xác Effect của bạn.
  3. Khi React thêm các tính năng mới, bạn có thể xóa các Effect đó mà không cần thay đổi bất kỳ component nào của bạn.

Tương tự như một hệ thống thiết kế, bạn có thể thấy hữu ích khi bắt đầu trích xuất các thành ngữ phổ biến từ các component của ứng dụng của bạn vào Custom Hook. Điều này sẽ giúp code của các component của bạn tập trung vào ý định và cho phép bạn tránh viết Effect thô rất thường xuyên. Nhiều Custom Hook tuyệt vời được duy trì bởi cộng đồng React.

Tìm hiểu sâu

React sẽ cung cấp bất kỳ giải pháp tích hợp sẵn nào để tìm nạp dữ liệu không?

Chúng tôi vẫn đang hoàn thiện các chi tiết, nhưng chúng tôi hy vọng rằng trong tương lai, bạn sẽ viết code tìm nạp dữ liệu như thế này:

import { use } from 'react'; // Chưa có sẵn!

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

Nếu bạn sử dụng Custom Hook như useData ở trên trong ứng dụng của mình, bạn sẽ yêu cầu ít thay đổi hơn để di chuyển sang phương pháp được đề xuất cuối cùng so với việc bạn viết Effect thô trong mọi component theo cách thủ công. Tuy nhiên, phương pháp cũ vẫn sẽ hoạt động tốt, vì vậy nếu bạn cảm thấy hài lòng khi viết Effect thô, bạn có thể tiếp tục làm điều đó.

Có nhiều hơn một cách để làm điều đó

Giả sử bạn muốn triển khai một hiệu ứng hoạt ảnh mờ dần từ đầu bằng API requestAnimationFrame của trình duyệt. Bạn có thể bắt đầu với một Effect thiết lập một vòng lặp hoạt ảnh. Trong mỗi khung hình của hoạt ảnh, bạn có thể thay đổi độ mờ của nút DOM mà bạn giữ trong một ref cho đến khi nó đạt đến 1. Code của bạn có thể bắt đầu như thế này:

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

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // We still have more frames to paint
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Để làm cho component dễ đọc hơn, bạn có thể trích xuất logic vào một Custom Hook useFadeIn:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Bạn có thể giữ code useFadeIn như hiện tại, nhưng bạn cũng có thể tái cấu trúc nó nhiều hơn. Ví dụ: bạn có thể trích xuất logic để thiết lập vòng lặp hoạt ảnh ra khỏi useFadeIn vào một Hook useAnimationLoop tùy chỉnh:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

Tuy nhiên, bạn không bắt buộc phải làm điều đó. Như với các hàm thông thường, cuối cùng bạn quyết định nơi vẽ ranh giới giữa các phần khác nhau của code của bạn. Bạn cũng có thể thực hiện một cách tiếp cận rất khác. Thay vì giữ logic trong Effect, bạn có thể di chuyển hầu hết logic mệnh lệnh bên trong một class: JavaScript:

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

Effect cho phép bạn kết nối React với các hệ thống bên ngoài. Càng cần nhiều sự phối hợp giữa các Effect (ví dụ: để xâu chuỗi nhiều hoạt ảnh), thì càng có ý nghĩa khi trích xuất logic đó ra khỏi Effect và Hook hoàn toàn như trong sandbox ở trên. Sau đó, code bạn đã trích xuất trở thành “hệ thống bên ngoài”. Điều này cho phép Effect của bạn luôn đơn giản vì chúng chỉ cần gửi tin nhắn đến hệ thống bạn đã di chuyển ra ngoài React.

Các ví dụ trên giả định rằng logic mờ dần cần được viết bằng JavaScript. Tuy nhiên, hoạt ảnh mờ dần cụ thể này vừa đơn giản hơn vừa hiệu quả hơn nhiều để triển khai với một Hoạt ảnh CSS: đơn giản:

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Đôi khi, bạn thậm chí không cần một Hook!

Tóm tắt

  • Custom Hooks cho phép bạn chia sẻ logic giữa các component.
  • Custom Hooks phải được đặt tên bắt đầu bằng use theo sau là một chữ cái viết hoa.
  • Custom Hooks chỉ chia sẻ logic có trạng thái, không phải bản thân trạng thái.
  • Bạn có thể truyền các giá trị phản ứng từ Hook này sang Hook khác và chúng luôn được cập nhật.
  • Tất cả các Hook chạy lại mỗi khi component của bạn re-render.
  • Code của custom Hooks của bạn phải thuần khiết, giống như code của component của bạn.
  • Bọc các trình xử lý sự kiện nhận được bởi custom Hooks vào Effect Events.
  • Không tạo custom Hooks như useMount. Giữ cho mục đích của chúng cụ thể.
  • Tùy thuộc vào bạn cách và nơi chọn ranh giới code của bạn.

Challenge 1 of 5:
Trích xuất một Hook useCounter

Component này sử dụng một biến trạng thái và một Effect để hiển thị một số tăng lên mỗi giây. Trích xuất logic này vào một custom Hook có tên là useCounter. Mục tiêu của bạn là làm cho việc triển khai component Counter trông giống hệt như sau:

export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}

Bạn sẽ cần viết custom Hook của mình trong useCounter.js và import nó vào file App.js.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Seconds passed: {count}</h1>;
}