- vừa được xem lúc

React component code smells

0 0 14

Người đăng: kentrung

Theo Viblo Asia

Bài viết gốc: https://antongunnarsson.com/react-component-code-smells/

Code Smell là gì? code smell là thứ có thể chỉ ra vấn đề sâu hơn bên trong code, nhưng không nhất thiết là lỗi. Đọc thêm trên Wikipedia

1. Too many props

Một component có quá nhiều props là dấu hiệu nên chia nhỏ component đó ra.

Vậy bao nhiêu thì coi là nhiều? Cái đấy còn tùy.

Bạn có thể thấy một component có 20 props và hài lòng nó vẫn đang làm việc ngon, giờ bạn muốn thêm một cái nữa vào danh sách props vốn đã dài, có một số điều cần xem xét:

Component này có làm nhiều thứ không?

Giống như functions, component chỉ nên làm tốt một việc, vì vậy luôn kiểm tra xem có thể chia component đó thành nhiều component con hay không?

Tôi có nên dùng composition?

Một mô hình tốt nhưng thường bị bỏ qua là tạo các compose components thay vì xử lý tất cả logic bên trong nó. Giả sử chúng ta có một component như sau:

<ApplicationForm user={userData} organization={organizationData} categories={categoriesData} locations={locationsData} onSubmit={handleSubmit} onCancel={handleCancel} ...
/>

Nhìn vào các props của component này, chúng ta có thể thấy rằng tất cả chúng đều liên quan đến chức năng của component, nhưng vẫn còn chỗ để cải thiện điều này bằng cách chuyển thành một số component con thay thế:

<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}> <UserField user={userData} /> <OrganizationField organization={organizationData} /> <CategoryField categories={categoriesData} /> <LocationsField locations={locationsData} />
</ApplicationForm>

Giờ đây, chúng ta đã đảm bảo rằng ApplicationForm chỉ xử lý việc onSubmitonCancel. Các component con có thể xử lý mọi thứ liên quan đến phần của chúng trong bức tranh lớn hơn. Đây cũng là một cơ hội tuyệt vời để sử dụng React Context cho việc giao tiếp giữa component cha và con.

Đọc thêm về compound components in React.


Tôi có đang gửi quá nhiều props config không?

Trong một số trường hợp, bạn nên nhóm các props lại với nhau thành một object, chẳng hạn như để hoán đổi cấu hình này dễ dàng hơn.

<Grid data={gridData} pagination={false} autoSize={true} enableSort={true} sortOrder="desc" disableSelection={true} infiniteScroll={true} ...
/>

Tất cả các props ngoại trừ data có thể được coi là config. Trong những trường hợp này, bạn nên thay đổi component Grid để nó nhận một options gom các config ở trên.

const options = { pagination: false, autoSize: true, enableSort: true, sortOrder: 'desc', disableSelection: true, infiniteScroll: true, ...
} <Grid data={gridData} options={options}
/>

2. Incompatible props

Tránh gửi các props không tương thích với nhau.

Ví dụ chúng ta tạo một component <Input /> chung chung nhằm mục đích xử lý type=text, nhưng sau một thời gian chúng ta lại thêm chức năng cho nó với type=tel. Việc triển khai có thể như sau:

function Input({ value, isPhoneNumberInput, autoCapitalize }) { if (autoCapitalize) capitalize(value) return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}

Vấn đề xảy ra là isPhoneNumberInputautoCapitalize không hợp cạ với nhau. Chúng ta đâu thể viết hoa số điện thoại.

Trong trường hợp này, giải pháp có lẽ là chia thành nhiều component nhỏ hơn. Nếu chúng ta có một số logic muốn chia sẻ, hãy chuyển nó sang custom hook:

function TextInput({ value, autoCapitalize }) { if (autoCapitalize) capitalize(value) useSharedInputLogic() return <input value={value} type='text' />
} function PhoneNumberInput({ value }) { useSharedInputLogic() return <input value={value} type='tel' />
}

Mặc dù ví dụ này hơi phức tạp, nhưng việc tìm kiếm các props không tương thích với nhau thường là một dấu hiệu tốt cho thấy bạn nên kiểm tra xem component có cần được chia nhỏ hay không.

3. Copying props into state

Đừng chặn dòng chảy của data bằng cách copy props vào state.

function Button({ text }) { const [buttonText] = useState(text) return <button>{buttonText}</button>
}

Bằng việc đặt props text vào initialValue của useState khiến cho component này bỏ qua các update của props text. Nếu props thay đổi thì component vẫn render với giá trị đầu tiên, đối với hầu hết các props, đây là hành vi không mong muốn, có thể làm cho component dễ bị lỗi hơn.

Một ví dụ thực tế hơn về điều này là khi chúng ta muốn lấy một giá trị mới nào đó từ một giá trị và đặc biệt nếu điều này đòi hỏi một số tính toán chậm. Ở ví dụ dưới đây, chúng ta chạy hàm slowFormatText để định dạng props text, điều này mất rất nhiều thời gian để thực thi.

function Button({ text }) { const [formattedText] = useState(() => slowlyFormatText(text)) return <button>{formattedText}</button>
}

Một cách tốt hơn để giải quyết vấn đề này là sử dụng hook useMemo để ghi nhớ kết quả:

function Button({ text }) { const formattedText = useMemo(() => slowlyFormatText(text), [text]) return <button>{formattedText}</button>
}

Bây giờ thì slowlyFormatText chỉ chạy khi text thay đổi và chúng ta không chặn update component.

Đôi khi chúng ta vẫn cần prop trong đó các thay đổi bị bỏ qua, ví dụ bộ chọn màu có các option cho sẵn, nhưng khi người dùng thay đổi thì chúng ta chưa muốn thay đổi ngay chỉ lưu tạm. Trong trường hợp này, việc sao chép prop vào state là hoàn toàn ổn, nhưng để biểu thị hành vi này của người dùng, chúng ta có thể phân tách ra thành initialColor hoặc defaultColor

Further reading: Writing resilient components by Dan Abramov.

4. Returning JSX from functions

Đừng trả về JSX từ các functions bên trong component.

Đây là một pattern phần lớn đã biến mất khi các function components trở nên phổ biến hơn, nhưng tôi vẫn thỉnh thoảng bắt gặp nó.

function Component() { const topSection = () => { return ( <header> <h1>Component header</h1> </header> ) } const middleSection = () => { return ( <main> <p>Some text</p> </main> ) } const bottomSection = () => { return ( <footer> <p>Some footer text</p> </footer> ) } return ( <div> {topSection()} {middleSection()} {bottomSection()} </div> )
}

Mặc dù điều này ban đầu có thể ổn, nhưng nó khiến bạn khó suy luận về code, không khuyến khích và nên tránh. Để giải quyết vấn đề này bạn nên chia các phần này thành các component con thay thế.

Hãy nhớ rằng khi bạn tạo một component mới, bạn không cần phải tách ra file mới. Đôi khi bạn nên giữ nhiều components trong cùng một file nếu chúng được kết hợp chặt chẽ với nhau.

5. Multiple booleans for state

Tránh sử dụng nhiều state boolean để thể hiện trạng thái của component

Khi viết một component và mở rộng chức năng cho nó, bạn có nhiều state để cho biết component đang ở trạng thái nào. Ví dụ:

function Component() { const [isLoading, setIsLoading] = useState(false) const [isFinished, setIsFinished] = useState(false) const [hasError, setHasError] = useState(false) const fetchSomething = () => { setIsLoading(true) fetch(url) .then(() => { setIsLoading(false) setIsFinished(true) }) .catch(() => { setHasError(true) }) } if (isLoading) return <Loader /> if (hasError) return <Error /> if (isFinished) return <Success /> return <button onClick={fetchSomething} />
}

Mặc dù về mặt kỹ thuật thì nó hoạt động tốt nhưng thật khó để lý giải về trạng thái của component, nó có thể xảy ra lỗi. Chúng ta có thể rơi vào trạng thái impossible state nếu chúng ta vô tình đặt cả isLoadingisFinished thành true cùng một lúc.

Cách tốt hơn để quản lý trạng thái đó là dùng enum. Ở các ngôn ngữ khác thì enum là cách để khai báo một tập hợp các giá trị không đổi, vì enum không tồn tại trong javascript nên chúng ta có thế sử dụng string thay thế.

function Component() { const [state, setState] = useState('idle') const fetchSomething = () => { setState('loading') fetch(url) .then(() => { setState('finished') }) .catch(() => { setState('error') }) } if (state === 'loading') return <Loader /> if (state === 'error') return <Error /> if (state === 'finished') return <Success /> return <button onClick={fetchSomething} />
}

Làm theo cách này chúng ta đã gỡ bỏ trạng thái impossible state và khiến cho component trở nên dễ hiểu hơn. Nếu bạn sử dụng TypeScript thì có thể khai báo như sau:

const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')

6. Too many useState

Tránh sử dụng quá nhiều useState trong component.

Một component có quá nhiều useState giống như làm quá nhiều thứ trong đó, tốt hơn là tách ra làm nhiều component, tuy nhiên sẽ có những component phức tạp cần nhiều state.

function AutocompleteInput() { const [isOpen, setIsOpen] = useState(false) const [inputValue, setInputValue] = useState('') const [items, setItems] = useState([]) const [selectedItem, setSelectedItem] = useState(null) const [activeIndex, setActiveIndex] = useState(-1) const reset = () => { setIsOpen(false) setInputValue('') setItems([]) setSelectedItem(null) setActiveIndex(-1) } const selectItem = (item) => { setIsOpen(false) setInputValue(item.name) setSelectedItem(item) } ...
}

Chúng ta có function reset để reset các state, function selectItem để cập nhật một số state. Cả hai hàm này đều sử dụng khá nhiều setState để thực hiện nhiệm vụ. Bây giờ, nếu chúng ta có thêm nhiều hành động khác phải cập nhật state, điều này trở nên khó để giữ cho không có lỗi trong thời gian dài. Trong những trường hợp này, sẽ có lợi khi quản lý state bằng một useReducer

const initialState = { isOpen: false, inputValue: "", items: [], selectedItem: null, activeIndex: -1
}
function reducer(state, action) { switch (action.type) { case "reset": return { ...initialState } case "selectItem": return { ...state, isOpen: false, inputValue: action.payload.name, selectedItem: action.payload } default: throw Error() }
} function AutocompleteInput() { const [state, dispatch] = useReducer(reducer, initialState) const reset = () => { dispatch({ type: 'reset' }) } const selectItem = (item) => { dispatch({ type: 'selectItem', payload: item }) } ...
}

Với việc sử dụng useReducer chúng ta đã đóng gói logic quản lý state và chuyển sự phức tạp ra khỏi component. Điều này làm cho việc hiểu những gì đang diễn ra dễ dàng hơn, chúng ta đã tách UI và logic riêng biệt.

Cả useState và useReducer đều có ưu nhược điểm với các use case khác nhau, một trong các mục yêu thích của tôi với reducers đó là state reducer pattern của Kent C. Dodds.

7. Large useEffect

Tránh sử dụng các useEffect lớn và làm nhiều việc. Nó khiến code khó tìm lỗi, cứ lòng vòng update.

Một sai lầm mà tôi đã mắc phải là đặt quá nhiều thứ vào một useEffect.

function Post({ id, unlisted }) { ... useEffect(() => { fetch(`/posts/${id}`) .then(/* do something */) setVisibility(unlisted) }, [id, unlisted]) ...
}

useEffect này không lớn lắm nhưng lại làm nhiều việc. Nếu id thay đổi thì fetch bài viết, nếu unlisted thay đổi thì setVisibility, tuy nhiên chỉ cần một trong hai dependencies thay đổi thì cả hai việc đều thực hiện.

Để dễ theo dõi chúng ta nên tách ra làm hai useEffect với từng dependencies riêng biệt

function Post({ id, unlisted }) { ... useEffect(() => { // when id changes fetch the post fetch(`/posts/${id}`) .then(/* do something */) }, [id]) useEffect(() => { // when unlisted changes update visibility setVisibility(unlisted) }, [unlisted]) ...
}

Làm cách này chúng ta đã giảm độ phức tạp của component xuống, dễ dàng suy đoán logic, giảm nguy cơ lỗi.

8. Wrapping up

Được rồi, tạm thế nhé! Hãy nhớ rằng những điều nêu trên không phải là quy tắc mà là dấu hiệu cho thấy điều gì đó có thể "sai". Bạn chắc chắn sẽ gặp phải những tình huống trên và muốn thực hiện chỉnh sửa lại.

Bạn muốn feedback hay suggest về code smells khác? Tìm tôi trên Twitter!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Cùng tìm hiểu về các hook trong React hooks

Đối với ai đã từng làm việc với React thì chắc hẳn đã có những lúc cảm thấy bối rối không biết nên dùng stateless (functional) component hay là stateful component. Nếu có dùng stateful component thì cũng sẽ phải loay hoay với đống LifeCycle 1 cách khổ sở Rất may là những nhà phát triển React đã kịp

0 0 100

- vừa được xem lúc

Khi nào nên (và không nên) sử dụng Redux

. Công việc quản lý state với những hệ thống lớn và phức tạp là một điều khá khó khăn cho đến khi Redux xuất hiện. Lấy cảm hứng từ design pattern Flux, Redux được thiết kế để quản lý state trong các project JavaScript.

0 0 127

- vừa được xem lúc

ReactJS: Props và State

Nếu bạn đã học ReactJS hay React Native, bạn sẽ thấy các Props và State được sử dụng rất nhiều. Vậy chính xác chúng là gì? Làm thế nào để chúng ta sử dụng chúng đúng mục đích đây.

0 0 59

- vừa được xem lúc

State và Props trong Reactjs

Hello các bạn, tiếp tục seri tìm hiểu về ReactJs hôm nay mình xin giới thiệu đến các bạn hai thứ mình cho là thú vị nhất của ReactJs là State và Props. State bạn có thể hiểu đơn giản là một nơi mà bạn lưu trữ dữ liệu của Component, từ đó bạn có thể luân chuyển dữ liệu đến các thành phần trong Compon

0 0 54

- vừa được xem lúc

Memoization trong React

. 1.Introduction. Memoization có liên quan mật thiết đến bộ nhớ đệm, và dưới đây là một ví dụ đơn giản:. const cache = {}.

0 0 51

- vừa được xem lúc

Nâng cao hiệu suất React Hooks với React.memo, Memoization và Callback Functions

1.Ngăn Re-render và React.memo. React.

0 0 80