Từ React 16.8.0, chúng ta có React Hook, nó giải quyết toàn bộ những trường hợp chúng ta phải sử dụng higher-order component và giảm đáng kể độ phức tạp của việc set type so với HOC. Bạn sử dụng hook trong mọi tình huống có thể. Nếu gơi cảnh ngặt nghèo, anh lead của bạn ko rõ lý do gì bắt xài HOC với TypeScript. Thì bài viết này để giúp biết biết cách set type cho HOC.
HOC trong React là công cụ để chúng ta sử dụng nhiều đoạn code giống nhau trên các component khác nhau. Tuy nhiên khi dùng chung với TypeScript thì triệu triệu developer gặp không ít khó khăn khi set type cho nó. Bao gồm luôn mình trong đó.
Trong phạm vi bài viết này, chúng ta sẽ chi ra 2 loại HOC, 2 cách làm HOC phổ biến hiện nay, tạm gọi là enhancer và injector
- Enhancer: bọc một component, bổ sung thêm các hàm hoặc prop
- Injector: bơm/chích thêm prop vào một component
Để phân biệt rõ hơn, bạn xem tiếp ví dụ bên dưới.
Enhancer
Chúng ta bắt đầu với Enhancer vì nó dễ viết type
nhất. Ví dụ cơ bản nhất, bổ sung thêm prop loading
vào component.
Không bao gồm type
const withLoading = Component =>
class WithLoading extends React.Component {
render() {
const { loading, ...props } = this.props;
return loading ? <LoadingSpinner /> : <Component {...props} />;
}
};
... và với type
interface WithLoadingProps {
loading: boolean;
}
const withLoading = <P extends object>(Component: React.ComponentType<P>) => {
class WithLoading extends React.Component<P & WithLoadingProps> {
render() {
const { loading, ...props } = this.props;
return loading ? <LoadingSpinner /> : <Component {...(props as P)} />;
}
}
};
Có vài thứ cần giải thích ở đoạn trên, từng bước một nhé
interface WithLoadingProps {
loading: boolean;
}
Đây là interface
khai báo các prop và type
sẽ được thêm vào (enhance)
<P extends object>(Component: React.ComponentType<P>)
Chúng ta đang sử dụng một generic
, P
là ký tự dùng để đại diện cho toàn bộ prop của component khi truyền cho HOC. React.ComponentType<P>
là một type viết tắt cho cả hai React.FC<P>
và React.ClassComponent<P>
, nghĩa là một component truyền vào cho HOC này có thể là function cũng được, class component cũng được.
class WithLoading extends React.Component<P & WithLoadingProps>
Đây là đoạn chúng ta component sẽ return
từ HOC, nó chỉ định là component này sẽ bao gồm toàn bộ prop từ component (P
) và prop của chính thằng HOC (WithLoadingProps
), nó được cộng dồn bằng toán tử &
const { loading, ...props } = this.props;
Với phiên bản cũ của TypeScript, có thể chúng ta phải ép kiểu
this.props
như thế nàythis.props as WithLoadingProps
Cuối cùng chúng ta sử dụng prop loading
để đặt điều kiện hiển thị cái Spinner
return loading ? <LoadingSpinner /> : <Component {...props as P} />;
ép kiểu
props as P
là bắt buộc từ TypeScript 3.2, đây là bug của TypeScript
Với HOC withLoading
cũng có thể được viết để return một function component thay vì class
const withLoading = <P extends object>(
Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> => ({
loading,
...props
}: WithLoadingProps) =>
loading ? <LoadingSpinner /> : <Component {...(props as P)} />;
Chúng ta gặp vấn đề tương tự khi sử dụng rest/spread object, chúng ta chỉ định kiểu return là React.FC<P & WithLoadingProps>
, nhưng chỉ sử dụng WithLoadingProps
bên trong function component
Injector
Kiểu injector HOC sẽ hay gặp hơn, nhưng cũng khó set type hơn, bên cạnh việc chích thêm một số prop vào cho component, trong đa số các trường hợp nó còn xóa những prop đã chích vào khi nó bọc lại, như vậy những thằng từ bên ngoài không thể ghi đè lên. connect
của react-redux là một ví dụ cho injector HOC. Chúng ta không sử dụng nó, vì quá phức tạp, dùng một ví dụ đơn giản hơn, HOC chích thêm giá trị counter
và callback
để tăng giảm giá trị.
import { Subtract } from "utility-types";
export interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}
interface MakeCounterState {
value: number;
}
const makeCounter = <P extends InjectedCounterProps>(
Component: React.ComponentType<P>
) =>
class MakeCounter extends React.Component<
Subtract<P, InjectedCounterProps>,
MakeCounterState
> {
state: MakeCounterState = {
value: 0
};
increment = () => {
this.setState(prevState => ({
value: prevState.value + 1
}));
};
decrement = () => {
this.setState(prevState => ({
value: prevState.value - 1
}));
};
render() {
return (
<Component
{...(this.props as P)}
value={this.state.value}
onIncrement={this.increment}
onDecrement={this.decrement}
/>
);
}
};
Một vài điểm khác nhau
export interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}
Khai báo một interface để chỉ định những prop nào sẽ được chích, đồng thời export luôn để component nào dùng HOC có thể lấy xài.
import makeCounter, { InjectedCounterProps } from "./makeCounter";
interface CounterProps extends InjectedCounterProps {
style?: React.CSSProperties;
}
const Counter = (props: CounterProps) => (
<div style={props.style}>
<button onClick={props.onDecrement}> - </button>
{props.value}
<button onClick={props.onIncrement}> + </button>
</div>
);
export default makeCounter(Counter);
<P extends InjectedCounterProps>(Component: React.ComponentType<P>)
Một lần nữa chúng ta dùng một generic
, nhưng lần này để đảm bảo component sử dụng HOC có bao gồm các prop đã được chích, nếu không thì báo lỗi.
class MakeCounter extends React.Component<
Subtract<P, InjectedCounterProps>,
MakeCounterState
>
Component được trả về từ HOC sẽ sử dụng Subtract
, nó sẽ tách hết những prop đã chích thêm, nghĩa là nếu ai đó set lại từ kết quả trả về từ HOC, nó sẽ lỗi
Enhance + Inject
Kết hợp cả 2 cách làm này lại, chúng ta sẽ có một component counter cho phép đưa giá trị minimum và maximum
export interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}
interface MakeCounterProps {
minValue?: number;
maxValue?: number;
}
interface MakeCounterState {
value: number;
}
const makeCounter = <P extends InjectedCounterProps>(
Component: React.ComponentType<P>
) =>
class MakeCounter extends React.Component<
Subtract<P, InjectedCounterProps> & MakeCounterProps,
MakeCounterState
> {
state: MakeCounterState = {
value: 0
};
increment = () => {
this.setState(prevState => ({
value:
prevState.value === this.props.maxValue
? prevState.value
: prevState.value + 1
}));
};
decrement = () => {
this.setState(prevState => ({
value:
prevState.value === this.props.minValue
? prevState.value
: prevState.value - 1
}));
};
render() {
const { minValue, maxValue, ...props } = this.props;
return (
<Component
{...(props as P)}
value={this.state.value}
onIncrement={this.increment}
onDecrement={this.decrement}
/>
);
}
};
Subtract
được sử dụng để kết hợp cả những prop của chính component và prop của HOC
Subtract<P, InjectedCounterProps> & MakeCounterProps
Ngoài ra không còn gì thật sự khác nhau giữa 2 cách làm này cần phải nói thêm.
https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb
Initializing...