ReactJs

React 19: <Activity> Component

Ngọc Tiên
9
React 19: &lt;Activity><activity> Component</activity>

🫠 Bối cảnh: Ác mộng Conditional Rendering

Trước đây, khi cần ẩn/hiện một component (ví dụ: Sidebar, Tab không active, Modal ẩn), chúng ta có hai cách chính:

  1. Conditional Rendering (&& hoặc ? :): Component bị unmount (gỡ khỏi cây DOM) khi ẩn.
    • Ưu điểm: Sạch sẽ, không tốn tài nguyên.
    • Nhược điểm: Khi hiện lại, component phải mount lại từ đầu (render lại, chạy lại useEffect, state bị mất, DOM state như scroll position cũng bay màu). Worst case: Component đó cần data hoặc rất nặng thì UX cứ gọi là “lag lòi”.
  2. CSS (display: none): Component vẫn ở trong cây DOM, chỉ ẩn đi bằng CSS.
    • Ưu điểm: State và DOM state được bảo toàn, hiện lại siêu nhanh.
    • Nhược điểm: Mặc dù ẩn, component vẫn re-render và chạy Effect (timers, subscriptions…) khi props thay đổi, gây lãng phí tài nguyên và đôi khi tạo ra side effect không mong muốn.

<Activity>: Chơi Lớn, Khác Bọt!

Component <Activity> chính là “bản nâng cấp hoàn hảo” để giải quyết cả hai vấn đề trên. Nó giống như việc bạn tạm thời “mute” một component mà vẫn giữ nguyên trạng thái, không làm ảnh hưởng đến hiệu năng của phần còn lại.

Bạn chỉ cần bọc component cần ẩn/hiện và dùng prop mode:

<Activity mode={isActive ? 'visible' : 'hidden'}>
  <HeavyComponent />
</Activity>

🤯 Cơ chế “Vi diệu” (Không chỉ là display: none đâu!):

Khi mode="hidden", <Activity> làm những điều “đỉnh của chóp” sau:

  • ✅ Bảo toàn State và DOM: Giống như CSS, component vẫn ở trong cây DOM (với display: none ở ngoài cùng), giữ nguyên state nội bộ (ví dụ: giá trị input, scroll position) và DOM state.
  • ❌ Unmount Effects: Khác biệt lớn nhất! React sẽ tự động chạy cleanup function của tất cả các Effect (ví dụ: useEffect, useLayoutEffect) bên trong component bị ẩn. Tạm biệt những timer chạy vô ích hay subscription API liên tục gọi data trong lúc component không ai thèm nhìn!
  • 📉 Deprioritize Re-renders: Khi có update, React sẽ giảm độ ưu tiên render cho component bị ẩn này. Nó vẫn có thể render, nhưng sẽ chậm hơn so với các component đang visible. Giúp UI chính luôn mượt mà, không bị chặn.
  • 🚀 Pre-rendering Thông minh: Thậm chí, khi <Activity> lần đầu mount với mode="hidden", nó vẫn render ở background (low priority) và thực hiện data fetching (nếu dùng Suspense), nên khi bạn chuyển sang visible, component gần như đã sẵn sàng 100%! UX “max ping” luôn!

1. 📂 Ví dụ với Tabs (Thẻ giao diện)

Đây là trường hợp kinh điển nhất mà <Activity> tỏa sáng.

💡 Vấn đề truyền thống

Khi chuyển Tab, nếu dùng Conditional Rendering (ẩn/hiện):

  • Tab cũ bị Unmount, mất hết state (ví dụ: form đã điền, scroll position).
  • Tab mới bị Mount lại từ đầu, phải render lại và fetch data (nếu có).

Nếu dùng CSS (display: none):

  • Các Tab ẩn vẫn chạy Effects (ví dụ: timer, listener) và re-render khi có data global thay đổi, gây lãng phí tài nguyên.

🚀 Giải pháp với <Activity>

Sử dụng <Activity> sẽ giúp giữ statetạm dừng Effects của các Tab không hoạt động.

JavaScript

function TabManager({ activeTab }) {
  // activeTab = 'profile', 'settings', 'notifications'

  return (
    <div>
      {/* Tab 1: Profile */}
      <Activity mode={activeTab === 'profile' ? 'visible' : 'hidden'}>
        <UserProfileTab /> {/* Component có form, scrollbar, data fetching */}
      </Activity>

      {/* Tab 2: Settings */}
      <Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
        <SettingsTab /> {/* Component có nhiều toggles, useEffect cho setting live */}
      </Activity>

      {/* Tab 3: Notifications */}
      <Activity mode={activeTab === 'notifications' ? 'visible' : 'hidden'}>
        <NotificationsTab /> {/* Component có timer đếm ngược, subscription */}
      </Activity>
    </div>
  );
}

// Bên trong SettingsTab.jsx
function SettingsTab() {
  // ⛔️ Effect này sẽ bị TẠM DỪNG (cleanup) khi mode là 'hidden'
  useEffect(() => {
    const interval = setInterval(() => {
      console.log('Cập nhật trạng thái tự động...');
    }, 5000);
    return () => clearInterval(interval);
  }, []);

  return (
    // ... JSX render form settings
    <div>Trang Cài đặt. State (Input, Checkbox) sẽ được giữ nguyên khi ẩn.</div>
  );
}

Kết quả: Khi bạn chuyển từ Profile ➡ Settings và quay lại Profile, form và scroll position của Profile vẫn còn. Quan trọng hơn, khi Settings bị ẩn, interval timer sẽ dừng (nhờ cleanup Effect), tránh lãng phí.


2. 🪟 Ví dụ với Modal/Dialog nặng

Các Modal hoặc Dialog thường chứa form phức tạp, editor hoặc component từ bên thứ ba (ví dụ: bản đồ, trình chỉnh sửa).

💡 Vấn đề truyền thống

Bạn không muốn Modal bị Unmount (để giữ state form đã điền) nhưng cũng không muốn nó lãng phí tài nguyên khi ẩn.

🚀 Giải pháp với <Activity>

Bọc nội dung Modal bằng <Activity> để giữ state khi Modal bị đóng nhưng không bị gỡ khỏi DOM.

JavaScript

function EditPostModal({ isOpen, onClose }) {
  return (
    <Modal isVisible={isOpen}>
      {/* Dùng Activity để bọc nội dung phức tạp bên trong */}
      <Activity mode={isOpen ? 'visible' : 'hidden'}>
        <ComplexTextEditor /> {/* Editor nặng có dùng useEffect để quản lý DOM */}
        <PostForm />
      </Activity>
    </Modal>
  );
}

// Bên trong ComplexTextEditor.jsx
function ComplexTextEditor() {
  // ⛔️ Effect quản lý DOM (listener, scroll observer...) sẽ bị TẠM DỪNG khi Modal đóng
  useLayoutEffect(() => {
    // Khởi tạo thư viện Editor bên thứ 3, gắn listener vào DOM
    console.log('Editor được khởi tạo.');
    return () => {
      // Hủy thư viện Editor, gỡ listener khỏi DOM
      console.log('Editor bị hủy.');
    };
  }, []);

  return (
    <textarea defaultValue="State nội dung đã được điền sẽ được giữ lại."></textarea>
  );
}

Kết quả:

  • Khi isOpenfalse, ComplexTextEditor vẫn nằm trong DOM (giữ lại nội dung và DOM state) nhưng tất cả useLayoutEffect/useEffect của nó bị tạm dừng.
  • Khi mở lại Modal, Editor hiện ra ngay lập tức với state cũ, không tốn thời gian mount lại và khởi tạo thư viện nặng.

3. 🌐 Ví dụ với Pre-rendering (Kết hợp với Suspense)

Đây là tính năng mạnh mẽ nhất, đặc biệt khi dùng với data fetching.

JavaScript

// Giả định dùng React Suspense để fetch data
const resource = fetchUserData(); 

function UserDetail({ userId }) {
  // ⚠️ Đây là nơi data fetching diễn ra
  const user = resource.read(); 
  
  // Component nặng về render
  return <HeavyUserDashboard user={user} />;
}


function App() {
  // Ban đầu, Activity mode là 'hidden'
  // React 19 có thể bắt đầu fetch data cho <UserDetail> với priority thấp
  // ngay cả khi nó bị ẩn.
  return (
    <Activity mode={'hidden'}> 
      <Suspense fallback={<div>Loading User Data...</div>}>
        <UserDetail userId={123} />
      </Suspense>
    </Activity>
  );
  // Sau đó, khi cần hiện ra (ví dụ: sau khi đăng nhập), 
  // bạn chuyển mode sang 'visible'. Data có thể đã sẵn sàng!
}

Kết quả: Khi bạn chuyển mode từ hidden sang visible, data cho UserDetail có thể đã được fetch xong nhờ cơ chế ưu tiên thấp của <Activity> kết hợp với Suspense, giúp component hiện ra gần như tức thì (instant UI).