I. Giới thiệu về JSX

JSX là cách viết biểu thức Javascript dưới dạng giống với HTML.

Ví dụ:

const element = <h1>Hello, {name}</h1>;

Ứng dụng của JSX: Dùng để xây dựng UI theo hướng kết hợp giữa marking (html) & logic xử lý (javascript) trong một khối code chung.

React sử dụng JSX (tuy không bắt buộc) để phát triển ứng dụng.

Biên dịch JSX với Babel

Do JSX không phải là ngôn ngữ được trình duyệt mặc định hỗ trợ, để sử dụng JSX, cần dùng thư viện Babel để dịch JSX sang Javascript

Cách đơn giản nhất để sử dụng Babel là nhúng script Babel vào đầu chương trình html (như nhúng các script js thông thường)


      <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
      <script src="https://unpkg.com/react@17/umd/react.development.js"></script>

      <script type="text/babel">
        const element = <h1>Hello, {name}</h1>;
      </script>
    

Cách này có ưu điểm là đơn giản và dễ thực hiện, nhưng có nhược điểm là mất thời gian biên dịch mỗi khi người dùng tải trang ứng dụng.

Trong phần sau, chúng ta sẽ sử dụng tool create-react-app để biên dịch React/JSX cho môi trường production.


II. Một số khái niệm cơ bản trong React

Khung ứng dụng

        
        <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
        <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

        <script type="text/babel">
        function App() {
          let name = 'world';
          return <div>Hello {name}!</div>;
        }

        ReactDOM.render(<App />, document.getElementById('app'));
        </script>

        <body>
          <div id="app"></div>
        </body>
      
Chạy thử chương trình

Nội dung ứng dụng được hiển thị trong một element gốc (<div id="app"></div>) thông qua hàm ReactDOM.render .

Tham số đầu tiên của hàm ReactDOM.render là một function trả về nội dung hiển thị dưới dạng JSX, hoặc một class kế thừa class React.Component. Chi tiết xem phần Component ở dưới.


Component

Component là một phần của ứng dụng được tách ra dưới dạng một hàm/class.

Cách sử dụng component để viết UI tương tự như cách dùng function/module để viết logic, có 2 tác dụng chính là:

Khai báo Component

Có 2 cách khai báo Component: dùng function và dùng class.

Khai báo Component bằng function:

      function App() {
        let name = 'world';
        return <div>Hello {name}!</div>;
      }
    

Kết quả trả về của hàm là một đoạn JSX thể hiện nội dung hiển thị của Component.

Khai báo Component bằng Class

      class App extends React.Component {
        render(){
          let name = 'world';
          return <div>Hello {name}!</div>;
        }
      }
    

Class dùng để khai báo Component phải kế thừa class React.Component. Nội dung hiển thị của Component được trả về từ hàm render() của class này.

Sự khác nhau giữa 2 cách khai báo Component:

Khai báo bằng function đơn giản và gọn nhẹ, thích hợp cho các Component không có trạng thái. Khai báo bằng Class phù hợp cho các Component có trạng thái và cần được cập nhật hiển thị khi có các sự kiện.

Tuy nhiên trong các phiên bản gần đây, React bổ sung tính năng Hook cho phép các component dạng function cũng có thể chứa trạng thái và cập nhật hiển thị theo sự kiện. Về cơ bản, 2 cách khai báo Component hiện nay tương đương nhau.


Thuộc tính của Component

Thuộc tính của Component là các tham số đầu vào của các hàm hiển thị JSX.

Thuộc tính được truyền vào Component bằng cách viết tương tự như thuộc tính của các thẻ html, tuy nhiên giá trị thuộc tính là một biểu thức Javascript và được đặt trong cặp dấu ngoặc nhọn {}


      ReactDOM.render(<App name={'world'} />, document.getElementById('app'));
    

Ở ví dụ trên, thuộc tính name được truyền vào cho Component App.

Trong phần thân hàm/class của Component, thuộc tính được lấy ra từ biến props đóng vai trò như biến đầu vào của hàm hiển thị.


      // Với function-based component
      function App(props) {
        let name = props.name;    // lấy thuộc tính được truyền vào từ bên ngoài
        return <div>Hello {name}!</div>;
      }

      // Với class-based component
      class App extends React.Component {
        render(){
          let name = this.props.name; // lấy thuộc tính được truyền vào từ bên ngoài
          return <div>Hello {name}!</div>;
        }
      }
    

Trong một Component lại có các Component con và có thể truyền các thuộc tính xuống bên dưới cho các Component con đó, ví dụ:

        
        function Header(props) {
          return <h1>{props.title}</h1>;
        }

        class Body extends React.Component {
          render(){
            let content = this.props.content;
            return <div>{content}</div>;
          }
        }

        function App() {
          return (<div>
            <Header title="Title"/>
            <Body content={
              <div>
                <p>Line 1</p>
                <p>Line 2</p>
              </div>
            }/>
          </div>);
        }
        ReactDOM.render(<App />, document.getElementById('app'));
      
Chạy thử chương trình

Trạng thái (state) của Component

Trạng thái là các biến dữ liệu thay đổi theo các sự kiện tương tác với người dùng. Khi trạng thái thay đổi, Component sẽ được hiển thị lại (re-render).

Khai báo trạng thái

Khai báo trạng thái cho class-based Component:

Với class-based component, trạng thái được khai báo trong constructor của class:

      
        class Counter extends React.Component {
          constructor() {
            super();
            this.state = { value: 0 }
          }
          render(){
            return <div>{this.state.value}</div>;
          }
        }
      
Chạy thử chương trình

Biến state của class được khai báo trong constructor chứa các dữ liệu trạng thái của component. Trong hàm render, để truy xuất trạng thái này, sử dụng this.state

Khai báo trạng thái cho function-based Component:

Với function-based component, trạng thái được khai báo bằng cách sử dụng hook useState của React:

        
        function App() {
          const [value, setValue] = React.useState(0);
          return <div>{value}</div>;
        }
      
Chạy thử chương trình

Hàm useState là một hook của React. Hook là tính năng được đưa vào trong các phiên bản React gần đây nhằm giúp function-based Component có thể thực hiện được các tính năng tương tự như của class-based Component.

Hàm useState nhận một tham số đầu vào và trả về kết quả gồm 2 giá trị:

    const [value, setValue] = React.useState(0);

Lưu ý: Sử dụng useState chỉ khai báo được một biến trạng thái. Muốn có nhiều biến trạng thái như với class-based Component, phải sử dụng nhiều lệnh useState, hoặc sử dụng hook useReducer của React (không được đề cập trong tài liệu này).


Cập nhật trạng thái

Cập nhật trạng thái cho class-based Component:

Với class-based Component, việc cập nhật trạng thái được thực hiện thông qua hàm setState của class React.Component.

Ví dụ:

        
        class App extends React.Component {

          constructor(){
            super();
            this.state = {value: 0};
          }

          increase = () => {
            this.setState({value: this.state.value + 1});
          }

          render(){
            return(
              <div>
                <span>{this.state.value}</span>
                <button onClick={this.increase}>Increase</button>
              </div>
            );
          }
        }

        ReactDOM.render(<Counter />, document.getElementById('app'));
      
Chạy thử chương trình

Ở ví dụ trên, biến trạng thái value được cập nhật khi người dùng click vào button Increase:

    this.setState({value: this.state.value + 1});

Lưu ý : Nếu component có nhiều biến trạng thái thì việc setState với một/một số biến trạng thái không làm ảnh hưởng giá trị của các biến trạng thái còn lại, ví dụ:


      class Comp extends React.Component {

        constructor(){
          super();
          this.state = {a: 1, b: 2};
        }

        onBtnClick = () => {
          this.setState({a: 2}); // new state: {a: 2, b: 2}
        }

        render(){
          //...
        }
      }
    
Cập nhật trạng thái cho function-based Component:

Với function-based Component, việc cập nhật trạng thái thực hiện thông qua hàm cập nhật trả về từ useState

Ví dụ:

        
        function App() {
          const [value, setValue] = React.useState(0);
          const increase = () => setValue(value + 1);
          return (
            <div>
              <span>{value}</span>
              <button onClick={increase}>Increase</button>
            </div>
          );
        }

        ReactDOM.render(<Counter />, document.getElementById('app'));
      
Chạy thử chương trình

Ở ví dụ trên, biến trạng thái value được cập nhật ở dòng:

    const increase = () => setValue(value + 1);


Event

Tương tự như với Javascript thông thường, trong React, các component có các hàm để xử lý các sự kiện tương tác với người dùng.

Hai sự kiện thường dùng là onClickonChange:

      
        function App() {
          function btnHandler() {
            alert("Click");
          }

          function changeHandler(event) {
            alert(event.target.value);
          }
          
          return (
            <div>
              <input onChange={changeHandler}/>
              <button onClick={btnHandler}>Ok</button>
            </div>
          );
        }

        ReactDOM.render(<App />, document.getElementById('app'));
      
Chạy thử chương trình

Thuộc tính event.target.value chứa dữ liệu của phần tử khi xảy ra sự kiện on:change:

function changeHandler(event) {
  alert(event.target.value);
}

Dùng onChange để lấy lấy dữ liệu người dùng

Trong React, để lấy dữ liệu người dùng nhập vào các phần tử control (input, textarea, select, ...), cách thường dùng là khai báo biến trạng thái cho các element, sau đó khi có sự kiện onChange thì cập nhật dữ liệu vào biến trạng thái.

Lấy dữ liệu từ input/textarea:
      
        function App(props) {
          const [text, setText] = React.useState("");
          return (
            <div className="App">
              <input
                placeholder="Enter some text..."
                value={text}
                onChange={(event) => setText(event.target.value)}
              />
              <br />
              {text}
            </div>
          );
        }

        ReactDOM.render(>App />, document.getElementById('app'));
      
Chạy thử chương trình

Lấy dữ liệu từ select:
      
        function App(props) {
          const [gender, setGender] = React.useState("");
        
          return (
            <div className="App">
              <select
                value={gender}
                onChange={(event) => setGender(event.target.value)}
              >
                <option value="">--Choose your gender---</option>
                <option value="M">Male</option>
                <option value="F">Female</option>
                <option value="O">Other</option>
              </select>
              <br />
              {gender}
            </div>
          );
        }

        ReactDOM.render(>App />, document.getElementById('app'));
      
Chạy thử chương trình

Lấy dữ liệu từ checkbox:
      
        function App(props) {
          const [agree, setAgree] = React.useState(false);
        
          return (
            <div className="App">
              <input
                type="checkbox"
                checked={agree}
                onChange={(event) => setAgree(event.target.checked)}
              />{" "}
              Agree with terms & conditions
              <br />
              {agree ? "Checked" : "Unchecked"}
            </div>
          );
        }

        ReactDOM.render(>App />, document.getElementById('app'));
      
Chạy thử chương trình

Lấy dữ liệu từ radiobox:
      
        function App() {
          const [gender, setGender] = React.useState("");
        
          return (
            <div className="App">
              Your gender:

              <input
                type="radio"
                name="gender"
                value="M"
                checked={gender === "M"}
                onChange={(event) => setGender(event.target.value)}
              />{" "}
              Male

              <input
                type="radio"
                name="gender"
                value="F"
                checked={gender === "F"}
                onChange={(event) => setGender(event.target.value)}
              />{" "}
              Female
              
              {gender}
            </div>
          );
        }

        ReactDOM.render(>App />, document.getElementById('app'));
      
Chạy thử chương trình

Hiển thị có điều kiện

Trong React, để hiện/ẩn một block dựa trên một điều kiện logic, thường dùng phép tính logic and (&&) của Javascript.

Ví dụ:

      
        function App() {
          const [show, setShow] = React.useState(true);
          const message = "Hello world!";
        
          return (
            <div>
              {show && <div>{message}</div>}
              <button onClick={() => setShow(!show)}>Toggle message</button>
            </div>
          );
        }

        ReactDOM.render(<App />, document.getElementById('app'));
      
Chạy thử chương trình

Ở ví dụ trên, dòng điều khiển hiện/ẩn message là:

    {show && <div>{message}</div>}

Biểu thức này có giá trị bằng <div>{message}</div> khi show bằng true, và có giá trị bằng null (không hiển thị gì) khi show bằng false.


Hiển thị danh sách

Trong React, để hiển thị một danh sách các phần tử có cấu trúc giống nhau, thường sử dụng hàm map của Javascript.

Ví dụ:

        
        const productList = [
          {code: 'IPX', name: 'IPhone X', price: 10.5},
          {code: 'IP11', name: 'IPhone 11', price: 11.5},
          {code: 'IP12', name: 'IPhone 12', price: 12.5},
        ];

        function App() {
          return (
            <table border="1">
              <thead>
                <tr>
                  <th>Code</th>
                  <th>Name</th>
                  <th>Price</th>
                </tr>
              </thead>
              <tbody>
                {productList.map((product,index) =>
                  <tr key={index}>
                    <td>{product.code}</td>
                    <td>{product.name}</td>
                    <td>{product.price} M</td>
                  </tr>
                )}
              </tbody>
            </table>
          )
        }

        ReactDOM.render(<App />, document.getElementById('app'));
      
Chạy thử chương trình

Ở ví dụ trên, đoạn code sinh ra danh sách các sản phẩm trong bảng là:


      {productList.map((product,index) =>
        <tr key={index}>
          <td>{product.code}</td>
          <td>{product.name}</td>
          <td>{product.price} M</td>
        </tr>
      )}
    

Trong javascript, hàm map được dùng để biến đổi một danh sách thành một danh sách mới qua phép biến đổi thành phần (tham số của hàm map).

Ở đoạn code trên, hàm biến đổi thành phần biến mỗi bản ghi product thành một thẻ tr tương ứng với một dòng trong bảng dữ liệu:

    productList.map((product,index) => <tr key={index}>...</tr>)

Biến chỉ số index được dùng làm key cho phần tử tr. Việc sử dụng key trong một danh sách giúp React phát hiện xem phần tử nào thay đổi/được thêm mới/xoá để cập nhật hiển thị tương ứng.

Giá trị key cần duy nhất cho mỗi phần tử, do đó thường chọn là biến chỉ số index hoặc trường id của bản ghi.


III. Phát triển ứng dụng React với create-react-app

Việc nhúng Babel để dịch JSX trên trình duyệt chỉ để giúp người mới học React dễ tiếp cận.

Trên thực tế, các ứng dụng React đều được phát triển với công cụ create-react-app

Để sử dụng công cụ này, cần cài đặt Node.js

1. Tạo mới ứng dụng

Từ cửa sổ command (Windows cmd, Linux Shell, VsCode Terminal, ...) gõ lệnh:

npx create-react-app my-app

Ứng dụng React sẽ được tạo ra trong thư mục cùng tên với tên ứng dụng (my-app ở ví dụ trên)

Để chạy ứng dụng , gõ:

cd my-app
npm start

Cửa sổ trình duyệt sẽ tự động được mở ra và trỏ đến địa chỉ của server ứng dụng: http://localhost:3000. Nếu trình duyệt không tự động mở, cần mở trình duyệt có trên máy (Chrome/FireFox/Edge ...) và gõ trực tiếp địa chỉ trên vào thanh địa chỉ.


2. Phát triển ứng dụng

Để phát triển ứng dụng, sử dụng IDE để mở thư mục ứng dụng. IDE hay sử dụng để phát triển React là Visual Studio Code

Thư mục ứng dụng có nhiều file khác nhau, trong đó file để chứa ứng dụng gốc là App.js

Khi tạo mới project, create-react-app sinh ra một số mã html cho file này. Có thể xoá toàn bộ phần này và viết một app từ đầu như ở phần I.

Tạo khung ứng dụng

React không quy định cụ thể cấu trúc của ứng dụng, do đó có nhiều cách tạo khung ứng dụng ban đầu khác nhau. Phần này giới thiệu một khung đơn giản nhất. Trên thực tế sẽ có thêm các phần liên quan đến routerstore (sẽ giới thiệu ở phần sau)

Trong thư mục src của ứng dụng, tạo 2 thư mục với tên pagescomponents

Ứng dụng gốc trong file App.js sẽ import các trang trong phần pages và hiển thị trang tương ứng với địa chỉ người dùng truy nhập vào. Trên thực tế trong App.js sẽ có router để điều hướng truy nhập các trang thành phần. (Tham khảo phần IV bên dưới)

IV. Một số thư viện thường sử dụng kèm với React

Ứng dụng do create-react-app tạo ra thường chỉ có React và một số thư viện cơ bản. Các ứng dụng thực tế thường yêu cầu cài đặt thêm khá nhiều các thư viện khác. Để cài đặt một thư viện, mở cửa sổ command trong thư mục gốc ứng dụng và gõ lệnh:

npm install <tên_thư_viện>

Một số thư viện thường sử dụng:

Phần này giới thiệu 2 thư viện quan trọng là react-router-domreact-redux

react-router-dom

Đây là thư viện giúp cho việc điều hướng truy nhập các trang thành phần của ứng dụng.

Cài đặt thư viện:

npm install react-router-dom

Sau khi cài đặt xong, trong file package.json ở thư mục gốc của ứng dụng sẽ xuất hiện thư viện react-router-dom trong phần dependencies.

Cấu hình router:

Mở file index.js trong thư mục gốc ứng dụng và thêm router như ở hình dưới:

      
        import React from 'react';
        import ReactDOM from 'react-dom';
        import './index.css';
        import App from './App';
        import reportWebVitals from './reportWebVitals';
        import { BrowserRouter } from "react-router-dom";

        ReactDOM.render(
          <React.StrictMode>
            <BrowserRouter>
              <App />
            </BrowserRouter>
          </React.StrictMode>,
          document.getElementById('root')
        );

        // ...
        reportWebVitals();
      
    

Tạo thư mục pages trong thư mục gốc ứng dụng, sau đó tạo 2 file Page1.jsPage2.js trong thư mục Page

Nhập nội dung cho các file App.js, Page1.js, Page2.js như hình dưới đây:

      
        // File: App.js

        import Page1 from "./pages/Page1";
        import Page2 from "./pages/Page2";
        import { Link, Routes, Route } from "react-router-dom";
        
        export default function App() {
          return(
            <>
              <nav>
                <Link to="/">Page 1</Link> {" "}
                <Link to="/page-2">Page 2</Link>
              </nav>
              
              <Routes>
                <Route path="/" element={<Page1 />}/>
                <Route path="/page-2" element={<Page2 />}/>
              </Routes>
            </>
          )
        }
      
      
        // File: pages/Page1.js

        export default function Page1() {
          return (
            <div>
              <h1>Page 1</h1>
              Page 1 Content
            </div>
          )
        }
      

      
        // File: pages/Page2.js

        export default function Page2() {
          return (
            <div>
              <h1>Page 2</h1>
              Page 2 Content
            </div>
          )
        }
      
    

Khởi động ứng dụng (npm start), giao diện ứng dụng trên trình duyệt sẽ như sau:

Phía trên cùng của ứng dụng là navigator với các link để chuyển trang, phía dưới là nội dung trang được chọn.

Khi click vào các link trên navigator thì trang tương ứng sẽ được hiển thị. Ngoài ra, trên thanh địa chỉ trình duyệt, địa chỉ trang cũng thay đổi theo.

react-redux

Cài đặt thư viện:

npm install react-redux

Đây là thư viện giúp cho việc chia sẻ các biến trạng thái giữa các Component với nhau.

Quá trình trao đổi dữ liệu (biến trạng thái) giữa Component cha và Component con được thực hiện qua thuộc tính ( xem phần Component.). Việc trao đổi giữa liệu giữa các Component ở các cấp khác nhau không thể thực hiện theo cách này.

Để khắc phục vấn đề này, store giúp trao đổi dữ liệu giữa 2 component bất kỳ theo mô hình sau:

Khi có sự kiện ở Component 1, action được thực hiện (dispatch) để làm thay đổi trạng thái của store. Component 2 đăng ký theo dõi sự thay đổi này (subscribe) và cập nhật lại hiển thị khi trạng thái thay đổi.

Cấu hình store:

Tạo file store.js trong thư mục src của ứng dụng với nội dung như sau:


      import {createStore} from 'redux';

      const initialState = {
        // declare state here
      }
      
      const reducer = (state=initialState, action) => {
        //TODO: Process action
      
        return newState;
      }
      
      const store = createStore(reducer);
      
      export default store;
    

Phần initialState dùng để chứa giá trị ban đầu của các biến trạng thái (được chia sẻ cho tất cả các component trong ứng dụng).

Phần reducer dùng để chứa logic xử lý của các action làm thay đổi biến trạng thái. Sau khi xử lý xong, kết quả trả về của reducer sẽ được dùng làm trạng thái mới của store.

Trong file index.js bổ sung thêm phần cấu hình store như sau:

Sử dụng store:

Store có thể được sử dụng từ bất kỳ Component nào của ứng dụng.

Như trình bày ở trên, có 2 chiều sử dụng store:

Gửi action tới store:

Để gửi action đến store, sử dụng hook useDispatch của react-redux:

      
        import {useDispatch} from "react-redux"

        function Comp() {
          const dispatch = useDispatch();

          const eventHandler = () => {
            ...
            dispatch({
              type: ..., 
              payload: ...
            });
            ...
          }
          ...
        }
      
    

Bên trong hàm render của Component, hook useDispatch được gọi để lấy về đối tượng dispatch:

    const dispatch = useDispatch();

Bên trong các hàm xử lý sự kiện của Component, sử dụng đối tượng dispatch ở trên để gửi các action đến store:

    dispatch({
      type: ...,
      payload: ...
    });

Giá trị của type là tên hành động muốn gửi (mỗi hành động có logic xử lý riêng ở store), giá trị của payload là dữ liệu gửi đến action.

Ở phía store (store.js), reducer sẽ nhận được action kèm theo payload gửi từ component tới:


      //store.js
      ...
      
      const reducer = (state=initialState, action) => {
        console.log('type=', action.type);
        console.log('payload=', action.payload);
        //TODO: Process action
        return state;
      }
      
      ...
    
Đăng ký theo dõi thay đổi trạng thái của store:

Để theo dõi sự thay đổi trạng thái của store, sử dụng hook useSelector của react-redux:

      
        import {useSelector} from "react-redux"

        function Comp() {
          const value1 = useSelector(state => state.value1);
          const value2 = useSelector(state => state.value2);
          //const value3 = useSelector(state => state.value3); // do not use this, as 'value3' is not used in component's render
          ...
          return (
            <>
              State value 1: {value1}
              State value 2: {value2}
              ...
            </>
          )
        }
      
    

Tham số của useSelector là một function nhận giá trị vào là toàn bộ trạng thái của store, và trả về một/một số trường của trạng thái đó:

    const value1 = useSelector(state => state.value1);

Nếu component đăng ký theo dõi trường nào (value1, value2 ở ví dụ trên), thì khi những trường đó thay đổi, component sẽ được hiển thị lại (re-render). Do đó để tránh việc component bị hiển thị lại một cách không cần thiết, chỉ đăng ký theo dõi những trường được sử dụng trong phần render JSX của component .

Ví dụ: Counter app

Ứng dụng counter có một biến trạng thái (ban đầu bằng 0), khi người dùng click vào button tăng thì counter tăng thêm 1, khi người dùng click vào button giảm thì counter giảm đi 1.

Trong store.js, khai báo trạng thái ban đầu initialState gồm 1 trường counter với khởi đầu bằng 0, trong reducer, cung cấp logic để xử lý 2 action tăng và giảm counter:


      // store.js
      import {createStore} from 'redux';

      const initialState = {
        counter: 0
      }

      const reducer = (state=initialState, action) => {
        if(action.type === 'increase') {
          return {counter: state.counter + (action.payload ?? 1)};

        }else if(action.type === 'decrease') {
          return {counter: state.counter - (action.payload ?? 1)};
        }

        return state;
      }

      const store = createStore(reducer);

      export default store;
    

Trong file App.js, sử dụng các hook useDispatchuseSelector để gửi các action và nhận thay đổi trạng thái của counter:


      // App.js
      import { useSelector, useDispatch  } from "react-redux";

      function App() {
        const dispatch = useDispatch();
        const counter = useSelector(state => state.counter);

        const increase = () => {
          dispatch({
            type: 'increase',
            payload: 1
          });
        }

        const decrease = () => {
          dispatch({
            type: 'decrease',
            payload: 1
          });
        }

        return (
          <>
            Counter: {counter} 
            {" "}
            <button onClick={increase}>+</button>
            {" "}
            <button onClick={decrease}>-</button>
          </>
        );
      }

      export default App;
    

Khi chạy ứng dụng, kết quả sẽ như sau: