Qua bài viết này chúng ta sẽ cùng trả lời 2 câu hỏi bạn cần đặt ra khi tiếp cận một đồ chơi mới như React Hook
- Tại sao nó lại tồn tại trên trái đất này?
- Nó tồn tại trên trái đất này có lợi ích gì không?
Lịch sử
Tháng 5, 2013
Nếu bạn còn nhớ về cách viết một component trong React bằng React.createClass
, chứng tỏ bạn đã là già làng trong React, ngày mới ra đời khi javascript không hề có khai báo class
, chúng ta sẽ khai báo component như thế này
const ReposGrid = React.createClass({
getInitialState () {
return {
}
},
componentDidMount () {
},
componentDidUpdate (prevProps) {
},
render() {
return (<div />)
}
})
Tháng giêng, 2015
Tổ chức Ác ma thế giới công bố chuẩn EcmaScript 2015, còn gọi với tên thân thương ES6. class chính thức có mặt trong javascript. Đội ngũ phát triển của React lúc đó kết luận, chúng ta không cần phát minh lại cái bánh xe (don't reinvent the wheel), cứ xài theo chuẩn đã có. Thế là từ đó chúng ta khai báo component bằng class extends
class ReposGrid extends React.Component {
constructor (props) {
super(props)
this.state = {
repos: [],
loading: true
}
this.updateRepos = this.updateRepos.bind(this)
}
componentDidMount () {
}
componentDidUpdate (prevProps) {
}
render() {
return (<div />)
}
}
Khi khai báo component bằng class
, chúng ta khởi tạo giá trị của state
bên trong phương thức constructor
và nó sẽ được nhét vào trong this
. Tuy nhiên, với cách khai báo đã quốc tế hóa của class
, nếu chúng ta extends
từ một class
, chúng ta phải gọi super()
trước khi có thể sử dụng this
. Và riêng với React, chúng ta còn phải truyền thêm props
vào trong super
. Các bạn lập trình viên phát bệnh vì cách viết chướng mắt này.
constructor (props) {
super(props) // 🤮
}
Ngày xưa khi dùng createClass
của React, bên trong hàm đó nó sẽ làm luôn chuyện binding toàn bộ this
vào các phương thức cho một instance của component. Tuy nhiên khi viết extends React.Component
chuyện đó ko còn tự động xảy ra như phép màu nữa, chúng ta phải đi .bind
từng phương thức một trong constructor
constructor (props) {
this.updateRepos = this.updateRepos.bind(this) // 😭
}
Nếu nhìn vào các bạn sẽ nói, ồ cái này chả to tác gì đâu, chỉ là phải viết thêm mấy dòng ấy mà. Cũng vì lầm đường lạc lối theo class Ác ma mà React bị ko biết bao nhiều lời phàn nàn từ những lập trình viên khắp mọi nơi.
Hên sao, không lâu sau đó Class Field được thêm vào trong class
, chúng ta có thể khai báo một biến bên trong class
mà không cần dùng constructor
, thay vì .bind
chúng ta dùng arrow function
class ReposGrid extends React.Component {
state = {}
updateRepos = (id) => {}
}
Vấn đề tồn đọng
Vấn đề đã được giải quyết tương đối ổn thỏa? Tuy nhiên vẫn còn vấn đề khác React team cảm thấy chưa hài lòng lắm phiên bản hiện tại.
Ý tưởng chính của React là để chúng ta có thể quản lý những ứng dụng phức tạp bằng cách chia ra thành từng component nhỏ rồi kết hợp (compose) lại với nhau. Đây là cách làm tạo ra thương hiệu sáng ngời của React. Cách tiếp cận theo kiểu component chả có vấn đề gì, cách hiện thực những component hiện tại đang có vấn đề.
Logic trùng lặp
Trước đây chúng ta thiết kế component dựa rất nhiều vào component lifecycle. Chúng ta đặt để logic vào trong các từng lifecycle này, thí dụ như chúng ta cần phải gọi cùng một hàm bên trong cả 2 phương thức lifecycle componentDidMount
, componentDidUpdate
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
Để giải quyết vấn đề side effect (hàm fetchRepos
làm cái quần gì ở ngoài đường ai mà biết, rõ ràng nó không thuộc phạm vi quản lý của component). Chúng ta cần một cách tiếp cận khác không thể sử dụng lifecycle nữa
Chia sẽ logic
Khi nghĩ về sự kết hợp giữa các component trong React, chúng ta sẽ nghĩ về cách các đối tượng UI kết hợp với nhau.
view = fn(state)
Trong thực tế, viết một ứng dụng không phải chỉ bao gồm tầng UI, rất nhiều trường hợp chúng ta cần tái sử dụng logic, kết hợp các logic lại với nhau. Trước đây React chưa hề có cách nào đáp ứng được nhu cầu này.
Ví dụ nếu có một component khác, nó cũng cần xài biến state repos
và tất cả những logic liên quan, mà những cái đó nó đang nằm bên trong component ReposGrid
. Làm sao chúng ta lấy ra xài lại? Cách bình thường là chúng ta copy-paste toàn bộ code bên trong qua một component mới. Cũng nhiều người làm vậy, ai có kinh nghiệm hơn thì dùng Higher-Order Component
function withRepos (Component) {
return class WithRepos extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render () {
return (
<Component
{...this.props}
{...this.state}
/>
)
}
}
}
Rồi giờ có bất kỳ component nào muốn dùng repos
thì cứ mẹ-bồng-con thế này
// ReposGrid.js
function ReposGrid ({ loading, repos }) {
...
}
export default withRepos(ReposGrid)
// Profile.js
function Profile ({ loading, repos }) {
...
}
export default withRepos(Profile)
Hồi xưa chúng ta hay làm vậy, hoặc là dùng Render Props để chia sẻ những logic dùng tới dùng lui. Tuy nhiên, đây là cách tiếp cận không dành cho dân nghiệp dư, vì không phải dễ mà hiểu được cách tụi HOC nó chạy, thứ 2 nếu bạn cho chục mẹ bồng một đứa con thì sẽ sinh ra chuyện wrapper hell
giống như callback hell
export default withHover(
withTheme(
withAuth(
withRepos(Profile)
)
)
)
Vận động não để hiểu đoạn này chạy kết quả thể nào
<WithHover>
<WithTheme hovering={false}>
<WithAuth hovering={false} theme='dark'>
<WithRepos hovering={false} theme='dark' authed={true}>
<Profile
id='JavaScript'
loading={true}
repos={[]}
authed={true}
theme='dark'
hovering={false}
/>
</WithRepos>
</WithAuth>
<WithTheme>
</WithHover>
Tóm lại những vấn đề trước mặt cần giải quyết là gì
- gọi
super(props)
là quá xàm xí đú this
là thứ mơ hồ mà không dễ biết cách nó hoạt động, bạn có thể là chuyên gia và biết đấy, nhưng chúng ta tuân thủ nguyên tắc khi code KISS, ngu ngốc nhất có thể, đừng tỏ ra thông minh- Tổ chức logic theo các phương thức lifecycle không còn hợp lý hợp tình
- React chưa có câu trả lời chính thức nào cho việc chia sẻ logic (HOC là từ pattern của javascript, không phải đặc sản nhà React, nên không tính)
Giải quyết
Từ React 0.14 chúng ta có 2 cách tạo component, dùng class hoặc dùng function. Nếu cần state và các lifecycle thì dùng class, nếu chỉ nhận props rồi trả về UI thì dùng function. Đó là cách chúng ta được dạy.
Bác CTO John Carmack nói, em xin lỗi sửa câu văn của bác chút
Tụi bây dẹp phương thức, class, framework hết dùm tao cái, Dùng hết function đi
React team, chân lý đây rồi, chúng ta tìm cách biến function component đáp ứng được những gì class component làm được đi.
Với function component, chúng ta chả cần quan tâm tới super(props)
, this
chạy thế nào. Chúng ta sẽ bổ sung state, giải quyết lifecycle, chia sẻ logic nữa là xong.
Và thế là các hook của React ra đời: useState
, useEffect
, custom hook
Để sử dụng state, chúng ta dùng hook là React.useState
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
}
Lifecycle thì có thể bạn sẽ buồn (hoặc vui) khi nghe tin này. Nếu bắt đầu sử dụng React hook, function component, dẹp hết những gì bạn đã từng biết về lifecycle của component đi, quên đi những việc cần làm ở giai đoạn này, giai đoạn kia của component. Bạn hay tiếp cận cách tư duy khác hoàn toàn Đồng bộ hóa
Thử nghĩ những gì bạn làm ở một sự kiện của lifecycle, có thể là đổi state, fetch dữ liệu, cập nhập DOM, tất cả đều gom về một mục đích duy nhất Đồng bộ hóa. Những gì chúng ta cần đồng bộ thường là những thứ nằm ngoài React (gọi API, DOM, đại loại như thế) với những thứ bên trong React (state) hoặc ngược lại
Khi tiếp cận theo hướng đồng bộ hóa thay vì lifecycle event, nó cho phép chúng ta gom các logic liên quan lại với nhau. Để làm việc đó React cho chúng ta một Hook gọi là React.useEffect
Theo định nghĩa, useEffect
cho phép chúng ta thực hiện side effect bên trong function component. Hàm này sẽ dùng để re-sync (thực hiện đồng bộ hóa các giá trị)
React.useEffect(() => {
document.title = `Hello, ${username}`
}, [username])
Đoạn code trên sẽ chạy lại bất cứ khi nào giá trị state username
có thay đổi
Để gọi lại fetchRepos
khi có thay đổi từ state repos
ở ví dụ trên
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
if (loading === true) {
return <Loading />
}
return (<div />)
}
Như vậy, chúng ta đã có thể nói lời chia tay mãi mãi với React.Component
, constructor
, super
, this
, lifecycle
Còn lại với cuộc chiến chống Higher-Order Component và Render Props. Để dùng lại logic, chúng ta sẽ vẫn dùng Hook, nhưng không phải do React làm sẵn cho xơi, chúng ta phải tự viết những custom Hook
Giờ chúng ta sẽ viết một custom hook useRepos
, nó sẽ nhận một id
lấy dữ liệu tương ứng.
function useRepos (id) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
return [ loading, repos ]
}
Điều ngon lành ở đây là tất cả những gì liên quan đến repos
điều gói gọn trong hook, ở đây mình muốn nói đến loading
, repos
Sử dụng custom hook này trên các component khác nhau
function ReposGrid ({ id }) {
const [ loading, repos ] = useRepos(id)
...
}
function Profile ({ user }) {
const [ loading, repos ] = useRepos(user.id)
...
}
Thật không thể tin được bạn có thể khai báo và setState bên trong một function bình thường. Chúng ta đã có một React mạnh mẽ với các đặc tính sau
- Đơn giản hóa
- Đóng gói
- Linh động
- Mở rộng
Không những giải quyết vấn đề đang có, như cách mà các bạn làm marketing cho React tuyên truyền: sử dụng Hook để có state
bên trong function component. Thật ra nó còn mang tới những giá trị to lớn khác là tăng khả năng tái sử dụng và kết hợp logic.
Initializing...