Tìm hiểu ReactJs cơ bản và cách sử dụng
Nếu chưa từng sử dụng React thì bạn đã tìm đến đúng nơi rồi đấy. Còn nếu đã từng dùng qua React nhưng lại vướng vào một số vấn đề khó hiểu, bạn nên đọc bài viết này. Hướng dẫn này bao gồm tất cả những điều cơ bản. React là gì và tại sao bạn nên sử dụng nó? Trang chủ React đã trả lời cho ...
Nếu chưa từng sử dụng React thì bạn đã tìm đến đúng nơi rồi đấy. Còn nếu đã từng dùng qua React nhưng lại vướng vào một số vấn đề khó hiểu, bạn nên đọc bài viết này. Hướng dẫn này bao gồm tất cả những điều cơ bản.
React là gì và tại sao bạn nên sử dụng nó?
Trang chủ React đã trả lời cho chúng ta câu hỏi đầu tiên một cách rõ ràng. Còn nếu bạn hỏi tôi, tôi sẽ định nghĩa React như một "cơ chế hiển thị giao diện dựa trên nền tảng Javascript - JavaScript-based UI rendering engine". Lưu ý một chút là tôi đã bỏ "HTML" ra khỏi định nghĩa về React. Đó là bởi vì HTML chỉ là một trong rất nhiều các đối tượng khả thi mà React có thể làm việc được. Tôi cũng sẽ bỏ "browser" ra vì bạn cũng có thể sử dụng React trên server và native apps. "Engine" trong định nghĩa của tôi thì tương tự như các game engines. Với React, nó cũng giống như là một game engine, bạn chỉ cần xác định trạng thái của một ứng dụng, chuyển nó sang view, nếu có gì thay đổi, đừng thay đổi view, thay vào đó bạn chỉ cần render lại nó là nhận được view mới.
Chúng tôi đã chuyển tiếp từ Backbone views sang React views. Với React việc duy trì tính đồng bộ giữa application state (trạng thái ứng dụng) và view sẽ không còn phức tạp nữa. Chỉ cần render lại, clean-up, và lặp đi lặp lại. Do đó, bạn có thể dễ dàng xây dựng các UI components (thành phần giao diện) hoàn toàn độc lập, và có thể tái sử dụng. Nếu có sự thay đổi, cứ vô tư mà tái cấu trúc lại các views!
Hello world
Một ví dụ đơn giản và phổ biến nhất
React.render( <div>Hello, world!</div>, document.body );
Thật đơn điệu phải không? Rõ ràng React là một công cụ cực kỳ mạnh mẽ, vậy mà chúng ta lại sử dụng nó để in ra câu nói nhàm chán "Hello World!". Ok, tạm quên cái ví dụ trên đi đã, bây giờ chúng ta sẽ nói một chút về JSX.
JSX
Nếu đã từng nghe nói về React, bạn cũng có thể đã nghe qua về JSX. Để thực sự cảm thấy hứng thú trong việc sử dụng JSX, bạn phải lưu ý đến hai điều sau:
Bạn phải thực sự hiểu JSX là gì, cũng như tất cả các sắc thái của nó. Bạn phải có kinh nghiệm trong việc ra quyết định trong trường hợp nào thì nên sử dụng nó (React) và thực sự coi đó là điều cần thiết.
JSX không phải một ngôn ngữ template
Điều đầu tiên dễ dàng nhận thấy là JSX không phải một ngôn ngữ template. Nếu bạn nhầm lẫn nó với Handlebars, hãy dừng ngay suy nghĩ đó lại. JSX chỉ đơn giản là một cú pháp thay thế JavaScript, mặc dù cú pháp đó là đặc trưng của React (ít nhất ở thời điểm hiện tại). Ví dụ trên được viết lại như sau:
React.render( React.createElement('div', {}, 'Hello, world!'), document.body );
Cứ coi như React.createElement là thứ gì đó chưa biết, vì thế chúng ta sẽ làm nó trở nên rõ ràng hơn một chút. Khi sử dụng React, bạn thường sẽ không làm việc trực tiếp với DOM. Thay vào đó, bạn tạo các thành phần Virtual DOM. Virtual DOM thực ra là các JSON objects. Chúng là thể hiện của một cấu trúc bên dưới DOM nhưng không mang bất cứ đặc tính nào của các DOM thật. React sẽ convert các thành phần này thành các thành phần DOM thật (actual/real DOM) khi cần thiết. React.createElement là một phương thức được sử dụng để tạo ra các JSON objects này. Cú pháp như sau:
React.createElement(elementNameOrClass, props, children...); Trong ví dụ Hello World ở trên, chúng ta có sử dụng một thẻ HTML làm tham số đầu tiên. Các thẻ HTML ở vị trí tham số này thì còn được coi như là các component classes. Tham số thứ hai được sử dụng để truyền các properties cho component class trước đó. Các tham số còn lại trở thành property đặc biệt, là children của component. Chúng ta sẽ nói rõ về nó sau. Còn bây giờ, bạn chỉ cần nhớ rằng JSX (đại khái là) thực hiện việc chuyển đổi đoạn code trên như sau:
Nếu tham số đầu tiên là một chuỗi ánh xạ của một thẻ HTML nào đó thì sử dụng component class như là thẻ HTML đó. Nếu tag name không phải là một HTML element, cứ coi như nó là một biến cục bộ có tên trỏ vào một component class tùy chọn. Các thuộc tính được convert thành một đối tượng và được truyền vào như tham số thứ hai. Các thành phần con được truyền vào như các tham số còn lại. Ví dụ về JSX:
<div id="greeting-container" className="container"> <Greeting name="World"/> </div>
Nó có thể được convert thành
React.createElement("div", { id: "greeting-container", className: "container" }, React.createElement(Greeting, {name: "World"}) )
Handlebars, Django templates và rất nhiều template languages khác cho phép bạn thay thế giữa các plain text với một số template chuyên dùng. Template language hiếm khi có khả năng diễn đạt như của host language (ngôn ngữ lập trình chính thống: PHP, Python, Javascript...). JSX chỉ convert các đánh dấu XML thành code JavaScript. Nó tương tự như là bạn đang gõ code Javascript vậy.
Điều này rất tuyệt vì bạn có thể sử dụng tối đa sức mạnh của JavaScript và tất cả mọi thứ bạn đã học về nó như các biểu thức, cấu trúc hàm, hay bất kỳ các tính năng nào khác trong views của bạn. Bạn không cần viết các helpers (hàm trợ giúp chuyên dùng) đặc trưng cho template language của mình mà chỉ cần sử dụng Javascript
var names = ['Moe', 'Larry', 'Curly']; React.render( <div> { names.map(function (name) { return <div>Hello, {name}!</div> }) } </div>, document.body );
Dấu ngoặc {} cho phép chúng ta sử dụng Javascript thuần, nó không phải là một cơ chế escape dữ liệu. Chú ý là bên trong map function, chúng ta trả về một markup là một thẻ div, bên trong markup đó chúng ta lại có một biểu thức Javascipt. Đây là một ví dụ rõ ràng về việc JSX không phải là một template language.
JSX hoàn toàn không bắt buộc, nhưng lại thực sự hữu ích
Theo quan điểm cá nhân, tôi thấy JSX thực sự hữu ích vì hai lý do:
Nó cung cấp các biểu thức viết tắt (shorthand) hữu ích cho một số React boilerplate mà bạn bắt buộc phải sử dụng dù muốn hay không. Nó làm HTML markup của bạn trông giống như HTML markup. Lý do đầu tiên đã khá rõ ràng qua các ví dụ trên. Các ví dụ về JSX ngắn hơn so với các ví dụ về JS tương ứng. Tất nhiên, chúng ta có thể khéo léo làm theo cách của mình. Hãy xem xét ví dụ sau đây:
var R = React.DOM; Greeting = React.createFactory(GreetingClass); React.render( R.div({id: "greeting-container", className: "container"}, Greeting({name: "World"}) ), document.body );
React.DOM cung cấp một tập các factories cho các thẻ HTML. React.createFactory chỉ là một helper ràng buộc component class của bạn với React.createElement để bạn có thể tạo các factories cho riêng mình. Theo quan điểm của tôi, vấn đề khá lớn ở đây là code Javascript trộn trong các markup. Hãy xem markup sau:
<p> The live example is the same. The only difference is that we render to <code>mountNode</code>, which is just the DOM node for the example. </p>
Khi chuyển sang Javascript sẽ rất khó nhìn
R.p({}, 'The live example is the same. The only difference is that we render to ', R.code({}, 'mountNode' ), ', ' + 'which is just the DOM node for the example.' )
Ví dụ trên là một vấn đề thực tế khiến tôi nhận ra mình chỉ nên sử dụng JSX.
Nếu bạn có một designer đang làm việc với HTML thì sẽ không có nhiều hướng dẫn để chuyển sang JSX. Thay thế tất cả các markup với JavaScript đó sẽ mất rất nhiều công sức, mà có thể thằng designer sẽ tức điên lên mà thông bạn!
Các components tùy chỉnh
Từ đầu đến giờ, tôi đã giải thích với bạn JSX là gì, cách sử dụng JSX cho các views của bạn như thế nào (thực chất nó chỉ là JavaScript) để giúp bạn có một view language thật sự mạnh mẽ. Nhưng đó đều không phải các lý do để sử dụng React. Một khi đã bắt đầu tạo ra các components tùy chỉnh, bạn sẽ thấy được lý do thực sự khiến bạn không thể rời mắt khỏi React.
Component đầu tiên
Chúng ta hãy thử bắt đầu tạo một ứng dụng ghi chú. Để giữ mọi thứ đơn giản, ta sẽ bỏ qua việc lưu trữ dữ liệu trên server. Hãy bắt đầu một cách thật đơn giản và xác định dữ liệu cần để render.
var notepad = { notes: [ { id: 1, content: "Hello, world! Boring. Boring. Boring." }, { id: 2, content: "React is awesome. Seriously, it's the greatest." }, { id: 3, content: "Robots are pretty cool. Robots are awesome, until they take over." }, { id: 4, content: "Monkeys. Who doesn't love monkeys?" } ] }
Giờ chúng ta sẽ tạo một custom component và render một danh sách các notes của chúng ta. Dòng đầu tiên của note sẽ là tiêu đề (chưa xét tới các vấn đề về tối ưu hóa):
var NotesList = React.createClass({ render: function () { var notes = this.props.notepad.notes; return ( <div className="note-list"> { notes.map(function (note) { var title = note.content.substring(0, note.content.indexOf(' ') ); title = title || note.content; return ( <div key={note.id} className="note-summary"> {title} </div> ); }) } </div> ); } });
Lưu ý rằng một React component đơn giản chỉ cần một method render. Có rất nhiều methods khả dụng khác, nhưng render là method chủ đạo. Trong method render, bạn chỉ trả về thứ bạn muốn render. React sẽ đảm đương công việc từ đây.
Cũng cần lưu ý rằng tôi đã đặt một thuộc tính key trong đó. Thuộc tính key này là duy nhất, nó xác định một component bên cạnh các components anh chị của nó. Nói chung, nếu bạn đang sử dụng map hoặc filter hoặc một phương thức tương tự nào khác, bạn phải đặt cho mỗi element một key. Đôi khi với một element duy nhất mà nó đại diện cho một thứ duy nhất thì bạn cũng cần phải sử dụng key để đảm bảo React hủy bỏ DOM thay vì tái sử dụng nó. Chúng ta sẽ cùng thảo luận về sự cần thiết của việc này sau. Còn bây giờ, bạn chỉ cần hiểu được rằng React sẽ cảnh báo nếu bạn không làm điều đó. Và nếu bạn bỏ qua cảnh báo này, những bugs kỳ lạ có thể sẽ xảy ra.
Mọi thứ đang dần trở nên thú vị. Nếu React chỉ có thể render <div> và các thẻ tương tự thì có vẻ không hữu dụng lắm. Nhưng bạn lại có thể tạo các components của riêng mình. Thậm chí là tốt hơn, giao diện và tính năng của các components đó hoàn toàn được điều khiển bởi các thuộc tính được truyền vào nó. Component NotesList trên hoạt động như một hàm thuần. Tất cả các trạng thái của nó được truyền vào như các thuộc tính. Nếu sau đó chúng ta quyết định ứng dụng sẽ hỗ trợ nhiều notepads thì component này thực sự hữu ích. Chúng ta chỉ truyền vào notepad những gì chúng ta muốn render, và component sẽ làm việc của nó. Sau này việc hủy bỏ component cũng sẽ dễ dàng hơn.
var NoteSummary = React.createClass({ render: function () { var note = this.props.note; var title = note.content.substring(0, note.content.indexOf(' ') ); title = title || note.content; return ( <div className="note-summary">{title}</div> ); } }); var NotesList = React.createClass({ render: function () { var notes = this.props.notepad.notes; return ( <div className="note-list"> { notes.map(function (note) { return ( <NoteSummary key={note.id} note={note}/> ); }) } </div> ); } });
Khá dễ dàng phải không? Giống như tái cấu trúc một function lớn thành các functions nhỏ hơn, bạn có thể chia nhỏ một đoạn dữ liệu và đặt nó vào bên trong một component khác. Sau đó chỉ cần gọi component mới của bạn, truyền nó vào các mệnh đề thích hợp. Bây giờ chúng ta có thể sử dụng component NoteSummary bất cứ đâu chúng ta muốn. Nếu một note nào đó sai, chúng ta sẽ tìm được vị trí lỗi đó. Lỗi này hoặc nằm trong component hoặc trong các mệnh đề được truyền vào để tạo nó.
Reactive components
Các ví dụ trên đủ dùng để tạo một trang tĩnh, nhưng nếu phải tạo app làm công việc gì đó, như react-ive thì sao? Yên tâm đi, React đảm bảo những điều đó cho bạn.
Nếu trạng thái của app thay đổi, chỉ cần re-render nó
Cho phép tôi nhắc lại: Nếu bất cứ điều gì, bất cứ ở đâu trong app của bạn có sự thay đổi, hãy ném hết tất cả đi và re-render chúng.
Làm việc với React bạn cần phải quên đi những thói quen cũ khi sử dụng jQuery, nơi bạn phản hồi một sự kiện nào đó và sau đó thì tinh chỉnh DOM. Hãy quên các ràng buộc hai chiều (two-way binding) phức tạp hay các theo dõi về scopes trong Angular. Bất cứ khi bạn có app state mới, chỉ cần re-render app state đó.
Giờ chúng ta sẽ thực hiện việc thêm các notes. Với ví dụ đơn giản này, chúng ta sẽ thêm một số functions ở top-level để xử lý các events tương tác từ người dùng. Chúng ta sẽ giữ các components được tách biệt bằng cách truyền các handlers của chúng vào đó. Khi dev một app thực sự, bạn có thể muốn sử dụng một cái gì đó giống như Flux, đó là một pattern cho việc sử dụng các events được kiểm soát một cách chặt chẽ.
Hãy đặt phương thức React.render vào trong phương thức onChange để ta có thể gọi nó mỗi khi có sự thay đổi. Chúng ta cũng đặt vào đó một handler onAddNote để thêm notes mới và một handler onChangeNote để thay đổi notes. Tôi sẽ định nghĩa chúng như sau:
var onChange = function () { React.render( <Notepad notepad={notepad} onAddNote={onAddNote} onChangeNote={onChangeNote}/>, document.body ); }; onChange();
Bất cứ khi nào có sự thay đổi, chúng ta sẽ gọi onChange và nó sẽ re-render app state. Đừng lo lắng về cách hiển thị của nó. Tất cả được thực hiện với sự kỳ diệu của virtual DOM. React sẽ quyết định giữ DOM elements nào ở lại hay bỏ elements nào đi một cách rất thông minh. Nếu đó chính xác là DOM elements cần để re-render, nó sẽ giữ chúng và tinh chỉnh các thuộc tính và nội dung khi cần thiết.
Ngoài ra, chỉ cần bạn sử dụng các key phù hợp khi cần thiết, bạn sẽ không phải lo lắng về việc một input bị mất focus. Nếu đã từng sử dụng Backbone theo cách này, bằng việc xóa sạch DOM và bắt đầu lại từ đầu, bạn sẽ biết rằng khi một input được xóa bỏ và add lại vào document, nó sẽ mất focus, như thế thực sự là sẽ gây khó chịu cho user. Với React, nếu bạn re-render cùng một element, DOM element tương ứng sẽ vẫn còn, vì vậy user vẫn có thể tiếp tục nhập dữ liệu thoải mái ngay cả khi DOM bị thay đổi.
Bây giờ, hãy viết các handlers để add notes. Chúng ta sẽ loại bỏ các notes tĩnh lúc trước đi, thêm một cơ chế sinh ID, và tiếp tục theo dõi các notes đã chọn:
var notepad = { notes: [], selectedId: null }; var nextNodeId = 1; var onAddNote = function () { var note = {id: nextNodeId, content: '}; nextNodeId++; notepad.notes.push(note); notepad.selectedId = note.id; onChange(); };
Ta cũng thêm một handler để edit các notes mới này
var onChangeNote = function (id, value) { var note = _.find(notepad.notes, function (note) { return note.id === id; }); if (note) { note.content = value; } onChange(); };
Một lần nữa, chúng ta chỉ cần làm mọi thứ thật đơn giản. Trong một app lớn hơn, chúng ta có thể sẽ muốn làm gì đó phức tạp hơn. Nguyên tắc sẽ vẫn như cũ. Các components của chúng ta sẽ có một số cơ chế để thông báo rằng user đã thay đổi điều gì đó. Khi có thay đổi, chúng ta sẽ có một số cơ chế để re-render app. Component của chúng ta sẽ không bao giờ trở thành nguồn tin tưởng để thể hiện sự tồn tại của dữ liệu. Dữ liệu đó luôn luôn nằm bên ngoài component.
Hãy tạo một component để chỉnh sửa notes
var NoteEditor = React.createClass({ onChange: function (event) { this.props.onChange(this.props.note.id, event.target.value); }, render: function () { return ( <textarea rows={5} cols={40} value={this.props.note.content} onChange={this.onChange}/> ); } });
Component này nhận handler onChange, nó sẽ được sử dụng để cập nhật những thay đổi của note.
Chúng ta tạo một component Notepad mới để render danh sách notes, một button để tạo một note mới, và một editor để chỉnh sửa note.
var Notepad = React.createClass({ render: function () { var notepad = this.props.notepad; var editor = null; var selectedNote = _.find(notepad.notes, function (note) { return note.id === notepad.selectedId; }); if (selectedNote) { editor = <NoteEditor note={selectedNote} onChange={this.props.onChangeNote}/> } return ( <div id="notepad"> <NotesList notepad={notepad}/> <div> <button onClick={this.props.onAddNote}>Add note</button> </div> {editor} </div> ); } });
Thử kiểm tra xem nào
Bạn thêm một note mới và nhận thấy rằng tiêu đề thay đổi mỗi khi bạn nhập thêm note vào. Điều này quá tuyệt vời vì:
- Các components của chúng ta không liên quan đến app state của chúng. Tất cả tồn tại tách biệt với view.
- Các components của chúng ta không phải lo lắng về trạng thái của DOM. Nó chỉ cung cấp các fresh elements để render, và React thao tác DOM một cách kỳ diệu. Chúng ta chỉ render, ném chúng đi, sau đó lặp lại.
- Các components của chúng ta cũng giống như các hàm thuần. Chúng ta cung cấp nó trong trạng thái hiện tại, sau đó chỉ chỉ cần ánh xạ lại trạng thái đó.
Trong một app lớn hơn, quan điểm thứ ba có thể nằm ngoài tầm kiểm soát của chúng ta. Truyền vào callbacks thông qua một hệ phân cấp lồng nhau có thể sẽ khó sử dụng và quản lý. Tuy nó nằm ngoài phạm vi của bài viết này, nhưng Flux là giải pháp thường được trích dẫn cho vấn đề này. Nếu có thể, hãy làm theo các model đơn giản của việc truyền vào callbacks, code của bạn sẽ trở nên linh động hơn rất nhiều.
Hãy thêm một handler để chọn một note
var onSelectNote = function (id) { notepad.selectedId = id; onChange(); };
Ta sẽ truyền handler này vào component Notepad
var onChange = function () { React.render( <Notepad notepad={notepad} onAddNote={onAddNote} onSelectNote={onSelectNote} onChangeNote={onChangeNote}/>, document.body ); };
Sau đó ta có thể đưa code này vào component Notepad
var Notepad = React.createClass({ render: function () { var notepad = this.props.notepad; var editor = null; var selectedNote = _.find(notepad.notes, function (note) { return note.id === notepad.selectedId; }); if (selectedNote) { editor = <NoteEditor note={selectedNote} onChange={this.props.onChangeNote}/> } return ( <div id="notepad"> <NotesList notepad={notepad} onSelectNote={this.props.onSelectNote}/> <div> <button onClick={this.props.onAddNote}>Add note</button> </div> {editor} </div> ); } });
Trong component NotesList, chúng ta phản hồi một click bằng một summary và gọi callback. Ta cũng có thể style cho item được chọn
var NotesList = React.createClass({ render: function () { var notepad = this.props.notepad; var notes = notepad.notes; return ( <div className="note-list"> { notes.map(function (note) { return ( <div key={note.id} className={ notepad.selectedId === note.id ? 'note-selected' : ' }> <a href="#" onClick={ this.props.onSelectNote.bind(null, note.id) }> <NoteSummary note={note}/> </a> </div> ); }.bind(this)) } </div> ); } });
Bây giờ thì thử xem
Click vào tiêu đề trong list sẽ cho phép bạn chỉnh sửa note. App của chúng ta sẽ trở nên phức tạp hơn, nhưng từng components vẫn duy trì được sự đơn giản. Bất cứ khi nào bạn cảm thấy không kiểm soát được một component, hãy chia nhỏ nó ra.
Trạng thái tạm thời
Đôi khi một component cũng cần giữ một số trạng thái, đó không hẳn là application state. Giả sử chúng ta muốn thêm chức năng xóa một note. Và chúng ta muốn xác nhận trước khi xóa. Chúng ta không thực sự cần thiết phải giữ trạng thái có muốn tiếp tục xóa hay không, mà đơn giản chỉ cần throw state (giống throw exception) đó ra. Vì thế chúng ta muốn component của mình giữ state đó hơn là duy trì nó ở bên ngoài. React có một cơ chế mạnh mẽ để thiết lập và sử dụng state nội tại này.
Trước tiên hãy thêm một delete handler và kết nối với nó mà không cần xác thực. Các bước tương tự như trên. Chúng ta thêm một handler delete, truyền nó vào, hook onClick vào một button cho handler đó. Bây giờ bạn có thể thực hiện việc xóa note!
Trong trường hợp này, button delete là một phần của note editor. Các nguyên tắc tương tự được áp dụng nếu bạn muốn thêm nó vào summary list.
Đây là note editor của chúng ta khi không có sự xác thực
var NoteEditor = React.createClass({ onChange: function (event) { this.props.onChange(this.props.note.id, event.target.value); }, render: function () { return ( <div> <div> <textarea rows={5} cols={40} value={this.props.note.content} onChange={this.onChange}/> </div> <button onClick={ this.props