Có 2 điều quan trọng cần nói trước khi bắt đầu. Một là, chúng ta đang nói đến một cách làm (pattern) trong lập trình, ko phải đặc sản của React. Thứ 2, đây không phải là kiến thức bắt buộc để viết một ứng dụng React. Bạn có thể không cần đọc bài này, vẫn có thể vỗ ngực xưng tên là một React developer và viết React như thường. Còn nếu bạn tò mò muốn biết thêm món đồ chơi thì đọc tiếp.
Những lập trình viên mới vào nghề cũng biết đến câu thần chú "D.R.Y" (đừng tự lập lại chính mình). Câu thần chú rất đáng để trong tâm niệm. Cái cách làm Render Props này cũng là để đạt được cái gọi là DRY, giống như Higher-Order-Components.
Trước khi xem đến giải pháp, chúng ta cần biết vấn đề cần giải quyết là gì. Ví dụ chúng ta muốn làm lại cái dashboard như bên dưới
Bạn sẽ cần hiển thị một đống cái kiểu tooltip khác nhau khi một element được hover lên
Có vài cách để tiếp cận vấn đề này, một là bạn kiểm tra một component cụ thể nào đó có đang hover không, rồi hiển thị hoặc ẩn tooltip. Có 3 component bạn cần kiểm tra, bạn đưa hàm kiểm tra vào cả 3 component Info
, TrendChart
, DailyChart
Component Info
class Info extends React.Component {
render() {
return (
<svg
className="Icon-svg Icon--hoverable-svg"
height={this.props.height}
viewBox="0 0 16 16" width="16">
<path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
)
}
}
Chúng ta sẽ sử dụng onMouseOver
và onMouseOut
, dùng thêm state hovering
để chúng ta có thể kêu nó re-render
khi cần thiết
class Info extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<>
{this.state.hovering === true
? <Tooltip id={this.props.id} />
: null}
<svg
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
className="Icon-svg Icon--hoverable-svg"
height={this.props.height}
viewBox="0 0 16 16" width="16">
<path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
</svg>
</>
)
}
}
Giờ chúng ta cần copy tính năng này cho 2 component còn lại. Việc copy này vi phạm nghiêm trọng câu thần chú DRY. Chúng ta phải làm sao để sử dụng lại những logic giống nhau mà ko phải copy-paste
Trong hầu hết các trường hợp khi chúng ta dựng một component trong React, kết quả output cuối cùng là một cái UI gì đó
View = fn(state)
Tuy nhiên chỉ là hầu hết, chứ ko phải toàn bộ, có nhưng component như là wrapper của một component khác, nó chỉ mang nhiệm vụ quản lý logic.
class Users extends React.Component {
state = {
users: null
}
componentDidMount() {
getUsers()
.then((users) => {
this.setState({ users })
})
}
render() {
<Grid data={this.state.users} />
}
}
Như component Users
ở trên, không chịu trách nhiệm quản lý UI, chuyện đó là việc của Grid
Trong React, chúng ta có thể truyền function vào như prop cho component
function User (props) {
const id = props.id(true) // vuilaptrinh.com
}
<User id={(isAuthed) => isAuthed === true ? 'vuilaptrinh.com' : null} />
Với ý tưởng này, chúng ta giải quyết vấn đề trên như thế nào?
Trước tiên chúng ta tạo ra một component wrapper chịu trách nhiệm quản lý logic của hover
class Hover extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
</div>
)
}
}
Câu hỏi tiếp theo, component Hover
thì render cái gì. Vận dụng ý tưởng truyền prop là một function, chúng ta cho Hover
nhận vào một prop function có tên render
, chúng ta sẽ nhét vào trong cái function render
giá trị hovering
<Hover render={(hovering) =>
<div>
Is hovering? {hovering === true ? 'Yes' : 'No'}
<div>
} />
Việc còn lại là cập nhập lại component Hover
class Hover extends React.Component {
state = { hovering: false }
mouseOver = () => this.setState({ hovering: true })
mouseOut = () => this.setState({ hovering: false })
render() {
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
{this.props.render(this.state.hovering)}
</div>
)
}
}
Vấn đề đã được giải quyết. Bất kể khi nào cần dùng logic hover
chúng ta sẽ gọi lại component Hover
. Cách làm này được gọi với cái tên đúng như những gì đã diễn ra (truyền prop render là một function) Render Props
Một cách viết khác không dùng prop tên render
, xài luôn prop tên children
có sẵn, chúng ta sẽ viết một cách gọn hơn
function User (props) {
return (
<div>
{props.children()}
</div>
)
}
<User>
{() => This is props.children}
</User>
So với cách làm của Higher-Order-Component, Render Props sẽ không vướng phải vấn đề đụng tên props, không mất quyền kiểm soát vào tay component được wrap lại, và wrapper hell.
Initializing...