Những ngày đầu đông gõ cửa, tiết trời Hà Nội sao mà đỏng đảnh đến thế. Sáng ra vẫn còn vấn vương những tia nắng ấm, làn gió mơn man mà chiều về đã mang theo cơn mưa nhẹ lất phất. Ấy thế mà Hà Nội vẫn có cho riêng mình sức hút lạ kì. Còn gì tuyệt vời hơn khi cùng homies ghé qua những con phố, nghe hương hoa sữa thoang thoảng, hay "cố tình" đi chậm lại chỉ để ngắm nhìn những gánh cúc họa mi một chút
Chill như vậy, âu cũng là cái cớ để chúng ta cùng ngồi lại trao đổi về một chủ đề nữa trong ReactJS
đón chào chiếc giao mùa nhỉ =))
Đặt vấn đề
Vẫn là anh bạn mình nhắc tới trong bài viết Yêu React chẳng cần cớ, cần hiểu rõ setState() cơ !, sau hơn năm chinh chiến dự án thì React
chẳng còn là cái gì đó "xoắn quẩy" với người anh nữa. Ngay ngày hôm qua, còn phím đố mình:
- Tại sao một vài trường hợp hàm trong useEffect() vẫn chạy lại trong khi dependency không hề thay đổi?
Hmm... Không biết có bạn nào gặp trường hợp này chưa?
Trong bài viết này, chúng ta cùng tìm hiểu về useEffect()
để giúp mình tìm ra câu trả lời thỏa đáng để "đá bóng"
lại ông anh nhé !!!
■ Đối tượng
Bài viết chủ yếu hướng tới các bạn đã nắm được các concepts
cơ bản của ReactJS
nhưng trong quá trình tìm hiểu còn băn khoăn về useEffect()
cũng như muốn có cái nhìn rõ nét hơn về React API
này ^^
Nhấp ngụm mật ong chanh cho ấm bụng rồi cùng bắt đầu thôiii!
■ useEffect() API
■ Tổng quan
Đó giờ ReactJS
đã hỗ trợ chúng ta khai báo một component
theo 2 hướng cú pháp:
const FunctionComponent = () => <p>Function Component</p>
class ClassComponent extends React.Component { render() { return <p>Class Component</p> } }
Ở thời điểm trước đó, Function Component
chỉ được xem như một kiểu Presentation component/Stateless Component
do chưa được React
hỗ trợ về việc quản lý state
, track life cycles
so với Class Component
.
Cho tới 2019
, Function component
mới thật sự bùng nổ và bắt đầu được sử dụng rộng rãi hơn khi có thêm sức mạnh của React Hooks
- một nhóm các APIs
được ReactJS
trình làng trong version 16.8
.
useEffect()
là một trong số đó. API này
hỗ trợ chúng ta giải quyết một số thách thức khi làm việc với life cycle
trong component
.
Giờ thì hãy đi vào cú pháp của useEffect()
))
■ Cú pháp
useEffect(callback, dependency);
useEffect()
là một hàm nhận vào 2 tham số
:
-
Callback
:
- Là một hàm:
- Không nhận tham số.
- Bên trong chứa các
effects
cần được xử lý; Chúng sẽ được thực thi khi component
được mount
ở lần render-đầu-tiên
và được-gọi-lại
nếu một-trong-các-giá-trị
trong mảng dependency
thay đổi. - Luôn trả về hoặc một hàm khác (gọi là
cleanup
) hoặc undefined
.
- Bắt buộc truyền vào
-
Dependency
:
-
Là một mảng gồm các dependencies
, quyết định việc có gọi lại các effects
trong callback
hay không:
- Nếu không truyền gì thì mặc định, các
effects
sẽ được gọi lại sau mỗi lần render
. - Nếu truyền vào một mảng rỗng
[]
thì effects
sẽ CHỈ chạy trong lần render
đầu tiên.
-
Tùy chọn truyền vào hoặc không.
Đâu đó chúng ta đã bắt gặp:
useEffect(() => { getPostDetail(id); // <-- effect return () => { cleanPostDetail(); // <-- cleanup } }, [id]);
Theo Trang chủ ReactJS:
The Effect Hook
lets you perform side effects
in function components
.
Này là một số use cases
về các side effects
thực tế chúng ta thường gặp:
- Thêm
subscriber
, event listener
cho element
. - Gọi dữ liệu từ API sau khi
component
được render
trong giai đoạn mounting
. - Thực thi một
business logic
/DOM update
nào đó khi state
hoặc props
trong component
thay đổi. - Tiến hành dọn dẹp,
unsubscribe
các event listeners
trước đó đã sử dụng trước khi component
unmount
. - ...
Nói như vậy, liệu rằng life cycle methods
trong Class component(CC)
làm được gì thì useEffect()
trong Function component(FC)
cũng "cân" được hết hay sao nhỉ!?!
Cùng chuyển sang phần tiếp theo để đi tìm câu trả lời!
■ useEffect() vs. Lifecycle methods
Nếu như bạn đã quen với các life cycle methods
trong Class component
, có thể hiểu rằng useEffect()
là sự kết hợp giữa 3 methods
là componentDidMount()
, componentDidUpdate()
và componentWillUnmount()
))
Chi tiết được mô tả trong bảng sau:
Class Component | Function Component |
---|
componentDidMount(){ effects() } | useEffect(() => { effects() }, []) |
componentDidUpdate(){ effects() } | useEffect(() => { effects() }, dependencies) |
componentWillUnmount(){ cleanup() } | useEffect(() => () => { cleanup() }, []) |
Open in CodeSandbox ▷▷▷
Nếu muốn tìm hiểu thêm về phần này, bạn có thể tham khảo trên Trang chủ React hoặc nghía qua chiếc slide trong một seminar giới thiệu chung về hooks mình làm cho team
gần đây nhé ^^
Giờ thì điểm qua một số vấn đề thường gặp khi làm việc với useEffect()
!
■ Các vấn đề thường gặp
Như đã đề cập ở trên, thay vì:
thì với cú pháp
useEffect(callback, dependency);
chúng ta có thể kiểm soát những lần gọi lại callback
không cần thiết của component
.
Các giá trị được sử dụng bên trong useEffect()
và nằm trong component
nên được truyền vào dependency
. Nếu chưa quen, thời gian đầu chúng ta có thể nhờ tới sự hỗ trợ của Lint Tools
:
We provide the exhaustive-deps ESLint rule
as a part of the eslint-plugin-react-hooks
package to find components that don't handle updates consistently.
Giờ thì xem qua một ví dụ:
useEffect(() => { getPostDetail(id); }, [id]);
Này thì dễ rồi nhỉ getPostDetail()
sẽ chạy trong lần render
đầu tiên và được gọi lại nếu như giá trị id
thay đổi.
Hmm...
Dựa vào đâu để React
phát hiện được "sự thay đổi" (change detection
)?
React
sẽ so sánh id
hiện tại và id
ở lần render
ngay trước đó.
Vậy nếu nó là một Object
hay Function
thì sao? ))
Trong JavaScript
, chúng ta biết rằng khi so sánh:
// Primitive values const prevURL = 'https://haodev.wordpress.com'; const currURL = 'https://haodev.wordpress.com'; prevURL === currURL // TRUE // Reference values const prevBlog = { name: 'Make It Awesome' }; const currBlog = { name: 'Make It Awesome' }; prevBlog === currBlog // FALSE
Ở một số trường hợp thực tế, Dependency
có thể là 1 mảng các props
, state
, thậm chí là một function
, như vậy thì sẽ không thể tránh khỏi việc trigger
effects
thừa thãi.
Cùng đi vào chi tiết nhé!
■ Dependency chứa object
const ObjDependency = () => { const [vote, setVote] = useState({ value: 0, }); useEffect(() => { console.log("Component is invoked when vote changes"); }, [vote]); return ( <> <p>Vote value: {vote.value}.</p> <button onClick={() => setVote({ value: 0 })}>Set vote = 0</button> </> ); };
Lúc này, dù vote.value
vẫn bằng 0
nhưng chuỗi Component is invoked when vote changes
vẫn sẽ được log
ra khi ta click
vào button
Để xem nào, chúng ta sẽ có một vài hướng tiếp cận để xử lý như sau:
- Chỉ thêm những giá trị
property
thật sự cần thiết
useEffect(() => { console.log("Component is invoked when vote.value changes"); }, [vote.value]);
Truyền vote.value
vào mảng dependency
thay vì đưa cả object
vote
vào như trước.
Song, chẳng phải lúc nào value
cũng tồn tại trong vote
(optional property) hoặc nếu object
đó có nhiều properties
thì ta phải liệt kê hết vào hay sao? ))
Đến đây thì có thể tham khảo 03 cách tiếp theo:
useEffect(() => { console.log("Component is invoked when JSON.stringify(vote) changes"); }, [JSON.stringify(vote)]);
- Kết hợp
useRef()
and một số helpers
hỗ trợ so sánh object
const useDeepCompareWithRef = (value) => { const ref = useRef(); // Hoặc 1 helper deep comparison 2 objects thay vì lodash _.isEqual() if (!_.isEqual(value, ref.current)) { ref.current = value; } return ref.current; }; useEffect(() => { console.log("Component is invoked when vote changes with useDeepCompareWithRef()"); }, [useDeepCompareWithRef(vote)]);
- Dùng
use-deep-compare-effect
Tới đây, nếu object
của chúng ta vẫn quá phức tạp để so sánh (thông thường thì không tới mức đó, hoặc chỉ đơn giản là bạn muốn kế thừa open source
sẵn có ) thì có thể tham khảo package này
.
Chỉ cần thay useEffect()
bằng useDeepCompareEffect()
là mọi thứ ổn thỏa, chẳng cần "xoắn quẩy"
nữa:
import useDeepCompareEffect from 'use-deep-compare-effect'; useDeepCompareEffect(() => { console.log("Component is invoked when vote changes with useDeepCompareEffect()"); },[obj])
■ Dependency chứa function
Xét một trường hợp dưới đây:
const FuncDependency = ({ data}) => { const doSomething = () => { console.log(data); }; useEffect(() => { doSomething(); }, []); // ... };
doSomething()
sử dụng props
data
, nhưng data
lại không nằm trong dependency
. Điều này dẫn tới việc khi ai đó cập nhật data
, doSomething()
sẽ không được gọi lại.
Theo Trang chủ React:
It is only safe to omit a function
from the dependency list
if nothing-in-it
(or the functions called by it) references props, state, or values derived from them.
The recommended fix is to move that function inside of your effect
.
Do đó, trong trường hợp này, chúng ta có thể định nghĩa doSomething
bên trong useEffect()
rồi gọi luôn.
Thực tế trong dự án, chúng ta thường tách các requests
, logics
, UIs
, helpers
thành các files
độc lập để thuận tiện cho việc tái sử dụng và viết unit test
hoặc hàm đó là một props
nhận từ component cha
.
Cùng đi tới một ví dụ nữa, chúng ta có 02 components
: Parent
và Child
:
Parent
là component cha của Child
Parent
truyền 2 hàm updateAnyStates
và updateCounter
xuống Child
như đoạn code
dưới đây:
const Parent = () => { const [counter, setCounter] = useState(0); const [anotherState, setAnotherState] = useState(0); const doSTOnAnyChange = () => { console.log("doSTOnAnyChange runs on ANY changes") }; const doSTOnCounterChange = () => { console.log("doSTOnCounterChange should run on COUNTER changes") }; return ( <> <button onClick={() => setCounter(counter + 1)}>Update counter state</button> <button onClick={() => setAnotherState(anotherState + 1)}>Update a different state</button> <Child doSTOnAnyChange={doSTOnAnyChange} doSTOnCounterChange={doSTOnCounterChange} /> </> ); }
const Child = ({ doSTOnAnyChange, doSTOnCounterChange }) => { useEffect(() => { doSTOnAnyChange(); }, [doSTOnAnyChange]); useEffect(() => { doSTOnCounterChange(); }, [doSTOnCounterChange]); return <p>Child</p> }
Cùng đoán xem điều gì sẽ xảy ra khi chúng ta click vào 2 buttons
nào?
Luôn có 2 chuỗi
> doSTOnAnyChange runs on ANY changes > doSTOnCounterChange should run on COUNTER changes
được log
dưới cửa sổ Console
, điều này nghĩa là, khi Parent
re-renders
, Child
nhận thấy sự thay đổi của cả updateAnyStates
, updateCounter
.
Thực tế thì hàm doSTOnCounterChange
- đúng như tên gọi của nó - chỉ cần chạy lại khi có sự thay đổi của state couter
thôi.
Tới đây thì useCallback()
được sinh ra cho "đời bớt khổ", hạn chế những lần chạy không cần thiết
Chỉ cần thay đổi một chút khi khai báo hàm doSTOnCounterChange
:
const doSTOnCounterChange = useCallback(() => { console.log("doSTOnCounterChange should run on COUNTER changes"); }, [counter]);
Tương tự với cú pháp của useEffect()
, useCallback()
cũng nhận vào 2 tham số: callback
và dependency
.
useCallback()
will return a memoized version of the callback
that only changes-its-identity if any of the dependencies has changed
, ensuring we don't create a new instance of the function every time the parent re-renders.
Với useCallback()
, chẳng cần phải lo nghĩ doSTOnCounterChange
bị tạo lại mỗi lần Parent
re-renders
nữa
Thử chạy lại và cảm nhận nhé
Source code
trong các ví dụ ở các mục trên, bạn có thể vào đây tham khảo ^^
Xong rồi thì cùng đi tiếp 02 lưu ý nhỏ xíu xiu
nữa nào!
■ Infinite loop
Một vòng lặp vô hạn (infinite loop
) có thể được tạo ra và dẫn đến các lỗi không mong muốn trong một vài trường hợp chúng ta trigger
một vài sự kiện làm component re-renders
(props
hoặc state
thay đổi) bên trong useEffect()
.
Quan sát ví dụ dưới đây:
const InfiniteLoop = () => { const [value, setValue] = useState(""); const [count, setCount] = useState(-1); useEffect(() => { setCount(count + 1); console.log("Infinite Loop is created & go on ..."); }); return ( <input type="text" value={value} onChange={({ target }) => setValue(target.value)} /> ); };
Khi component
thay đổi giá trị trường input
⇒ kích hoạt sự kiện onChange
⇒ setValue()
được gọi ⇒ Component
được re-render
⇒ callback
trong useEffect()
được gọi lại ⇒ setCount
chạy ⇒ component
lại re-render
⇒ callback
trong useEffect()
được gọi lại ⇒ setCount
chạy ⇒ ....
Cứ như vậy, một vòng lặp vô hạn được tạo ra.
Hướng giải quyết thì có thể chọn cách thêm dependency
vào params thứ 2
của hook
này:
useEffect(() => setCount(count + 1), [value]);
Do đó, trong quá trình làm việc, chúng ta cần hiểu rõ cơ chế hoạt động của useEffect()
và ReactJS lifecycle
để có thể nắm rõ được luồng chạy của ứng dụng ^^
■ Parent Effect vs. Child Effect
Giờ thì chúng ta có 02 components
:
const ParentComponent = () => { useEffect(() => { console.log('Parent Component') }); return <ChildComponent /> } function ChildComponent() { useEffect(() => { console.log('Child Component') }); }
Khi ParentComponent
được render
, chuỗi Child Component
sẽ được log
ra trước Parent Component
.
Hmm... Qua đây thì cần lưu ý gì không nhỉ?
Giả sử chúng ta cần làm chức năng Tự động thanh toán
. Đoạn code
xử lý này được viết trong component con
sau mỗi lần render
. Trong khi đó, thông tin hóa đơn (tổng chi phí, thông tin giảm giá, tổng thanh toán hay các chi tiết bắt buộc khác) lại được xử lý trong effect
của component cha
!?!
Như vậy thì có gì đó "chưa ổn"
rồi, thanh toán không thành công
Thông qua ví dụ này, điều mình muốn nhấn mạnh là, ngoài việc nắm rõ được thứ tự lifecycle-trong-1-component
, chúng ta cũng cần lưu ý một chút về tương-quan-lifecycle-giữa-các-components
để có thể xây dựng một cấu trúc components
phù hợp nhaaa
■ Kết
Như vậy là chúng ta đã cùng nhau điểm qua cơ chế hoạt động của useEffect()
và một số trường hợp thú vị xung quanh nó rồi.
Hy vọng rằng bài viết này có thể giúp ích được các bạn đang tiếp cận với ReactJS
, từ đó có thể hiểu về luồng của ứng dụng và kiểm soát được một số lỗi liên quan tốt hơn.
Cảm ơn các bạn đã đọc bài chia sẻ này. Tặng mình 1 upvote
để có thêm động lực cho những bài viết sắp tới nhé
Và trong thời điểm hiện tại thì...
Mặc dù thời gian này (thời điểm mình publish
bài viết, 01/12/2021), Hà Nội
đã nới lỏng giãn cách xã hội và việc tiêm vaccine Covid-19
cũng đã được triển khai, song, chúng ta cũng chưa thể chủ quan, hãy tiếp tục tuân thủ quy tắc 5K
được Bộ Y tế
khuyến cáo:
Khẩu trang - Khử khuẩn - Khoảng cách - Không tập trung - Khai báo y tế
để có thể giữ an toàn cho bản thân và mọi người xung quanh
Chúc các bạn ngày làm việc hiệu quả! Tiện ghé quanh nhà mình chơi một chút rồi về!
■ Credits
Happy coding!