=
=

Bài 3: Quản lý dữ liệu ứng dụng web với React

I. Props, Ref và State

Thuộc tính của component

Các component trong React có các thuộc tính tương tự các thuộc tính của các phần tử HTML. Cú pháp:

<Tên_Component Thuộc_tính_1 = giá_trị_1 Thuộc_tính_2 = giá_trị_2 ... />

Ví dụ: Các thuộc tính brand và model của component Car

<Car brand="Ford" model=" Mustang" />

Để sử dụng các thuộc tính, component sử dụng các đối tượng props và state.

Đối tượng props

Các thuộc tính của component có thể được kết xuất đến người dùng thông qua đối tượng props theo cú pháp

this.props.Thuộc_tính.

Ví dụ component Car có hai thuộc tính là brand và model và để render giá trị các thuộc tính này chúng ta dùng đối tượng props như sau:

class Car extends React.Component {
  render() {
    return
      <h2>I am a {this.props.brand} and {this.props.model}!</h2>;
  }
}
const myComponent = <Car brand="Ford" model="Mustang" />;
ReactDOM.render(myComponent, document.getElementById('root'));

Chú ý: Hàm ReactDOM không còn được hỗ trợ trong phiên bản React 18 trở lên. Để chạy mã trên, bạn cần cập nhật phiên bản React đến phiên bản thấp hơn (ví dụ 17.x.x). Trong CodeSandbox, tìm đến mục Dependencies và tìm đến react và react-dom để chọn lại phiên bản.

Có thể thấy props tương tự các đối số trong hàm. Nhờ props, dữ liệu có thể được chuyển đổi qua lại giữa các component. Ví dụ gửi dữ liệu các thuộc tính brand và model từ Garage đến Car:

class Car extends React.Component {
  render() {
    return <h2>I am a {this.props.brand} and {this.props.model}!</h2>;
  }
}
class Garage extends React.Component {
  render() {
    return (
      <div>
        <h1>Who lives in my garage?</h1>
        <Car brand="Ford" model="Mustang" />
      </div>
    );
  }
}
ReactDOM.render(   <Garage />, document.getElementById('root'));

Nếu dữ liệu thuộc tính một component có cấu trúc phức tạp thay vì một chuỗi đơn giản như Ford hay Mustang chúng ta có thể dùng biến. Ví dụ khai báo biến carinfo kiểu object chứa nhiều thuộc tính:

class Car extends React.Component {
  render() {
    return
      <h2>I am a {this.props.info.brand} and {this.props.info.model}!</h2>;
  }
}
const carinfo = { brand: "Ford", model: "Mustang" };
const myComponent =
    <Car info = {carinfo} />;
ReactDOM.render(myComponent, document.getElementById('root'));

Nếu component có chứa hàm constructor thì props nên được chuyển đến constructor và React.Component thông qua hàm super():

class Car extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <h2>I am a {this.props.info.brand} and {this.props.info.model}!</h2>;
  }
}

Lý do cho việc này sẽ được giải thích trong đối tượng state. Tham khảo tại stackoverflow.com.

Đối tượng state

Đối tượng state là nơi chúng ta lưu trữ các giá trị thuộc tính của component. Đối tượng state luôn được khởi tạo trong hàm constructor của component. Ví dụ đối tượng state lưu trữ thuộc tính brand:

class Car extends React.Component {
  constructor(props) {
     super(props);
     this.state = {brand: "Ford"};
  }
  render() {
     return <h2>I am a Car.</h2>;
  }
}

Hay lưu trữ dữ liệu phức tạp hơn (tức nhiều thuộc tính):

class Car extends React.Component {
  constructor(props) {
     super(props);
     this.state = {
        brand: "Ford",
        model: "Mustang",
     };
  }
  render() {
     return <h2>I am a Car.</h2>;
  }
}

Sử dụng state theo cú pháp this.state.Thuộc_tính:

class Car extends React.Component {
  constructor(props) {
     super(props);
     this.state = {
        brand: "Ford",
        model: "Mustang",
     };
  }
  render() {
     return (
        <div>
           <h2>I am a Car.</h2>
           <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
        </div>
     );
  }
}
const myComponent = <Car />;
ReactDOM.render(myComponent, document.getElementById('root'));

Lưu ý rằng, với state khi component được sử dụng sẽ không cần chuyển tên thuộc tính như khi sử dụng đối tượng props. Một điều khác phân biệt props và state đó là props chỉ đọc (read only) trong khi state có thể thay đổi bằng cách dùng hàm setState()

class Car extends React.Component {
  constructor(props) {
     super(props);
     this.state = {
        brand: "Ford",
        model: "Mustang"
    };
  }
  changeCar = () => {
     this.setState({ brand: "Toyota", model: "Camry" });
  }
  render() {
    return (
       <div>
          <h2>I am a Car.</h2>
          <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
          <button onClick={this.changeCar}>
             Change Car
          </button>
       </div>
    );
  }
}
const myComponent = <Car />;
ReactDOM.render(myComponent, document.getElementById('root'));
Chuyển state lên trên (Lifting state up)

Thông thường, khi một dữ liệu thay đổi nó sẽ ảnh hưởng tới nhiều component cùng lúc. State được khuyến khích chia sẻ ở component cha của chúng.

Xem giải thích và ví dụ minh họa tại https://vi.reactjs.org/docs/lifting-state-up.html

So sánh props và state

Cuối cùng, hãy tóm tắt lại và xem sự khác biệt chính giữa props và state:

Tính năng ref

Tham chiếu, hoặc ref, là một tính năng cho phép các thành phần React tương tác với các phần tử con. Trường hợp sử dụng phổ biến nhất cho các refs là tương tác với các phần tử UI có đọc đầu vào từ người dùng. Hãy xem xét phần tử form HTML. Những phần tử này được kết xuất đến trình duyệt nhưng người dùng có thể tương tác với chúng và các form cần phản hồi một cách thích hợp.

Hãy xem xét component FancyButton hiển thị phần tử DOM của button gốc:

function FancyButton(props) {
  return (
    <button className="FancyButton">
    {props.children}
    </button>
  );
}

Các React components ẩn chi tiết triển khai của chúng, bao gồm cả đầu ra được hiển thị của chúng. Các components khác sử dụng FancyButton thường sẽ không cần lấy tham chiếu đến phần tử DOM của button bên trong. Điều này là tốt vì nó ngăn các components dựa vào cấu trúc DOM của nhau quá nhiều.

Trong đoạn mã dưới đây, FancyButton sử dụng React.forwardRef để lấy ref được chuyển đến nó, sau đó chuyển tiếp nó đến DOM button mà nó hiển thị:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));
// Bây giờ có thể nhận được ref trực tiếp đến nút DOM:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

Bằng cách này, các components sử dụng FancyButton có thể nhận được tham chiếu đến DOM button bên dưới và truy cập nó nếu cần - giống như nếu chúng sử dụng trực tiếp DOM button.

Dưới đây là giải thích từng bước về những gì xảy ra trong ví dụ trên:

Tham khảo thêm về ref tại reactjs.org.

II. Chu kỳ sống của các thành phần React (React Lifecycle)

Mỗi component trong React có một vòng đời (lifecycle) mà chúng ta có thể theo dõi và xử lý. Vòng đời này gồm 3 giai đoạn:

Giai đoạn Mounting

Trong giai đoạn này, React có 4 phương thức cơ bản là constructor(), getDerivedStateFromProps(), render() và componentDidMount(). Trong đó phương thức render() phải luôn luôn được gọi và các phương thức khác là tùy chọn. Trong các bài trước chúng ta đã làm quen với render() và constructor():

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {brand: "Ford"};
  }
  render() {
    return
      <h2>I am a Car.</h2>;
  }
}

Trong component, phương thức constructor được gọi đầu tiên và phương thức super() cũng phải được gọi đầu tiên trong constructor. Đối tượng state cũng phải được khởi tạo trong constructor và đối tượng props cũng là tham số của constructor và super.

Phương thức render() dùng để kết xuất các component đến trình duyệt hay chính xác hơn là kết xuất các phần tử HTML đến cây DOM.

Phương thức getDerivedStateFromProps() được gọi trước khi các phần tử được kết xuất đến cây DOM và là nơi khởi tạo đối tượng state dựa trên đối tượng props. Nó nhận state như đối số và sẽ trả về một đối tượng với sự thay đổi đối tượng state. Ví dụ khởi tạo các thuộc tính brand và model từ state đến các thuộc tính newbrand và newmodel của props.

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang",
    };
  }
  static getDerivedStateFromProps(props, state) {
    return { brand: props.newbrand, model: props.newmodel };
  }
  render() {
    return (
      <div>
        <h2>I am a Car.</h2>
        <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
      </div>
    );
  }
}
const myComponent = <Car newbrand="Toyota" newmodel="Camry" />;
ReactDOM.render(myComponent, document.getElementById('root'));

Ngược với getDerivedStateFromProps, phương thức componentDidMount() được gọi sau khi component được kết xuất đến DOM. Điều này cũng có nghĩa phương thức này là nơi dùng các lệnh yêu cầu component khi nó đã được đặt trên DOM. Ví dụ Car sẽ được kết xuất và thay đổi các thuộc tính sau 3 giây:

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang",
    };
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({ brand: "Toyota", model: "Camry" });
        }, 3000);
  }
  render() {
    return (
      <div>
        <h2>I am a Car.</h2>
        <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
      </div>
    );
  }
}
const myComponent = <Car />;
ReactDOM.render(myComponent, document.getElementById('root'));
Giai đoạn Updating

Khi component được kết xuất đến DOM có thể sẽ có sự thay đổi liên quan đến state hay props và đây là giai đoạn updating. Trong giai đoạn này có các phương thức getDerivedStateFromProps(), shouldComponentUpdate(), render(), getSnapshotBeforeUpdate() và componentDidUpdate(). Trong đó, render() là phương thức được gọi bắt buộc, các phương thức khác tùy chọn.

Khi xuất hiện cập nhật thông tin đến component, phương thức getDerivedStateFromProps() luôn được ưu tiên số 1 (nếu chúng ta định nghĩa). Xét trở lại ví dụ

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang",
    };
  }
  changeCar = () => {
    this.setState({ brand: "Toyota", model: "Camry" });
  }
  render() {
    return (
      <div >
        <h2 >I am a Car. </h2 >
        <p >My brand is {this.state.brand} and my model is {this.state.model} </p >
        <button onClick={this.changeCar} >
        Change Car
        </button >
      </div >
    );
  }
}
const myComponent = <Car newbrand="Honda" newmodel="Civic" / >
ReactDOM.render(myComponent, document.getElementById('root'));

Kết quả:

Nếu nhấn nút Change Car

Hai thuộc tính newbrand và newmodel không được liên quan gì vì chúng thuộc quyền quản lý của props. Bây giờ thêm phương thức getDerivedStateFromProps() đến component Car:

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang",
    };
    }
  static getDerivedStateFromProps(props, state) {
    return {brand: props.newbrand, model:props.newmodel};
  }
  changeCar = () => {
    this.setState({ brand: "Toyota", model: "Camry" });
  }
  render() {
    return (
      <div>
        <h2>I am a Car.</h2>
        <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
        <button onClick={this.changeCar}>
          Change Car
        </button>
      </div>
    );
  }
}
const myComponent = <Car newbrand="Honda" newmodel="Civic" />; ReactDOM.render(myComponent, document.getElementById('root'));

Kết quả:

Dù chúng ta nhấn nút Change Car cũng không thay đổi gì. Phương thức khác trong giai đoạn Updating là shouldComponentUpdate() quyết định liệu component có tiếp tục cập nhật hay không. Giá trị trả về của phương thức này là true (tiếp tục cập nhật) hay false (ngừng cập nhật). Xét lại ví dụ component Car:

class Car extends React.Component {   constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang",
    };
  }
  changeCar = () => {
      this.setState({ brand: "Toyota", model: "Camry" });
    }
  render() {
    return (
      <div>
        <h2>I am a Car.</h2>
        <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
        <button onClick={this.changeCar}>
        Change Car
        </button>
      </div>
    );
  }
}
const myComponent = <Car />;
ReactDOM.render(myComponent, document.getElementById('root'));

Thuộc tính của Car sẽ thay đổi khi nhấn nút Change Car. Bây giờ chúng ta thêm phương thức shouldComponentUpdate()

constructor(props) {
  super(props);
  this.state = {
    brand: "Ford",
    model: "Mustang",
  };
}
shouldComponentUpdate() {
  return false;
}
changeCar = () => {
  this.setState({ brand: "Toyota", model: "Camry" });
}

Vì trả về false nên Car sẽ không thay đổi khi nhấn nút Change Car. Tuy nhiên, nếu chúng ta thay đổi giá trị trả về thành true thì mọi thứ sẽ trở lại bình thường (nghĩa là được cập nhật). Tại giai đoạn cập nhật chúng ta có thể thay đổi thông tin về component và chúng ta cũng muốn biết thông tin trước và sau khi thay đổi component. Phương thức getSnapshotBeforeUpdate() sẽ cung cấp các thông tin trước khi cập nhật và phương thức componentDidUpdate() sẽ cung cấp thông tin sau khi cập nhật. Xét một ví dụ diễn ra quá trình cập nhật component Car và các thông tin trước và sau cập nhật được trả về. Xét ví dụ

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang",
    };
  }
  getSnapshotBeforeUpdate(prevProps, prevState) {
    document.getElementById("div1").innerHTML = "Before the update, the brand was " + prevState.brand + " and model was " + prevState.model;
  }
  componentDidUpdate() {
    document.getElementById("div2").innerHTML = "The updated car is " + this.state.brand + " brand and " + this.state.model + " model";
  }
  changeCar = () => {
    this.setState({ brand: "Toyota", model: "Camry" });
  }
  render() {
    return (
      <div>
        <h2>I am a Car.</h2>
        <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
        <button onClick={this.changeCar}>
        Change Car
        </button>
        <div id="div1"></div>
        <div id="div2"></div>
      </div>
    );
  }
}
const myComponent = <Car />;
ReactDOM.render(myComponent, document.getElementById('root'));

Thực thi

Nhấn Change Car

Giai đoạn Unmounting

Đây là giai đoạn khi một component được xóa khỏi DOM. Tại giai đoạn này chỉ có một phương thức là componentWillUnmount() được gọi. Tham khảo ví dụ tại https://www.w3schools.com/react/showreact.asp?filename=demo2_react_lifecycle_componentwillunmount

III. Xử lý sự kiện (Events Handling)

Giống như HTML, các component React cũng phản ứng đến các hành động người dùng thông qua các sự kiện (events). Chúng ta đã dùng sự kiện onClick cho button và trong bài này chúng ta sẽ tìm hiểu chi tiết hơn.

Sự kiện (events) và trình xử lý sự kiện (event handlers)

Sự kiện là các hành động tương tác của người dùng (user) đến giao diện ứng dụng như nhấn chuột trái đến button, nhập thông tin,...

Ứng dụng sẽ đưa ra phản ứng tương ứng với các hành động của người dùng gọi là xử lý sự kiện. Hiểu một cách đơn giản, một trình xử lý sự kiện là hàm hay phương thức thực hiện chức năng nào đó tương ứng với một sự kiện nào đó của người dùng.

Trong HTML, sự kiện nhấn chuột trái của button được viết như sau:

<button onclick="changeCar()">Change Car!</button>

Với onclick phản ánh sự kiện nhấn chuột trái và changeCar() là hàm (hay phương thức) thực thi khi người dùng nhấn chuột trái đến button. Hàm changeCar() còn được gọi là trình xử lý sự kiện onclick. Trong HTML các sự kiện được viết thường như onclick, onchange, onmouseover,...

Các sự kiện trong React được viết theo kiểu camelCase tức là onClick, onChange, onMouseOver,…(xem danh sách chi tiết tại https://reactjs.org/docs/events.html). Xem lại ví dụ:

class Car extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      brand: "Ford",
      model: "Mustang"
    };
  }
  changeCar = () => {
    this.setState({ brand: "Toyota", model: "Camry" });
  }
  render() {
    return (
      <div>
        <h2>I am a Car.</h2>
        <p>My brand is {this.state.brand} and my model is {this.state.model}</p>
        <button onClick={this.changeCar}>
        Change Car
        </button>
      </div>
    );
  }
}

Có một số khác biệt so với HTML chúng ta cần lưu ý:

Từ khóa this và hàm mũi tên

Từ khóa this trong React chỉ component chứa hàm sử dụng. Ví dụ this trong ví dụ trên chỉ component Car chứa hàm changeCar. Đây cũng chính là lý do chúng ta phải dùng hàm mũi tên để định nghĩa changeCar thay vì định nghĩa hàm kiểu thông thường. Với hàm mũi tên, từ khóa this luôn thể hiện đối tượng định nghĩa hàm đó. Để hiểu hơn, giả sử chúng ta định nghĩa changeCar theo kiểu thông thường như sau:

changeCar() {
  this.setState({ brand: "Toyota", model: "Camry" });
}

Kết quả sẽ xuất hiện lỗi vì React không xác định được this chỉ đối tượng nào:

Để khắc phục lỗi này mà không dùng hàm mũi tên chúng ta dùng hàm bind() để kết nối hàm với đối tượng định nghĩa nó. Ví dụ chúng ta dùng hàm bind() trong hàm constructor của component Car như sau:

constructor(props) {
  super(props);
  this.state = {
    brand: "Ford",
    model: "Mustang"
  };
  this.changeCar = this.changeCar.bind(this)
}

Với JS hiện đại chúng ta nên làm quen với hàm mũi tên vì những tiện ích nó mang lại. Để hiểu hơn về hàm mũi tên trong JS bạn tham khảo tại https://ngocminhtran.com/2020/02/26/javascript-hien-dai-phan-1/ Trong trường hợp hàm chứa các tham số ví dụ changeCar có thể định nghĩa như sau:

changeCar = (b, m) => {
  this.setState({ brand: b, model: m });
}

Khi đó, tại sự kiện onClick chúng ta viết:

<button onClick={() => this.changeCar("Toyota", "Camry")}>
</button>

React cũng có thể cho chúng ta biết chính xác đối tượng sự kiện chúng ta đang làm việc. Có thể tham khảo tại W3Schools.

IV. Một số vấn đề khác

Kết xuất theo điều kiện

Trong React để kết xuất các component theo điều kiện, chúng ta thực hiện bằng nhiều cách khác nhau.

Lệnh if

Có thể dùng lệnh if trong javaScript để truy xuất các component. Ví dụ chúng ta có 2 component sau:

function Fail() {
  return <h1>BẠN ĐÃ RỚT RỒI!</h1>;
}

function Pass() {
  return <h1>CHÚC MỪNG BẠN ĐÃ ĐẬU!</h1>;
}

Chúng ta tạo ra component Exam để kết xuất các component Fail hay Pass dùng lệnh if như sau:

function Exam(props) {
  const isExam = props.isExam;
  if (isExam) {
    return <Pass />;
  }
  return <Fail />;
}

Kiểm tra kết xuất Exam đến DOM

ReactDOM.render(
  <Exam isExam={false} />, document.getElementById("root"));

Kết quả

Kiểm tra kết xuất Exam đến DOM

ReactDOM.render(
  <Exam isExam={true} />, document.getElementById("root"));

Kết quả

Toán tử ? :

Trong JS, toán tử ba ngôi ?: là một biến thể của lệnh if else. Các component cũng có thể được kết xuất dùng toán tử ba ngôi ? : Ví dụ component Exam có thể được viết lại như sau:

function Exam(props) {
  const isExam = props.isExam;
  return (
    <>
      {isExam ? <Pass /> : <Fail />}
    </>
  );
}

Lưu ý rằng biểu thức dùng toán tử ba ngôi ?: là toán tử JS nên được đặt trong cặp ngoặc móc {}.

Toán tử &&

Các component cũng có thể được kết xuất theo điều kiện trong React dùng toán tử &&. Ví dụ

function Garage(props) {
  const cars = props.cars;
  return (
    <>
      <h1>Sưu tập xe</h1>
      {cars.length > 0 &&
        <h2>
          Bạn có {cars.length} xe trong bộ sưu tập.
        </h2>
      }
</>
  );
}
const cars = ['Ford', 'BMW', 'Audi'];
ReactDOM.render(
  <Garage cars={cars} />, document.getElementById("root"));

Lưu ý rằng biểu thức dùng toán tử && là toán tử JS nên được đặt trong cặp ngoặc móc {}. Trong ví dụ trên, nếu cars.length hay số phần tử mảng cars lớn hơn 0 thì sẽ kết xuất <h2> đến DOM. Kết quả:

Bây giờ, kiểm tra với mảng cars rỗng

const cars = [];
ReactDOM.render(
  <Garage cars={cars} />, document.getElementById("root"));

Kết quả

Kết xuất danh sách (List)

Trong React để kết xuất một danh sách chúng ta ưu tiên dùng phương thức map(). Phương thức map() cho phép chúng ta chạy một hàm trên mỗi giá trị trong hàm và trả về một mảng mới. Xem xét ví dụ sau:

function Car(props) {
  return <li>{props.brand}</li>;
}

function Garage() {
  const cars = ['Ford', 'BMW', 'Audi'];
  return (
     <>
        <h1>Bộ sưu tập xe của tôi gồm:</h1>
        <ul>
              {cars.map((car) => <Car brand={car} />)}
        </ul>
     </>
  );
}
ReactDOM.render(<Garage />, document.getElementById("root"));

Kết quả:

Các mục (brand) trong danh sách trên không thể được kiểm soát bởi React khi chúng thay đổi, thêm hay xóa. Để giám sát các mục trong danh sách, React dùng key.

Key

Các key giúp React xác định những mục nào đã thay đổi, được thêm vào hoặc bị loại bỏ. Các key phải được cấp cho các phần tử bên trong mảng để cung cấp cho các phần tử một danh tính ổn định. Thông thường chúng ta dùng id là key.

Ví dụ mảng cars từ ví dụ trên:

const cars = [
  { id: 1, brand: 'Ford' },
  { id: 2, brand: 'BMW' },
  { id: 3, brand: 'Audi' }
];

Các key chỉ có ý nghĩa trong ngữ cảnh xung quanh mảng. Xét ví dụ về các component Car và Garage, key được sử dụng trong Garage vì có khai báo mảng thay vì dùng key trong Car. Car và Garage sẽ được viết lại như sau:

function Car(props) {
  return <li>{props.brand}</li>;
}

function Garage() {
  const cars = [
    { id: 1, brand: 'Ford' },
    { id: 2, brand: 'BMW' },
    { id: 3, brand: 'Audi' }
  ];
  return (
    <>
      <h1>Bộ sưu tập xe của tôi gồm:</h1>
      <ul>
        {cars.map((car) => <Car key={car.id} brand={car.brand}
/>)}
      </ul>
    </>
  );
}

Các key được sử dụng trong các mảng phải là duy nhất giữa các anh chị em của chúng. Tuy nhiên, chúng không cần phải là duy nhất với phạm vi toàn cục. Chúng ta có thể sử dụng các key giống nhau khi tạo ra hai mảng khác nhau. Ví dụ sau hai mảng anh em sidebar và content dùng chung một mảng posts toàn cục nên có key giống nhau:

function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
      <li key={post.id}>
        {post.title}
      </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  { id: 1, title: 'Hello World', content: 'Welcome to learning React!' },
  { id: 2, title: 'Installation', content: 'You can install React from npm.' }
];

ReactDOM.render(
  <Blog posts={posts} />, document.getElementById("root"));

Kết quả

Làm việc với biểu mẫu (Form)

Giống như HTML, React sử dụng các biểu mãu cho phép người dùng tương tác với trang web. Ví dụ sau đây tạo một form đơn giản cho phép người dùng nhập tên

import React from 'react';
import ReactDOM from 'react-dom';
function MyForm() {
  return (
    <form>
      <label>Enter your name:
      <input type="text" />
      </label>
    </form>
  )
}

ReactDOM.render(
  <MyForm />, document.getElementById('root'));

Chú ý, ví dụ trên sử dụng React Function thay vì React Class như các bài trên. Thực thi

Nếu nhập thông tin bất kỳ và nhấn Enter, kết quả trang sẽ được tải lại và xuất hiện dấu ? trên URL

Bình thường, biểu mẫu sẽ được gửi và trang sẽ làm mới. Nhưng điều này nói chung không phải là những gì chúng ta muốn xảy ra trong React. Chúng ta muốn ngăn chặn hành vi mặc định này và để React kiểm soát biểu mẫu.

Xử lý biểu mẫu

Xử lý biểu mẫu là về cách bạn xử lý dữ liệu khi nó thay đổi giá trị hoặc được gửi. Trong HTML, dữ liệu biểu mẫu thường được xử lý bởi DOM và trong React, dữ liệu biểu mẫu thường được xử lý bởi các component. Khi dữ liệu được xử lý bởi các component, tất cả dữ liệu được lưu trữ ở trạng thái component. Chúng ta kiểm soát các thay đổi bằng cách thêm trình xử lý sự kiện trong thuộc tính onChange, và chúng ta có thể sử dụng useState Hook để theo dõi từng giá trị đầu vào. useState chỉ được sử dụng trong React Function (nên các ví dụ trong bài này chỉ dùng React Function) và sẽ được đề cập chi tiết hơn trong phần React Hook. Ví dụ về sử dụng onChange và useState trong biểu mẫu:

import { useState } from "react";
import ReactDOM from 'react-dom';
function MyForm() {
  const [name, setName] = useState("");

  return (
    <form>
      <label>Enter your name:
        <input type="text" value={name} onChange={(e)=> setName(e.target.value)}/>
      </label>
    </form>
  )
}

ReactDOM.render(
  <MyForm />, document.getElementById('root'));

Chúng ta cũng có thể kiểm soát các hoạt động gửi dữ liệu từ biểu mẫu (submit action) bằng cách thêm trình xử lý sự kiện trong thuộc tính onSubmit cho <form>. Ví dụ minh họa:

import { useState } from "react";
import ReactDOM from 'react-dom';
function MyForm() {
  const [name, setName] = useState("");
  const handleSubmit = (event) => {
    event.preventDefault();
    alert(`The name you entered was: ${name}`);
  }
  return (
    <form onSubmit={handleSubmit}>
      <label>Enter your name:
        <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
      </label>
      <input type="submit" />
    </form>
  )
}

ReactDOM.render(
  <MyForm />, document.getElementById('root'));

Kết quả thực thi:

Biểu mẫu nhiều trường thông tin

Chúng ta có thể kiểm soát các giá trị của nhiều trường đầu vào bằng cách thêm thuộc tính name cho mỗi phần tử. Kế tiếp, sẽ khởi tạo trạng thái với một đối tượng rỗng. Để truy cập các trường trong trình xử lý sự kiện, chúng ta sử dụng cú pháp event.target.name và event.target.value. Để cập nhật trạng thái, hãy sử dụng dấu ngoặc vuông [ký hiệu dấu ngoặc vuông] xung quanh tên thuộc tính.

Ví dụ sau đây tạo một biểu mẫu 2 trường username và age:

import { useState } from "react";
import ReactDOM from "react-dom";
function MyForm() {
  const [inputs, setInputs] = useState({});

  const handleChange = (event) => {
    const name = event.target.name;
    const value = event.target.value;
    setInputs(values => ({ ...values, [name]: value }))
  }

  const handleSubmit = (event) => {
    event.preventDefault();
    alert(inputs);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>Enter your name:
        <input type="text" name="username" value={inputs.username || "" } onChange={handleChange} />       </label>
      <label>Enter your age:
        <input type="number" name="age" value={inputs.age || "" } onChange={handleChange} />
      </label>
      <input type="submit" />
    </form>
  )
}

ReactDOM.render(
  <MyForm />, document.getElementById('root'));
Sử dụng Textarea

Textarea trong HTML sử dụng phần tử <textarea>, ví dụ:

<textarea>
  Content of the textarea.
</textarea>

Textarea trong React có một ít khác biệt. Trong React, textarea được đặt trong một thuộc tính giá trị. Chúng ta sẽ dùng useState Hook để quản lý giá trị của textarea như ví dụ sau:

import { useState } from "react";
import ReactDOM from "react-dom";
function MyForm() {
  const [textarea, setTextarea] = useState("The content of a textarea goes in the value attribute");

  const handleChange = (event) => {
    setTextarea(event.target.value)
  }

  return (
    <form>
      <textarea value={textarea} onChange={handleChange} />
    </form>
  )
}

ReactDOM.render(
  <MyForm />, document.getElementById('root'));
Select

Tương tự textarea, select cũng được sử dụng với một ít khác biệt so với HTML như ví dụ sau:

import { useState } from "react";
import ReactDOM from "react-dom";
function MyForm() {
  const [myCar, setMyCar] = useState("Volvo");

  const handleChange = (event) => {
    setMyCar(event.target.value)
  }

  return (
    <form>
      <select value={myCar} onChange={handleChange}>
        <option value="Ford">Ford</option>
        <option value="Volvo">Volvo</option>
        <option value="Fiat">Fiat</option>
      </select>
    </form>
  )
}

ReactDOM.render(
  <MyForm />, document.getElementById('root'));

Thực thi

React Router

Khi bắt đầu, hầu hết các trang web bao gồm một loạt các trang mà người dùng có thể điều hướng (navigating) bằng cách yêu cầu và mở các tập tin riêng biệt. Vị trí của tập tin hoặc tài nguyên hiện tại đã được liệt kê trong thanh vị trí (loaction bar) của trình duyệt. Các nút chuyển tiếp (forward) và quay lại (back) của trình duyệt sẽ hoạt động như mong đợi. Đánh dấu (bookmark) nội dung một website sẽ cho phép người dùng lưu tham chiếu đến một tập tin cụ thể có thể được tải lại theo yêu cầu của người dùng. Website dựa trên trang hoặc do máy chủ hiển thị, tính năng điều hướng và các tính năng lịch sử (history) của trình duyệt sẽ hoạt động như mong đợi.

Tuy nhiên, trong ứng dụng một trang (single-page app), tất cả các tính năng này đều trở nên có vấn đề. Hãy nhớ rằng, trong một ứng dụng một trang, mọi thứ đều diễn ra trên cùng một trang. JavaScript tải thông tin và thay đổi giao diện người dùng. Các tính năng như lịch sử trình duyệt, đánh dấu trang, nút chuyển tiếp và nút quay lại sẽ không hoạt động nếu không có giải pháp định tuyến (routing solution). Định tuyến (Routing) là quá trình xác định các điểm cuối (endpoints) cho các yêu cầu của khách hàng của bạn. Các điểm cuối này hoạt động cùng với các đối tượng lịch sử và vị trí của trình duyệt. Chúng được sử dụng để xác định nội dung được yêu cầu để JavaScript có thể tải và hiển thị giao diện người dùng thích hợp. Không giống như Angular, Ember hoặc Backbone, React không đi kèm với một bộ định tuyến tiêu chuẩn. Nhận thức được tầm quan trọng của giải pháp định tuyến, các kỹ sư Michael Jackson và Ryan Florence đã tạo ra một cái tên đơn giản là React Router. React Router đã được thông qua bởi cộng đồng như một giải pháp định tuyến phổ biến cho các ứng dụng React. Nó được sử dụng bởi các công ty bao gồm Uber, Zendesk, PayPal, Vimeo,...

Cài React Router

Để cài đặt React Router phiên bản mới nhất, chúng ta gõ lệnh sau từ thư mục gốc của ứng dụng

npm i -D react-router-dom@latest

Trong CodeSandbox tại mục Dependencies gõ react-router-dom trong ô Add Dependency và enter.

Ứng dụng sử dụng React Router

Để tạo một ứng dụng định tuyến các trang chúng ta bắt đầu bằng cấu trúc các tập tin. Trong thư mục src tạo thư mục con tên pages chứa các tập tin như sau:

Mỗi tập tin sẽ chứa một component đơn giản. Nội dung các tập tin như sau:

Layout.js

import { Outlet, Link } from "react-router-dom";

const Layout = () => {
  return (
    <>
      <nav>
        <uv>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/blogs">Blogs</Link>
          </li>
          <li>
            <Link to="/contact">Contact</Link>
          </li>
        </uv>
      </nav>

<Outlet />
</>
)
};
export default Layout;

Thành phần Layout có các phần tử <Outlet> và <Link>

Bất cứ khi nào chúng ta liên kết đến một đường dẫn nội bộ, chúng ta sẽ sử dụng <Link> thay vì <a href="">.

Layout là một component được chia sẻ để chèn nội dung chung trên tất cả các trang, chẳng hạn như menu điều hướng.

Home.js

const Home = () => {
  return <h1>Home</h1>;
};
export default Home;

Blogs.js

const Blogs = () => {
return <h1>Blog Articles</h1>;
};

export default Blogs;

Contact.js

const Contact = () => {
  return <h1>Contact Me</h1>;
};
export default Contact;

NoPage.js

const NoPage = () => {
  return <h1>404</h1>;
};

export default NoPage;

Và tập tin index.js của chúng ta sẽ có nội dung sau

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./pages/Layout";
import Home from "./pages/Home";
import Blogs from "./pages/Blogs";
import Contact from "./pages/Contact";
import NoPage from "./pages/NoPage";

export default function RouterApp() {
  return (
    <BrowserRouter>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home />} />
            <Route path="blogs" element={<Blogs />} />
            <Route path="contact" element={<Contact />} />
            <Route path="*" element={<NoPage />} />
          </Route>
        </Routes>
    </BrowserRouter>
  );
}
ReactDOM.render(
  <RouterApp />, document.getElementById("root"));

Trước tiên, chúng ta đặt toàn bộ nội dung vào trong phần tử <BrowserRouter>. Sau đó, chúng ta định nghĩa <Routes> là các tuyến chúng ta dùng để điều hướng. Một ứng dụng có thể có nhiều <Routes>. Ví dụ này chúng ta chỉ sử dụng một. Trong mỗi phần tử <Routes> sẽ chứa nhiều phần tử <Route>.

Các <Route> có thể được lồng vào nhau. <Route> đầu tiên có một đường dẫn / và hiển thị thành phần Layout. Đây là phần tử <Route> cha hay tuyến cha.

Các <Route> lồng nhau kế thừa và thêm vào <Route> cha. Vì vậy, đường dẫn blogs (thuộc tính path) được kết hợp với cha và trở thành / blogs.

Tuyến thành phần Home không có đường dẫn nhưng có thuộc tính index. Điều này có ý nghĩa rằng đây là tuyến mặc định cho tuyến cha, tức là chỉ cần dấu /.

Thiết lập thuộc tính path đến * sẽ hoạt động như một phương thức truy cập cho mọi URL không xác định. Điều này là tuyệt vời cho một trang lỗi 404.

Thuộc tính element của <Route> chỉ component cần định tuyến đến.

Thực thi ứng dụng

Nhấn liên kết Blogs

Nhấn liên kết Contact

Sử dụng CSS trong React

Sử dụng CSS trong React có nhiều cách nhưng có 3 cách phổ biến sau:

Chèn định nghĩa CSS trực tiếp (Inline Styling)

Để chèn trực tiếp định nghĩa CSS dùng thuộc tính style trong React chúng ta phải dùng đối tượng JavaScript. Xét ví dụ sau (Mã hoàn chỉnh tại đây):

<h1 style={{color: "red" }}>Hello Style!</h1>

Chú ý thuộc tính style của h1:

Các thuộc tính trong CSS với hai từ trở lên cách nhau bởi dấu gạch ngang, ví dụ background-color, khi viết lại dưới dạng đối tượng JS phải tuân theo quy tắc (camel case syntax):

Ví dụ background-color viết lại thành backgroundColor (Mã hoàn chỉnh tại đây):

<h1 style={{backgroundColor: "lightblue" }}>Hello Style!</h1>

Nếu đối tượng có nhiều thông tin, chúng ta có thể tạo riêng đối tượng này như myStyle sau (Mã hoàn chỉnh tại đây):

const myStyle = {
  color: "white",
  backgroundColor: "DodgerBlue",
  padding: "10px",
  fontFamily: "Sans-Serif"
};
...
<h1 style={myStyle}>Hello Style!</h1>
Tạo một tập tin có phần mở rộng là .css (CSS Stylesheets)

Khai báo CSS và lưu trong một tập tin có phần mở rộng là .css, ví dụ App.css, sau đó liên kết đến ứng dụng React dùng lệnh import. Ví dụ liên kết tập tin App.css trong tập tin index.js và hai tập tin này được lưu trong cùng thư mục:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './App.css';
Tạo mô-đun CSS (CSS Modules)

Nếu ứng dụng React sử dụng các component được định nghĩa trong nhiều tập tin khác nhau, một giải pháp dùng CSS là dùng các mô-đun CSS. Chi tiết về mô-đun CSS tham khảo tại đây.

React Hook

JavaScript là một trong số hiếm các ngôn ngữ lập trình có thể được tiếp cận theo các mẫu hình khác nhau. Nổi bật nhất là mẫu hình lập trình chức năng hay hàm (Functinal Programming) và mẫu hình lập trình hướng đối tượng (Object Oriented Programming).

Trong React, các component có thể được tạo theo kiểu hàm và kiểu Class. Với kiểu hàm (chức năng), chúng ta không có quyền truy cập vào trạng thái và các tính năng khác của React và React Hook ra đời giúp xử lý vấn đề này. React Hook đã được thêm vào React trong phiên bản 16.8. React Hook cho phép các thành phần chức năng (function components) có quyền truy cập vào trạng thái và các tính năng khác của React. Do đó, các thành phần lớp (class components) nói chung không còn cần thiết nữa.

React Hook là gì?

Hook cho phép chúng ta "móc nối" vào các tính năng của React như các phương thức trạng thái và vòng đời.

Xét ví dụ sau đây:

Chúng ta phải import Hook từ react. Ở đây chúng ta đang sử dụng useState Hook để theo dõi trạng thái ứng dụng. useState đã được giới thiệu và sẽ được đề cập chi tiết hơn phần bên dưới bài này. Trạng thái (state) thường đề cập đến dữ liệu ứng dụng hoặc thuộc tính cần được theo dõi.

Quy tắc Hook

Có 3 quy tắc cho hook:

Tài liệu này sẽ tìm hiểu các React Hook cơ bản là useState, useEffect, useContext và useRef. Các Hook khác chúng ta có thể tham khảo thêm tại https://react.dev/reference/react

useState

React useState Hook cho phép chúng ta theo dõi trạng thái (state) trong một thành phần hàm (function component). Trạng thái thường đề cập đến dữ liệu (data) hoặc thuộc tính (properties) cần được theo dõi trong một ứng dụng. Sử dụng useState

Để sử dụng useState Hook, chúng ta phải import nó từ react:

import { useState } from "react";
Khởi tạo useState

Chúng ta khởi tạo trạng thái bằng cách gọi useState trong thành phần hàm của chúng ta. useState chấp nhận một trạng thái ban đầu và trả về hai giá trị:

Ví dụ:

import { useState } from "react";
function FavoriteColor() {
   const [color, setColor] = useState("");
}

Một vài lưu ý từ ví dụ trên:

Ở đây chúng ta đang sử dụng kỹ thuật giải cấu trúc (destructuring) các giá trị trả về từ useState. Tham khảo thêm về React ES6 Destructuring tại https://www.w3schools.com/REACT/react_es6_destructuring.asp

Đọc trạng thái

Bây giờ chúng ta có thể gộp trạng thái ở bất kỳ đâu trong thành phần của chúng ta.

Ví dụ sử dụng biến trạng thái trong thành phần được kết xuất như sau:

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

Chúng ta sử dụng hàm để cập nhật trạng thái của chúng ta. Chúng ta không bao giờ nên cập nhật trực tiếp trạng thái. Ví dụ: color = "red" là không được phép.

Ví dụ sử dụng một nút để cập nhật trạng thái:

Trạng thái có thể theo dõi những gì

UseState Hook có thể được sử dụng để theo dõi các chuỗi, số, boolean, mảng, đối tượng và bất kỳ sự kết hợp nào của chúng. Chúng ta có thể tạo nhiều Hooks trạng thái để theo dõi các giá trị riêng lẻ.

Ví dụ tạo nhiều Hooks trạng thái:

Hoặc, chúng ta có thể chỉ sử dụng một trạng thái và thay vào đó bao gồm một đối tượng như ví dụ tạo một Hook duy nhất chứa một đối tượng:

Vì bây giờ chúng ta đang theo dõi một đối tượng duy nhất, chúng ta cần tham chiếu đối tượng đó và sau đó là thuộc tính của đối tượng đó khi hiển thị thành phần. (Ví dụ: car.brand).

Cập nhật các đối tượng và mảng ở trạng thái

Khi trạng thái được cập nhật, toàn bộ trạng thái sẽ bị ghi đè. Điều gì sẽ xảy ra nếu chúng ta chỉ muốn cập nhật màu sắc (color) của chiếc xe của mình?

Nếu chúng ta chỉ gọi setCar ({color: "blue"}), điều này sẽ xóa brand, model và year khỏi trạng thái của chúng ta. Chúng ta có thể sử dụng toán tử lan truyền (spread operator) (toán tử có kí hiệu ba chấm) JavaScript để giải quyết vấn đề này. (Tham khảo thêm toán tử lan truyền tại https://ngocminhtran.com/2020/02/26/javascript-hien-dai-phan-1/ )

Ví dụ sử dụng toán tử lan truyền JavaScript để chỉ cập nhật màu của chiếc xe:

Bởi vì chúng ta cần giá trị hiện tại của trạng thái, chúng ta truyền một hàm vào hàm setCar của chúng ta. Hàm này nhận giá trị trước đó. Sau đó, chúng ta trả về một đối tượng, áp dụng toán tử lan truyền đến previousState và chỉ ghi đè lên màu.

useEffect

UseEffect Hook cho phép bạn thực hiện các hiệu ứng phụ trong các thành phần của mình. Một số ví dụ về hiệu ứng phụ như tìm nạp dữ liệu (fetching data), cập nhật trực tiếp DOM hay bộ tính giờ (timer).

Cú pháp sử dụng:

useEffect (đối số 1, đối số 2)

useEffect chấp nhận hai đối số. Đối số thứ hai là tùy chọn.

Hãy sử dụng bộ đếm thời gian làm ví dụ. Sử dụng setTimeout () để đếm 1 giây sau lần kết xuất ban đầu:

Lưu ý từ đoạn mã trên:

Đây không phải là những gì chúng ta mong đợi. Có một cách để kiểm soát hiệu ứng phụ đó là chúng ta phải luôn sử dụng tham số thứ hai chấp nhận một mảng trong useEffect. Chúng ta có thể tùy ý chuyển các ràng buộc đến useEffect trong mảng này.

Nếu không ràng buộc được chuyển đến:

useEffect(() => {
  //chạy trên mọi kết xuất
});

Nếu một mảng rỗng được chuyển đến:

useEffect(() => {
  //Chỉ chạy cho lần kết xuất đầu tiên
}, []);

Các giá trị prop hay state được chuyển đến:

useEffect(() => {
  //Chạy trên lần kết xuất đầu tiên
  // Và bất kỳ khi nào bất kỳ giá trị phụ thuộc nào thay đổi
}, [prop, state]);

Vì vậy, để khắc phục sự cố này, hãy chỉ chạy hiệu ứng này trên kết xuất ban đầu. Ví dụ:

Đây là một ví dụ về useEffect Hook phụ thuộc vào một biến. Nếu biến đếm count cập nhật, hiệu ứng sẽ chạy lại:

Xóa hiệu ứng (Effect Cleanup)

Một số hiệu ứng phải được hủy để giảm rò rỉ bộ nhớ. Hết thời gian chờ, đăng ký, trình nghe sự kiện và các hiệu ứng khác không còn cần thiết nên được xử lý. Chúng ta thực hiện điều này bằng cách bao gồm một hàm trả về ở cuối useEffect Hook.

Ví dụ: Hủy bộ hẹn giờ khi kết thúc sử dụng

useContext

useContext là một cách để quản lý trạng thái mức toàn cục. Nó có thể được sử dụng cùng với useState Hook để chia sẻ trạng thái giữa các thành phần lồng vào nhau dễ dàng hơn so với useState một mình.

Vấn đề

Trạng thái phải được giữ bởi thành phần cha cao nhất trong ngăn xếp yêu cầu quyền truy cập vào trạng thái. Để minh họa, chúng ta có nhiều thành phần lồng nhau. Thành phần ở trên cùng và dưới cùng của ngăn xếp cần có quyền truy cập vào trạng thái.

Để thực hiện điều này mà không có Context, chúng ta sẽ cần chuyển trạng thái dưới dạng các thuộc tính qua mỗi thành phần lồng nhau.

Xét một ví dụ với 5 component và giả sử chúng ta muốn component1 và component5 truy cập trạng thái (user). Để thực hiện điều này nếu không có context, trạng thái phải được chuyển lần lượt qua các component mặc dùng các component2 đến component4 không cần truy cập trạng thái:

Giải pháp dùng Context

Để xử lý vấn đề trên chúng ta dùng Context theo các bước sau:

Bước 1: Tạo Context bằng cách import createContext và khởi tạo:

import { useState, createContext } from "react";
const UserContext = createContext()

Bước 2: Gôm các component con trong Context Provider và cung cấp giá trị trạng thái:

Bước 3: Dùng Context trong một componnet con bằng cách dùng useContext. Để dùng useContext chúng ta phải import

import { useState, createContext, useContext } from "react";

sau đó truy cập

Như vậy lúc này chúng ta cần chuyển trạng thái qua các component2 đến component4:

useRef

useRef Hook cho phép chúng ta duy trì các giá trị giữa các lần kết xuất. Nó có thể được sử dụng để lưu trữ một giá trị có thể thay đổi mà không gây ra kết xuất lại khi cập nhật.

Xét ví dụ: Nếu chúng ta cố gắng đếm số lần ứng dụng của chúng ta kết xuất bằng useState Hook, chúng ta sẽ bị mắc vào một vòng lặp vô hạn vì chính Hook này gây ra kết xuất lại. Để tránh điều này, chúng ta có thể sử dụng useRef Hook như sau:

useRef () trả về một Đối tượng được gọi là current. Khi chúng ta khởi tạo useRef, chúng ta đặt giá trị ban đầu: useRef (0).

Như tìm hiểu ở ở mục 1 về tính năng ref, trong React, chúng ta có thể thêm thuộc tính ref vào một phần tử để truy cập nó trực tiếp trong DOM. Nói chung, chúng ta muốn để React xử lý tất cả các thao tác DOM, nhưng có một số trường hợp useRef có thể được sử dụng mà không gây ra sự cố.

Xét ví dụ dùng sự kiện focus trong input

Một vai trò khác của UseRef Hook là nó có thể được sử dụng để theo dõi các giá trị trạng thái trước đó. Điều này là do chúng ta có thể duy trì các giá trị useRef giữa các lần hiển thị. Xét ví dụ

Lần này chúng ta sử dụng kết hợp useState, useEffect và useRef để theo dõi trạng thái trước đó. Trong useEffect, chúng ta đang cập nhật giá trị hiện tại useRef mỗi khi inputValue được cập nhật bằng cách nhập văn bản vào trường input.