Sử dụng state trong react một cách hiệu quả
Khi viết một ứng dụng React, việc sử dụng state là việc khó tránh khỏi. Vậy sử dụng state như thế nào cho đúng, tôi xin đưa ra một vài quy tắc giúp việc sử dụng state hiệu quả và đúng đắn hơn Đưa dữ liệu vào state khi cần thiết Đầu tiên cũng là phần quan trọng nhất, việc quản lí dữ liệu trong ...
Khi viết một ứng dụng React, việc sử dụng state là việc khó tránh khỏi. Vậy sử dụng state như thế nào cho đúng, tôi xin đưa ra một vài quy tắc giúp việc sử dụng state hiệu quả và đúng đắn hơn
Đưa dữ liệu vào state khi cần thiết
Đầu tiên cũng là phần quan trọng nhất, việc quản lí dữ liệu trong state rất quan trọng, dữ liệu state của một component không nên bị phụ thuộc vào props truyền vào. Bạn có thể nghĩ như sau nếu state bị phụ thuộc vào props, ta sẽ có một câu hỏi props truyền vào thì khi props thay đổi state có thay đổi không?
Để dễ hiểu hơn ta sẽ lấy một ví dụ như sau
var React = require('react/addons'); class UserWidget extends React.Component { // ... // BAD: set this.state.fullName with values received through props constructor (props) { this.state = { fullName: `${props.firstName} ${props.lastName}` }; } render () { var fullName = this.state.fullName; var picture = this.props.picture; return ( <div> <img src={picture} /> <h2>{fullName}</h2> </div> ); } }
Trong ví dụ này ta có thể thấy có những vấn đề sau
-
Component UserWidget hoàn toàn không quản lí dữ liệu firstName và lastName. Đơn giản nó chỉ nhận firstName và lastName rồi hiển thị ra giá trị tương ứng.
-
Khi props firstName và lastName thay đổi thì phần hiển thị của component UserWidget vẫn không thay đổi. Bởi vì hàm khởi tạo (contructor) chỉ chạy duy nhất một lần khi khởi tạo component nên state vẫn giữ nguyên giá trị ban đầu trong khi props đã thay đổi rồi.
Để giải quyết vấn đề này việc đơn giản là ta không xử dụng state để lưu giá trị fullName mà sẽ tính toán trực tiếp từ props
render () { var fullName = `${this.props.firstName} ${this.props.lastName}`; // ... }
Sau khi thay đổi như trên thì giá trị của fullName luôn được thay đổi tương ứng với giá trị props firstName và lastName.
Lưu trữ dữ liệu một cách đơn giản và dễ thao tác nhất
Cấu trúc dữ liệu của state nên đơn giản, dễ hiểu dễ thao tác nhất có thể. Dưới đây là một ví dụ việc sử dụng dữ liệu state không đúng, rất khó để thao tác, làm cho logic code trở nên rất rối và khó hiểu
var React = require('react/addons'); var cx = React.addons.classSet; class ArbitraryWidget extends React.Component { // ... constructor () { this.state = { classes: [] }; } // BAD: push 'hover' into this.state.classes when mousing over the component handleMouseOver () { var classes = this.state.classes; classes.push('hover'); this.setState({ classes: classes }); } // BAD: remove 'hover' from this.state.classes when the mouse leaves the component handleMouseOut () { var classes = this.state.classes; var index = classes.indexOf('hover'); classes.splice(index, 1); this.setState({ classes: classes }); } // BAD: toggle 'active' in this.state.classes when the component is clicked handleClick () { var classes = this.state.classes; var index = classes.indexOf('active'); if (index != -1) { classes.splice(index, 1); } else { classes.push('active'); } this.setState({ classes: classes }); } render () { var classes = this.state.classes; return ( <div className={cx(classes)} onClick={this.handleClick.bind(this)} onMouseOver={this.handleMouseOver.bind(this)} onMouseOut={this.handleMouseOut.bind(this)} /> ); } }
Đọc một lúc thì bạn cũng có thể hiểu được component này làm những gì. Chúng ta có state classes lưu trữ các className của component này bao gồm active và hover. Khi người dùng hover qua component thì state classes của component này sẽ bao gồm cả phần tử hover, ngược lại khi người dùng không hover nữa thì cũng xóa phần tử hover ra khỏi state classes tương tự với className active. Bạn có thể thấy nó vẫn hoạt động bình thường. Tuy nhiên việc thao tác với dữ liệu kiểu mảng (array) trong trường hợp này là không đúng và gây khó hiểu. Ta thấy việc component này có className hover hay active không giống với dữ liệu kiểu boolean (true, false) hơn, và việc tách chúng độc lập cũng dễ thao tác hơn. Như vậy ta sẽ chuyển sang sử dụng 2 flag là isHovering, isActive tương ứng với horver và active.
var React = require('react/addons'); var cx = React.addons.classSet; class ArbitraryWidget extends React.Component { // ... constructor () { this.state = { isHovering: false, isActive: false }; } // GOOD: set this.state.isHovering to true on mouse over handleMouseOver () { this.setState({ isHovering: true }); } // GOOD: set this.state.isHovering to false on mouse leave handleMouseOut () { this.setState({ isHovering: false }); } // GOOD: toggle this.state.isActive on click handleClick () { var active = !this.state.isActive; this.setState({ isActive: active }); } render () { // use the classSet addon to concat an array of class names together var classes = cx([ this.state.isHovering && 'hover', this.state.isActive && 'active' ]); return ( <div className={cx(classes)} onClick={this.handleClick.bind(this)} onMouseOver={this.handleMouseOver.bind(this)} onMouseOut={this.handleMouseOut.bind(this)} /> ); } }
Sau khi tối ưu hóa code (refactor) ta thấy đoạn code này dễ đọc hơn rất nhiều và logic code cũng trở nên đơn giản hơn. Ta thấy this.state.isHovering sẽ dễ hiểu hơn rất nhiều so với việc sử dụng this.state.classes.indexOf('hover') != -1 để xác định xem component này có được hover không. Việc thao tác với isHovering cũng đơn giản hơn nhiều so với classes. Sau khi tối ưu hóa lại code thì component này cũng dễ mở rộng và kiểm thử hơn.
Không tính toán hay so sánh điều kiện ở phương thức render
Việc tính toán quá nhiều hoặc có nhiều điều kiện so sánh làm cho phương thức render trở nên phức tạp và khó kiểm soát. Hơn nữa việc viết tất cả một khối code phức tạp ở trong một phương thức cũng là không nên. Tốt nhất là ta nên di chuyển nó ra thành các phương thức hỗ trợ (helper) với tên mô tả đúng công việc mà nó thực hiện. Điều này làm giảm sự phức tạp của phương thức render giúp đoạn code của bạn trở nên sáng sủa và dễ đọc hơn.
// GOOD: Helper function to render fullName renderFullName () { return `${this.props.firstName} ${this.props.lastName}`; } render () { var fullName = this.renderFullName(); // ... }
Không lưu trữ các giá trị vào trong một đối tượng của một component như là một thuộc tính
var React = require('react/addons'); class ArbitraryWidget extends React.Component { // ... constructor () { this.derp = 'something'; } handleClick () { this.derp = 'somethingElse'; } render () { var something = this.derp; // ... } }
Việc lưu trữ các giái trị vào một thuộc tính của component là không tốt, không chỉ bởi vì nó đi ngược lại với quy định lưu trữ trạng thái của component trong state mà còn bởi vì khi lưu trữ theo kiểu này khi các thuộc tính thay đổi thì component cũng không tự động render theo giá trị mới được cập nhật.
Không thay đổi state một cách trực tiếp
State nên được thay đổi thông qua phương thức setState, việc thay đổi trực tiếp giá trị của state mà ko qua setState sẽ làm cho trạng thái của state không khớp với phần hiện thị ra bên ngoài (khi thay đổi state một cách trực tiếp component sẽ không tự động render). Dưới đây là một ví dụ
// Wrong this.state.comment = 'Hello';
// Correct this.setState({comment: 'Hello'});
Chú ý: việc thay đổi giá trị của state một cách trực tiếp chỉ được phép sử dụng ở trong hàm tạo (contructor).
State được update một cách không đồng bộ
Đây là một điểm đặc biệt cần chú ý. Vì setState hoạt động bất đồng bộ, nên khi sử dụng setState cần chú ý trong trường hợp dùng lại giá trị của state cũ. Ta có ví dụ sau
// Current: this.state.counter is 0 // Wrong this.setState({ counter: this.state.counter + 1}); this.setState({ counter: this.state.counter + 1}); this.setState({ counter: this.state.counter + 1});
Trong ví dụ trên ta thấy ban đầu state counter có giá trị là 0. Sau 3 lần chạy lệnh setState tăng giá trị của counter lên 1 đơn vị. Nhưng thực tế thì giá trị của counter chỉ tăng thêm 1 đơn vị chứ không phải là 3 đơn vị như chúng ta vẫn nghĩ. Vậy tại sao lại có điểm khác biệt này? Chúng ta có thể nghĩ đơn giản là 3 lệnh này chạy bất đồng bộ, lệnh thứ 2 không cần lệnh thứ nhất phải chạy xong mới chạy tiếp. Có thể coi 3 lệnh này chạy cùng lúc, tại thời điểm chạy thì cả 3 lệnh đều nhận state counter là 0 và chúng đều cập nhật state counter thành 1. Để giải quyết được vấn đề trên ta làm như sau
// Current: this.state.counter is 0 // Correct this.setState((prevState) => ({ counter: prevState.counter + 1 })); this.setState((prevState) => ({ counter: prevState.counter +1 })); this.setState((prevState) => ({ counter: prevState.counter + 1 }));
Sau khi thay đổi thành như trên ta sẽ thấy, mỗi lệnh setState sẽ được truyền vào là một function có parameter là prevState (state trước đó). Phần thân của function sẽ tính toán counter bằng cách lấy state counter trước đó cộng thêm 1 đơn vị. Sau 3 lần chạy lệnh setState trên ta nhận được state counter tăng lên 3 giá trị, đúng với giá trị ta mong muốn. Các bạn cần chú ý đặc điểm này để tránh những lỗi phát sinh khi sử dụng react !!!