Bài viết được sự cho phép của tác giả Lưu Bình An
compound component
Khi gặp tình huống một component không thể đứng độc lập, mà nó buộc phải kết hợp với một component khác và cùng chia sẻ một bộ state và phương thức. Đó là lúc chúng ta cân nhắc cách viết compound component.
Một ví dụ rất dễ thấy của compound component là element <select />
và <option />
, <option/>
không thể đứng độc lập, nó luôn được đặt trong <select/>
có thể truy xuất và gọi các phương thức tương tự như <select/>
Tìm việc làm lập trình viên React
Tại sao lại sử dụng compound component?
Nếu bạn là người viết component, người khác sử dụng component này, các dev khác sẽ cảm ơn bạn rất nhiều. Bạn đóng gói mọi thứ logic vào bên trong component cha như vậy, người sau sẽ không cần bận tâm nữa.
// parent component
// xử lý event onChange, quản lý state selected value
<RadioImageForm>
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
</RadioImageForm>
Với child component của <RadioImageForm />
, để cho nó rõ ràng minh bạch là chúng ta sẽ sử dụng những giá trị cung cấp từ parent, chúng ta dùng kiểu viết <RadioImageForm.RadioInput />
export class RadioImageForm extends React.Component<Props, State> {
static RadioInput = ({
currentValue,
onChange,
label,
value,
name,
imgSrc,
key,
}: RadioInputProps): React.ReactElement => (
// ...
);
onChange = (): void => {
// ...
};
state = {
currentValue: '',
onChange: this.onChange,
defaultValue: this.props.defaultValue || '',
};
render(): React.ReactElement {
return (
<RadioImageFormWrapper>
<form>
{/* .... */}
</form>
</RadioImageFormWrapper>
)
}
}
Một nhu cầu rất phổ biến là người khác sẽ muốn control component con <RadioInput />
bằng việc truyền thêm prop
, nhưng thay vì truyền thằng vào component con, chúng ta hãy để họ truyền thông qua component cha <RadioImageForm/>
, vì một lý do nào đó chúng ta cần truy cập các prop này bên trong component cha thì sao? Chúng ta làm thêm một bước pass-through
các prop xuống cho component con với React.Children.map
hoặc React.cloneElement
render(): React.ReactElement {
const { currentValue, onChange, defaultValue } = this.state;
return (
<RadioImageFormWrapper>
<form>
{
React.Children.map(this.props.children,
(child: React.ReactElement) =>
React.cloneElement(child, {
currentValue,
onChange,
defaultValue,
}),
)
}
</form>
</RadioImageFormWrapper>
)
}
Quay lại với <RadioInput />
static RadioInput = ({
currentValue,
onChange,
label,
value,
name,
imgSrc,
key,
}: RadioInputProps) => (
<label className="radio-button-group" key={key}>
<input
type="radio"
name={name}
value={value}
aria-label={label}
onChange={onChange}
checked={currentValue === value}
aria-checked={currentValue === value}
/>
<img alt="" src={imgSrc} />
<div className="overlay">
{/* .... */}
</div>
</label>
);
Toàn bộ source code: https://codesandbox.io/s/compound-components-radio-image-form-k1h8x
Hạn chế
Với cách viết compound component này, chúng ta bị một hạn chế là bắt buộc phai viết component theo kiểu
<RadioImageForm>
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
</RadioImageForm>
Chúng ta không được phép chèn thêm một số thẻ <div />
ở giữa nếu có nhu cầu tùy biến giao diện chẳng hạn
<RadioImageForm>
<div>
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
</div>
</RadioImageForm>
Compound component Compound component với React Hook
Flexible compound component
Flexible compound component ra đời để giải quyết hạn chế của compound component, chúng ta sẽ sử dụng React Context API.
Chúng ta sẽ tạo ra một context
mà ở đó cả component con và cha điều có thể truy xuất được, đúng như mục đích ra đời của Context API
const RadioImageFormContext = React.createContext({
currentValue: '',
defaultValue: undefined,
onChange: () => { },
});
RadioImageFormContext.displayName = 'RadioImageForm';
Chúng ta sẽ refactor lại <RadioImageForm/>
, bỏ đi đoạn React.Children.map
, thay bằng <Provider />
render(): React.ReactElement {
const { children } = this.props;
return (
<RadioImageFormWrapper>
<RadioImageFormContext.Provider value={this.state}>
{children}
</RadioImageFormContext.Provider>
</RadioImageFormWrapper>
);
}
Sử dụng Provider có một lưu ý sống còn là đừng bao giờ truyền
value={{ some bla bla}}
, như vậy nó sẽ khác nhau trên tất cả những lần render, hãy nhớ truyền một thứ gì đó cache được và chỉ bị thay đổi khi cần thiết như this.state
Trong component con <RadioInput />
chúng ta có thể truy xuất tất cả dữ liệu nội bộ thông qua consumer, bởi vì <RadioInput />
đang nằm trong <RadioImageForm />
luôn theo cách viết của chúng ta, nên có thể khai báo một static property Consumer
bên trong RadioImageForm
export class RadioImageForm extends React.Component<Props, State> {
static Consumer = RadioImageFormContext.Consumer;
//...
Source code ví dụ Source code Flexible compound component bằng functional component
Provider Pattern
Provider pattern là kỹ thuật kết hợp giữa React Context API và render props pattern, vẫn là để giải quyết câu chuyện chia sẻ state giữa các component trong cây.
Nếu bạn có thắc mắc, ủa vậy sao không dùng Redux, Mobx, Recoil, React Sweet State, Rematch, Unstated,… cho khỏe người ơi? Thì câu trả lời của mình là, ừ các bạn nên xài những thư viện quản lý state như vậy cho khỏe người, khỏe cho cả người maintain code bạn. Còn đây là cách làm nếu bạn muốn tham khảo, nếu không dùng gì hết, tôi dư giả thời gian để code từ đầu thì bạn có thể go-ahead với cách này
// src/components/DogDataProvider.tsx
interface State {
data: IDog;
status: Status;
error: Error;
}
const initState: State = { status: Status.loading, data: null, error: null };
const DogDataProviderContext = React.createContext(undefined);
DogDataProviderContext.displayName = 'DogDataProvider';
const DogDataProvider: React.FC = ({ children }): React.ReactElement => {
const [state, setState] = React.useState<State>(initState);
React.useEffect(() => {
setState(initState);
(async (): Promise<void> => {
try {
// MOCK API CALL
const asyncMockApiFn = async (): Promise<IDog> =>
await new Promise(resolve => setTimeout(() => resolve(DATA), 1000));
const data = await asyncMockApiFn();
setState({
data,
status: Status.loaded,
error: null
});
} catch (error) {
setState({
error,
status: Status.error,
data: null
});
}
})();
}, []);
return (
<DogDataProviderContext.Provider value={state}>
{children}
</DogDataProviderContext.Provider>
);
};
// src/components/DogDataProvider.tsx
export function useDogProviderState() {
const context = React.useContext(DogDataProviderContext);
if (context === undefined) {
throw new Error('useDogProviderState phải được sử dụng bên trong DogDataProvider.');
}
return context;
}
// src/index.tsx
function App() {
return (
<Router>
<div className="App">
{/* DataProvider phải nằm trên cùng của cây.*/}
<DogDataProvider>
<Nav />
<main className="py-5 md:py-20 max-w-screen-xl mx-auto text-center text-white w-full">
<Banner
title={'React Component Patterns:'}
subtitle={'Provider Pattern'}
/>
<Switch>
<Route exact path="/">
{/* Component con sử dụng dữ liệu qua Consumer */}
<Profile />
</Route>
<Route path="/friends">
{/* Component con sử dụng dữ liệu qua Consumer */}
<DogFriends />
</Route>
</Switch>
</main>
</DogDataProvider>
</div>
</Router>
);
}
const Profile = () => {
// custom hook nhận "subscribes" khi có state thay đổi
const { data, status, error } = useDogProviderState();
return (
<div>
<h1 className="//...">Profile</h1>
<div className="mt-10">
{error ? (
<Error errorMessage={error.message} />
) : status === Status.loading ? (
<Loader isInherit={true} />
) : (
<ProfileCard data={data} />
)}
</div>
</div>
);
};
Bài viết gốc được đăng tải tại vuilaptrinh.com
Có thể bạn quan tâm:
- Lựa chọn framework frontend nào trong thời điểm hiện tại
- Tiki đã dùng React Native như thế nào?
- Bí kíp học Front-end của Grab (Phần 2)
Xem thêm các việc làm it hấp dẫn trên TopDev