Bài 23.2: React.memo trong TypeScript

Chào mừng trở lại với series blog của chúng ta! Hôm nay, chúng ta sẽ cùng khám phá một công cụ cực kỳ mạnh mẽ để tối ưu hiệu suất ứng dụng React của bạn, đặc biệt khi làm việc với TypeScript: React.memo.

Bạn đã bao giờ gặp tình huống giao diện người dùng của mình trở nên chậm chạp, giật lag, đặc biệt khi ứng dụng trở nên phức tạp hơn với nhiều component lồng nhau? Một trong những nguyên nhân phổ biến là hiện tượng re-render không cần thiết. Khi trạng thái (state) hoặc thuộc tính (props) của một component cha thay đổi, React mặc định sẽ re-render component cha đó, và đồng thời, tất cả các component con của nó cũng sẽ bị re-render theo, bất kể props của component con có thay đổi hay không. Đây chính là lúc React.memo tỏa sáng!

Re-render Không Cần Thiết Là Gì và Tại Sao Nó Là Vấn Đề?

Trong React, quá trình render là việc React tính toán UI sẽ trông như thế nào dựa trên state và props hiện tại. Khi state hoặc props thay đổi, React sẽ thực hiện lại quá trình này (gọi là re-render) và cập nhật DOM nếu cần.

Vấn đề phát sinh khi một component re-render trong khi output của nó không thay đổi. Ví dụ, bạn có một component cha quản lý nhiều phần dữ liệu, và một trong số đó thay đổi. Component cha re-render. Nếu nó có một component con hiển thị dữ liệu khác và dữ liệu đó không hề thay đổi, component con đó vẫn bị re-render. Nếu component con này hoặc các component cháu chắt của nó phức tạp và tốn kém tài nguyên để render (ví dụ: chứa các phép tính phức tạp, duyệt qua danh sách lớn...), việc re-render liên tục mà không có lý do sẽ làm chậm ứng dụng của bạn.

Hãy tưởng tượng một component DanhSachSanPham (Product List) có một component con SanPham (Product) hiển thị thông tin chi tiết của từng sản phẩm. Nếu bạn có một chức năng "cập nhật trạng thái đơn hàng" ở component cha làm thay đổi state không liên quan đến danh sách sản phẩm, cả DanhSachSanPhamtất cả các component SanPham con của nó vẫn sẽ bị re-render, ngay cả khi thông tin sản phẩm hiển thị không hề thay đổi.

Giới Thiệu React.memo

React.memo là một Higher-Order Component (HOC) được React cung cấp để giúp tối ưu hóa hiệu suất của các functional component (component hàm). Nó hoạt động bằng cách ghi nhớ (memoize) kết quả render của một component. Nếu props của component không thay đổi kể từ lần render gần nhất, React sẽ bỏ qua việc render lại component đó và sử dụng lại kết quả đã ghi nhớ.

Nói cách khác, React.memo giúp component của bạn trở thành một dạng "pure component" phiên bản hàm. Nó sẽ chỉ re-render khi props của nó thực sự thay đổi.

Cách sử dụng rất đơn giản: bạn chỉ cần bọc component hàm của mình bằng React.memo.

```typescript jsx import React from 'react';

interface MyComponentProps { name: string; age: number; }

const MyComponent: React.FC<MyComponentProps> = ({ name, age }) => { console.log('MyComponent rendered'); // Dòng này sẽ giúp bạn biết khi nào component render return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); };

// Bọc component bằng React.memo export default React.memo(MyComponent);


Trong ví dụ trên, component `MyComponent` bây giờ đã được *memoize*. React sẽ kiểm tra props (`name`, `age`) mỗi khi component cha re-render. Nếu `name` và `age` không thay đổi so với lần trước, React sẽ không gọi hàm `MyComponent` để render lại, và dòng `console.log('MyComponent rendered')` sẽ không xuất hiện.

### `React.memo` Hoạt Động Như Thế Nào? So Sánh Nông (Shallow Comparison)

Mặc định, `React.memo` thực hiện việc so sánh props theo cơ chế **shallow comparison** (so sánh nông). Điều này có nghĩa là nó sẽ:

*   Đối với các kiểu dữ liệu nguyên thủy (primitive types) như string, number, boolean, null, undefined: So sánh giá trị (`===`). `5 === 5` (true), `'hello' === 'hello'` (true), `true === true` (true).
*   Đối với các kiểu dữ liệu không nguyên thủy (non-primitive types) như object, array, function: So sánh tham chiếu (reference). `{ a: 1 } === { a: 1 }` (false - vì chúng là hai đối tượng khác nhau trong bộ nhớ, dù có nội dung giống nhau), `[1, 2] === [1, 2]` (false), `() => {} === () => {}` (false).

Đây là điểm *quan trọng* cần hiểu. Nếu bạn truyền các props là object, array hoặc function được tạo *mới* trên mỗi lần render của component cha, `React.memo` sẽ nghĩ rằng props đã thay đổi (vì tham chiếu thay đổi) và component con vẫn sẽ bị re-render, làm mất đi tác dụng của `React.memo`.

### Ví Dụ Minh Họa: Khi `React.memo` Phát Huy Tác Dụng (Primitive Props)

Hãy xem một ví dụ cụ thể hơn với TypeScript.

Đầu tiên, component con của chúng ta:

```typescript jsx
// components/ChildComponent.tsx
import React from 'react';

interface ChildProps {
  id: number;
  label: string;
}

const ChildComponent: React.FC<ChildProps> = ({ id, label }) => {
  console.log(`ChildComponent ${id} rendered with label: ${label}`);
  return (
    <div style={{ border: '1px solid #ccc', margin: '5px', padding: '5px' }}>
      <p>ID: {id}</p>
      <p>Label: {label}</p>
    </div>
  );
};

// Bọc bằng React.memo
export default React.memo(ChildComponent);

Tiếp theo, component cha sử dụng component con:

```typescript jsx // components/ParentComponent.tsx import React, { useState } from 'react'; import ChildComponent from './ChildComponent';

const ParentComponent: React.FC = () => { const [count, setCount] = useState(0); const [otherState, setOtherState] = useState('initial'); // State không liên quan đến ChildComponent props

console.log('ParentComponent rendered');

return ( <div> <h2>Parent Component</h2> <p>Parent Count: {count}</p> <p>Other State: {otherState}</p>

  {/* Nút thay đổi state không liên quan đến ChildComponent props */}
  <button onClick={() => setOtherState('updated')}>
    Change Other State (Does not affect Child props)
  </button>

  {/* Nút thay đổi state liên quan đến ChildComponent props */}
   <button onClick={() => setCount(count + 1)}>
    Change Count (Affects Child props)
  </button>

  {/* ChildComponent được memoize */}
  <ChildComponent id={1} label={`Item ${count}`} /> {/* Prop 'label' phụ thuộc vào count */}
  <ChildComponent id={2} label="Static Item" /> {/* Prop 'label' không phụ thuộc vào count */}
</div>

); };

export default ParentComponent;


**Giải thích:**

*   Chúng ta có `ParentComponent` với hai state: `count` và `otherState`.
*   `ChildComponent` được bọc bằng `React.memo` và nhận props là `id` (number) và `label` (string).
*   Chúng ta render hai `ChildComponent`: một cái có `label` phụ thuộc vào `count`, cái kia có `label` cố định.
*   Khi bạn click "Change Other State", `otherState` của `ParentComponent` thay đổi. `ParentComponent` re-render. Tuy nhiên:
    *   `ChildComponent` với `id={1}` có prop `label` phụ thuộc vào `count`. `count` chưa thay đổi, nên `label` cũng chưa thay đổi. `React.memo` kiểm tra `id` và `label`, thấy cả hai đều *không* thay đổi (so sánh nông), nên *không* re-render `ChildComponent` này.
    *   `ChildComponent` với `id={2}` có prop `label` cố định là `"Static Item"`. Props `id` và `label` không thay đổi. `React.memo` kiểm tra, thấy *không* thay đổi, nên *không* re-render `ChildComponent` này.
    *   Bạn sẽ chỉ thấy `console.log('ParentComponent rendered')` xuất hiện.
*   Khi bạn click "Change Count", `count` của `ParentComponent` thay đổi. `ParentComponent` re-render.
    *   `ChildComponent` với `id={1}`: Prop `label` thay đổi (ví dụ: từ "Item 0" sang "Item 1"). `React.memo` kiểm tra, thấy `label` *đã thay đổi*, nên **re-render** `ChildComponent` này. Bạn sẽ thấy `console.log('ChildComponent 1 rendered...')` xuất hiện.
    *   `ChildComponent` với `id={2}`: Props `id` và `label` vẫn cố định và không thay đổi. `React.memo` kiểm tra, thấy *không* thay đổi, nên *không* re-render `ChildComponent` này. Bạn sẽ *không* thấy `console.log('ChildComponent 2 rendered...')` xuất hiện.

Đây là minh chứng rõ ràng cho việc `React.memo` giúp ngăn chặn re-render không cần thiết cho các component con khi props của chúng là kiểu nguyên thủy và không thay đổi.

### Thử Thách Với Non-Primitive Props (Object, Array, Function)

Như đã giải thích, vấn đề với so sánh nông là khi bạn truyền các object, array, hoặc function được tạo *mới* trên mỗi lần render của component cha. Hãy xem điều gì xảy ra.

Giả sử bạn truyền một object config cho component con:

```typescript jsx
// components/ObjectPropChild.tsx
import React from 'react';

interface Config {
  theme: string;
  fontSize: number;
}

interface ObjectChildProps {
  config: Config;
}

const ObjectPropChild: React.FC<ObjectChildProps> = ({ config }) => {
  console.log('ObjectPropChild rendered');
  return (
    <div style={{ border: '1px dashed blue', margin: '5px', padding: '5px' }}>
      <p>Theme: {config.theme}</p>
      <p>Font Size: {config.fontSize}</p>
    </div>
  );
};

export default React.memo(ObjectPropChild);

Và component cha sử dụng nó:

```typescript jsx // components/ParentWithObject.tsx import React, { useState } from 'react'; import ObjectPropChild from './ObjectPropChild';

const ParentWithObject: React.FC = () => { const [count, setCount] = useState(0);

console.log('ParentWithObject rendered');

// OBJECT NÀY ĐƯỢC TẠO MỚI TRÊN MỖI LẦN RENDER! const config = { theme: 'dark', fontSize: 16, };

return ( <div> <h2>Parent With Object</h2> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment Count </button> {/<em> Truyền object config </em>/} <ObjectPropChild config={config} /> </div> ); };

export default ParentWithObject;


**Vấn đề:** Mỗi khi bạn click "Increment Count", `ParentWithObject` re-render, và dòng `const config = { ... }` sẽ tạo ra một **object `config` mới** với *tham chiếu khác* so với lần render trước, ngay cả khi nội dung (`theme`, `fontSize`) không thay đổi. `React.memo` ở `ObjectPropChild` so sánh tham chiếu của object `config`, thấy nó khác, và *vẫn* re-render `ObjectPropChild`. Bạn sẽ thấy cả hai dòng `console.log` xuất hiện mỗi lần.

### Giải Quyết Với `useMemo` (Cho Object và Array)

Để khắc phục vấn đề này với object và array, chúng ta sử dụng hook **`useMemo`**. `useMemo` cho phép bạn *ghi nhớ giá trị* của một phép tính hoặc một object/array, và chỉ tính toán lại (hoặc tạo lại object/array) khi các dependencies của nó thay đổi.

```typescript jsx
// components/ParentWithObjectMemoized.tsx
import React, { useState, useMemo } from 'react'; // Import useMemo
import ObjectPropChild from './ObjectPropChild';

const ParentWithObjectMemoized: React.FC = () => {
  const [count, setCount] = useState(0);
  const [theme, setTheme] = useState('dark'); // Có thể thêm state để thay đổi theme nếu muốn

  console.log('ParentWithObjectMemoized rendered');

  // Sử dụng useMemo để ghi nhớ object config
  const config = useMemo(() => {
    console.log('Creating new config object...'); // Xem khi nào object thực sự được tạo lại
    return {
      theme: theme, // Sử dụng state theme
      fontSize: 16,
    };
  }, [theme]); // Dependencies array: useMemo chỉ chạy lại khi state 'theme' thay đổi

  return (
    <div>
      <h2>Parent With Object Memoized</h2>
      <p>Count: {count}</p>
      <p>Current Theme: {theme}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment Count (Does not affect config dependencies)
      </button>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme (Affects config dependencies)
      </button>

      {/* Truyền object config đã được useMemo ghi nhớ */}
      <ObjectPropChild config={config} />
    </div>
  );
};

export default ParentWithObjectMemoized;

Giải thích:

  • Chúng ta bọc việc tạo object config bên trong useMemo.
  • useMemo nhận một hàm (tạo object) và một mảng dependencies [theme].
  • Bây giờ, khi ParentWithObjectMemoized re-render (ví dụ do count thay đổi), useMemo sẽ kiểm tra dependencies (theme). Nếu theme không thay đổi, useMemo sẽ không chạy lại hàm bên trong, mà trả về tham chiếu của object config từ lần render trước.
  • React.memoObjectPropChild so sánh tham chiếu của object config. Vì tham chiếu không thay đổi, React.memo sẽ ngăn chặn re-render cho ObjectPropChild.
  • Nếu bạn click "Toggle Theme", state theme thay đổi. useMemo kiểm tra dependencies, thấy theme thay đổi, nên sẽ chạy lại hàm để tạo object config mới. Lúc này, tham chiếu của config thay đổi, React.memo thấy props khác nhau và sẽ re-render ObjectPropChild.

Tóm lại: useMemo giúp bạn đảm bảo rằng các object hoặc array props được truyền xuống component con được React.memo bọc sẽ có cùng tham chiếu qua các lần re-render của cha, miễn là các giá trị mà object/array đó phụ thuộc không thay đổi.

Giải Quyết Với useCallback (Cho Function)

Vấn đề tương tự xảy ra với function props. Khi bạn truyền một hàm được định nghĩa trực tiếp trong component cha, React sẽ tạo ra một phiên bản mới của hàm đó trên mỗi lần re-render.

```typescript jsx // components/FunctionPropChild.tsx import React from 'react';

interface FunctionChildProps { onClick: () => void; }

const FunctionPropChild: React.FC<FunctionChildProps> = ({ onClick }) => { console.log('FunctionPropChild rendered'); return ( <div style={{ border: '1px solid green', margin: '5px', padding: '5px' }}> <p>Click the button inside this child:</p> <button onClick={onClick}> Click Me </button> </div> ); };

export default React.memo(FunctionPropChild);


Và component cha:

```typescript jsx
// components/ParentWithFunction.tsx
import React, { useState } from 'react';
import FunctionPropChild from './FunctionPropChild';

const ParentWithFunction: React.FC = () => {
  const [count, setCount] = useState(0);

  console.log('ParentWithFunction rendered');

  // FUNCTION NÀY ĐƯỢC TẠO MỚI TRÊN MỖI LẦN RENDER!
  const handleClick = () => {
    console.log('Button inside child clicked!');
    // Có thể làm gì đó với state của cha ở đây
  };

  return (
    <div>
      <h2>Parent With Function</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment Count
      </button>
      {/* Truyền function handleClick */}
      <FunctionPropChild onClick={handleClick} />
    </div>
  );
};

export default ParentWithFunction;

Vấn đề: Giống như object/array, hàm handleClick được tạo mới mỗi khi ParentWithFunction re-render. React.memoFunctionPropChild so sánh tham chiếu của prop onClick, thấy nó khác, và vẫn re-render FunctionPropChild.

Để khắc phục, chúng ta sử dụng hook useCallback. useCallback cho phép bạn ghi nhớ tham chiếu của một hàm, và chỉ tạo lại hàm mới khi các dependencies của nó thay đổi.

```typescript jsx // components/ParentWithFunctionMemoized.tsx import React, { useState, useCallback } from 'react'; // Import useCallback import FunctionPropChild from './FunctionPropChild';

const ParentWithFunctionMemoized: React.FC = () => { const [count, setCount] = useState(0); // Giả sử hàm handleClick cần dùng state count const [message, setMessage] = useState('');

console.log('ParentWithFunctionMemoized rendered');

// Sử dụng useCallback để ghi nhớ hàm handleClick const handleClick = useCallback(() => { console.log(Button inside child clicked! Current count: ${count}); setMessage(Last clicked at count: ${count}); }, [count]); // Dependencies array: Hàm chỉ được tạo lại khi state 'count' thay đổi

return ( <div> <h2>Parent With Function Memoized</h2> <p>Parent Count: {count}</p> <p>Message: {message}</p> <button onClick={() => setCount(count + 1)}> Increment Count (Affects useCallback dependencies) </button>

  {/* Truyền function handleClick đã được useCallback ghi nhớ */}
  <FunctionPropChild onClick={handleClick} />
</div>

); };

export default ParentWithFunctionMemoized;


**Giải thích:**

*   Chúng ta bọc việc định nghĩa hàm `handleClick` bên trong `useCallback`.
*   `useCallback` nhận hàm và mảng dependencies `[count]`.
*   Khi `ParentWithFunctionMemoized` re-render, `useCallback` kiểm tra dependencies (`count`). Nếu `count` *không* thay đổi, `useCallback` sẽ *không* tạo lại hàm mới, mà trả về **tham chiếu của hàm `handleClick` từ lần render trước**.
*   `React.memo` ở `FunctionPropChild` so sánh tham chiếu của prop `onClick`. Vì tham chiếu không thay đổi, `React.memo` sẽ ngăn chặn re-render cho `FunctionPropChild`.
*   Nếu bạn click "Increment Count", state `count` thay đổi. `useCallback` kiểm tra dependencies, thấy `count` thay đổi, nên sẽ tạo lại hàm `handleClick` mới. Lúc này, tham chiếu của `handleClick` thay đổi, `React.memo` thấy props khác nhau và sẽ re-render `FunctionPropChild`.

**Tóm lại:** `useCallback` giúp bạn đảm bảo rằng các function props được truyền xuống component con được `React.memo` bọc sẽ có *cùng tham chiếu* qua các lần re-render của cha, miễn là các giá trị mà hàm đó phụ thuộc không thay đổi. `useCallback(fn, deps)` tương đương với `useMemo(() => fn, deps)`.

### Tùy Chỉnh Cơ Chế So Sánh Props (`React.memo` với Tham Số Thứ Hai)

Mặc định, `React.memo` sử dụng so sánh nông. Nhưng đôi khi, bạn cần một cơ chế so sánh phức tạp hơn. `React.memo` chấp nhận một tham số thứ hai là một hàm so sánh tùy chỉnh. Hàm này có signature là `(prevProps, nextProps) => boolean`.

**Lưu ý quan trọng:** Hàm so sánh này trả về `true` nếu props **giống nhau** (nghĩa là *không* cần re-render), và `false` nếu props **khác nhau** (nghĩa là *cần* re-render). Điều này *ngược lại* với phương thức `shouldComponentUpdate` trong class component.

Ví dụ, giả sử `ObjectPropChild` của chúng ta nhận một object `data` có nhiều trường, nhưng bạn chỉ muốn re-render nếu trường `value` trong object đó thay đổi.

```typescript jsx
// components/ComplexObjectPropChild.tsx
import React from 'react';

interface ComplexData {
  id: string;
  value: number;
  metadata: any; // Trường này có thể thay đổi nhưng ta không muốn re-render vì nó
}

interface ComplexObjectChildProps {
  data: ComplexData;
}

const ComplexObjectPropChild: React.FC<ComplexObjectChildProps> = ({ data }) => {
  console.log('ComplexObjectPropChild rendered');
  return (
    <div style={{ border: '1px solid purple', margin: '5px', padding: '5px' }}>
      <p>Data ID: {data.id}</p>
      <p>Data Value: {data.value}</p>
      {/* Hiển thị metadata chỉ để kiểm tra */}
      <p>Metadata: {JSON.stringify(data.metadata)}</p>
    </div>
  );
};

// Hàm so sánh tùy chỉnh: chỉ so sánh prop 'data.value'
const arePropsEqual = (prevProps: ComplexObjectChildProps, nextProps: ComplexObjectChildProps): boolean => {
  console.log('Comparing props...');
  // Trả về TRUE nếu KHÔNG cần re-render (props giống nhau theo logic tùy chỉnh)
  return prevProps.data.value === nextProps.data.value;
};

// Bọc bằng React.memo và truyền hàm so sánh tùy chỉnh
export default React.memo(ComplexObjectPropChild, arePropsEqual);

Component cha sử dụng nó:

```typescript jsx // components/ParentWithComplexObject.tsx import React, { useState, useMemo } from 'react'; // useMemo để memoize object nếu cần import ComplexObjectPropChild from './ComplexObjectPropChild';

const ParentWithComplexObject: React.FC = () => { const [count, setCount] = useState(0); const [metadataState, setMetadataState] = useState({ version: 0 }); // State chỉ ảnh hưởng metadata

console.log('ParentWithComplexObject rendered');

// Tạo object data. Sử dụng useMemo để giữ cùng tham chiếu nếu chỉ metadata thay đổi const data = useMemo(() => { console.log('Creating complex data object...'); return { id: 'item-abc', value: count * 10, // Value phụ thuộc vào count metadata: metadataState, // Metadata phụ thuộc vào metadataState }; }, [count, metadataState]); // Dependencies bao gồm cả count và metadataState

return ( <div> <h2>Parent With Complex Object</h2> <p>Parent Count: {count}</p> <p>Metadata Version: {metadataState.version}</p> <button onClick={() => setCount(count + 1)}> Increment Count (Affects data.value) </button> <button onClick={() => setMetadataState({ version: metadataState.version + 1 })}> Change Metadata (Does NOT affect data.value) </button>

  {/* Truyền object data */}
  <ComplexObjectPropChild data={data} />
</div>

); };

export default ParentWithComplexObject;


**Giải thích:**

*   `ComplexObjectPropChild` được bọc bằng `React.memo` và có hàm `arePropsEqual` tùy chỉnh. Hàm này chỉ so sánh `prevProps.data.value` và `nextProps.data.value`.
*   Trong `ParentWithComplexObject`, chúng ta dùng `useMemo` để tạo object `data`. Dependencies của `useMemo` là `[count, metadataState]` để object `data` chỉ được tạo mới khi *một trong hai* thay đổi.
*   Khi click "Increment Count", `count` thay đổi. `useMemo` tạo object `data` mới. `arePropsEqual` được gọi. Nó thấy `prevProps.data.value !== nextProps.data.value` (vì `value` phụ thuộc vào `count`), trả về `false`, nên `ComplexObjectPropChild` re-render.
*   Khi click "Change Metadata", `metadataState` thay đổi. `useMemo` tạo object `data` mới (vì `metadataState` là dependency). `arePropsEqual` được gọi. Nó thấy `prevProps.data.value === nextProps.data.value` (vì `count` không thay đổi), trả về `true`, nên `ComplexObjectPropChild` **không** re-render.

Ví dụ này cho thấy bạn có thể linh hoạt định nghĩa khi nào component con được `React.memo` bọc nên re-render, vượt qua giới hạn của so sánh nông mặc định. Tuy nhiên, hãy *cẩn thận* khi viết hàm so sánh tùy chỉnh, đảm bảo logic chính xác và hiệu quả.

### `React.memo` Với TypeScript: Cách Định Nghĩa Kiểu Dữ Liệu

Khi sử dụng `React.memo` với TypeScript, bạn có thể định nghĩa kiểu props theo cách thông thường. TypeScript thường có thể suy luận (infer) đúng kiểu dữ liệu. Tuy nhiên, để rõ ràng hơn, bạn có thể cung cấp kiểu props cho `React.memo`.

Cú pháp phổ biến nhất là định nghĩa interface props trước, sau đó định nghĩa component sử dụng `React.FC`, và cuối cùng bọc bằng `React.memo`.

```typescript jsx
import React from 'react';

// 1. Định nghĩa interface cho props
interface MyMemoizedComponentProps {
  id: number;
  name: string;
  data: { value: number }; // Ví dụ prop là object
  onClick: (id: number) => void; // Ví dụ prop là function
}

// 2. Định nghĩa component sử dụng React.FC<PropsInterface>
// TypeScript sẽ biết props là gì ở đây
const MyMemoizedComponent: React.FC<MyMemoizedComponentProps> = ({ id, name, data, onClick }) => {
  console.log(`MyMemoizedComponent ${id} rendered`);
  return (
    <div style={{ border: '1px solid orange', margin: '5px', padding: '5px' }}>
      <p>ID: {id}</p>
      <p>Name: {name}</p>
      <p>Data Value: {data.value}</p>
      <button onClick={() => onClick(id)}>Click Me</button>
    </div>
  );
};

// 3. Bọc bằng React.memo
// TypeScript thường suy luận đúng kiểu ở đây.
export default React.memo(MyMemoizedComponent);

// Tùy chọn: Bạn có thể cung cấp kiểu rõ ràng cho React.memo nếu muốn,
// nhưng thường không cần thiết khi dùng React.FC
// export default React.memo<MyMemoizedComponentProps>(MyMemoizedComponent);

// Nếu bạn không dùng React.FC, bạn cần định nghĩa kiểu tham số rõ ràng cho hàm component
// const MyMemoizedComponent = ({ id, name, data, onClick }: MyMemoizedComponentProps) => { ... }
// export default React.memo(MyMemoizedComponent);

Trong hầu hết các trường hợp, việc sử dụng React.FC<PropsInterface> khi định nghĩa component và sau đó bọc bằng React.memo là đủ để TypeScript hiểu và kiểm tra kiểu dữ liệu cho bạn một cách chính xác.

Khi sử dụng hàm so sánh tùy chỉnh, bạn cũng cần định nghĩa kiểu dữ liệu cho tham số của hàm đó:

```typescript jsx // components/CustomCompareMemoizedComponent.tsx import React from 'react';

interface ItemProps { item: { id: number; name: string; details: { price: number; quantity: number }; // Object details }; }

const CustomCompareMemoizedComponent: React.FC<ItemProps> = ({ item }) => { console.log(CustomCompareMemoizedComponent rendered for item ID: ${item.id}); return ( <div style={{ border: '1px solid teal', margin: '5px', padding: '5px' }}> <p>Item ID: {item.id}</p> <p>Item Name: {item.name}</p> <p>Price: {item.details.price}</p> <p>Quantity: {item.details.quantity}</p> </div> ); };

// Định nghĩa kiểu cho hàm so sánh tùy chỉnh const compareItemProps = (prev: ItemProps, next: ItemProps): boolean => { console.log(Comparing item ${prev.item.id} props...); // Ta chỉ muốn re-render nếu ID hoặc TÊN hoặc GIÁ thay đổi return prev.item.id === next.item.id && prev.item.name === next.item.name && prev.item.details.price === next.item.details.price; // Bỏ qua quantity };

// Bọc bằng React.memo với hàm so sánh tùy chỉnh export default React.memo(CustomCompareMemoizedComponent, compareItemProps); ```

TypeScript sẽ giúp bạn kiểm tra rằng bạn đang truy cập đúng các thuộc tính (id, name, details.price) trong hàm so sánh tùy chỉnh.

Khi Nào Không Nên Sử Dụng React.memo

React.memo không phải là viên đạn bạc và không nên được áp dụng cho mọi component. Memoization cũng có chi phí: React cần lưu trữ props lần trước và thực hiện quá trình so sánh. Đối với các component đơn giản, việc so sánh props có thể còn tốn kém hơn là để component re-render bình thường.

Bạn không nên sử dụng React.memo trong các trường hợp sau:

  1. Component rất đơn giản: Component chỉ hiển thị vài dòng text, không có logic phức tạp, và việc re-render rất nhanh. Chi phí so sánh của React.memo có thể lớn hơn chi phí re-render.
  2. Component thường xuyên thay đổi props: Nếu component của bạn nhận props là object, array hoặc function mà chúng luôn luôn thay đổi tham chiếu trên mỗi lần re-render của cha (và bạn không thể hoặc không muốn dùng useMemo/useCallback), React.memo sẽ luôn thấy props đã thay đổi và component sẽ luôn re-render. Việc sử dụng React.memo trong trường hợp này là vô ích và chỉ thêm chi phí so sánh không cần thiết.
  3. Bạn chưa gặp vấn đề hiệu suất: Đừng tối ưu hóa sớm. Chỉ sử dụng React.memo (và useMemo, useCallback) khi bạn đã profiling (phân tích hiệu suất) ứng dụng của mình bằng React Developer Tools và xác định được chính xác những component nào đang re-render không cần thiết và gây ra tắc nghẽn.
  4. Component nhận props là children hoặc render prop phức tạp: Nếu component nhận children là JSX hoặc một render prop tạo ra nội dung phức tạp, việc so sánh có thể không hiệu quả hoặc React.memo có thể không mang lại lợi ích rõ rệt.

Nguyên tắc chung: Sử dụng React.memo cho các component trung bình đến phức tạp, nằm sâu trong cây component, và bạn biết rằng props của chúng không thay đổi thường xuyên khi component cha re-render. Luôn kết hợp nó với useMemouseCallback khi truyền object, array hoặc function props để đảm bảo React.memo phát huy tác dụng.

Comments

There are no comments at the moment.