Trong bài trước chúng ta đã cùng nhau tạo ra một navigation đơn giản, trong bài này chúng ta sẽ cùng nhau sử dụng memento design pattern để tạo ra một navigation phức tạp hơn nhé.

Memento design pattern

Memento là một mẫu thiết kế cho phép chúng ta quản lý trạng thái, cụ thể ở đây là việc lưu trữ và khôi phục lại trạng thái khi cần thiết, các thành phần của nó bao gồm:

  1. Memento: Là lớp dùng để lưu trữ trạng thái.
  2. Originator: Là lớp dùng để tạo, lưu trữ và khôi phục các đối tượng memento.
  3. Caretaker: Là lớp sử dụng Originator để tạo ra một đối tượng memento mới, lưu trữ đối tượng memento hiện tại. Khi cần Caretaker cũng có thể gọi đến Originator để khôi phục lại đối tượng memento cũ.

Thiết kê sơ đồ lớp

Dựa vào sơ đồ lớp cơ sở của memento design pattern, chúng ta sẽ thiết kế sơ đồ lớp cho phần navigation của chúng ta như sau:

Ở đây chúng ta có:

  1. PageData: tương ứng với lớp Memento để lưu trữ trạng thái là uri của trang.
  2. NavigationStore: tương ứng với lớp Originator để quản lý các đối tượng PageData.
  3. App: tương ứng với Caretaker sử dụng uri có trong đối tượng PageData để điều hướng và và sử dụng NavigationStore để lưu trữ và khôi phục đối tượng PageData khi cần.

Cài đặt

Mã nguồn cài đặt của chúng ta sẽ có nhiều thay đổi.

Lớp PageData

Mã nguồn của lớp PageData sẽ như sau:

interface PageData {
  uri: string
}

Thực tế ở đây chúng ta đang định nghĩa một kiểu, trong đó có một trường uri của trang, ngoài ra trong các dự án phức tạp chúng ta có thể có nhiều trường dữ liệu hơn, ví dụ như trường data để lưu trữ dữ liệu của trang chẳng hạn.

Lớp NavigationStore

Mã nguồn của lớp NavigationStore sẽ như sau:

class NavigationStore {
  
  private stack = new Array<PageData>();

  public top(): PageData | undefined {
    return this.stack.length
      ? this.stack[this.stack.length - 1]
      : undefined;
  }
  
  public push (data: PageData) {
    const topData = this.top();
    if ((topData && topData.uri != data.uri) || !topData) {
      this.stack.push(data);
    }
  }

  public pop(): PageData | undefined {
    return this.stack.pop();
  }

  public reset() {
    this.stack.length = 0;
  }
};

Ở đây chúng ta đang coi một navigation giống như một ngăn xếp với các hàm:

  1. Top: Lấy ra một đối tượng PageData trên đỉnh ngăn xếp.
  2. Push: Lưu trữ một đối tượng PageData vào đỉnh của ngăn xếp.
  3. Pop: Lấy ra một đối tượng PageData trên đỉnh ngăn xếp đồng thời loại bỏ nó.
  4. Reset: Loại bỏ tất cả các đối tượng PageData khỏi ngăn xếp, đưa navigation trở về trạng thái ban đầu.

Hàm App

Mã nguồn của hàm App sẽ thay đổi như sau:

import { useEffect, useRef, useState } from 'react'
import './App.css'
import Entry from './Entry'
import Page1 from './Page1';
import Page2 from './Page2';

interface PageData {
  uri: string
}

class NavigationStore {
  
  private stack = new Array<PageData>();

  public top(): PageData | undefined {
    return this.stack.length
      ? this.stack[this.stack.length - 1]
      : undefined;
  }
  
  public push (data: PageData) {
    const topData = this.top();
    if ((topData && topData.uri != data.uri) || !topData) {
      this.stack.push(data);
    }
  }

  public pop(): PageData | undefined {
    return this.stack.pop();
  }

  public reset() {
    this.stack.length = 0;
  }
};

function App() {
  const [currentViewURI, setCurrentViewURI] = useState("/entry");
  const navigationStore = useRef(new NavigationStore());

  const doSetCurrentViewURI = (uri: string) => {
    setCurrentViewURI(uri);
    navigationStore.current.push({ uri: location.pathname });
    navigationStore.current.push({ uri });
  };

  const back = () => {
    navigationStore.current.pop();
    const data = navigationStore.current.top();
    if (data) {
      setCurrentViewURI(data.uri);
    }
    console.log(navigationStore.current);
  }

  useEffect(() => {
    window.onpopstate = () => {
      back();
    };
    return () => {
      window.onpopstate = null;
    };
  }, []);

  let view;
  switch(currentViewURI) {
    case "/entry":
      view = <Entry setCurrentViewURI={doSetCurrentViewURI} back={back} />;
      break;
    case "/page1":
      view = <Page1 setCurrentViewURI={doSetCurrentViewURI} back={back} />;
      break;
    case "/page2":
      view = <Page2 setCurrentViewURI={doSetCurrentViewURI} back={back} />;
      break;
    default:
      view = <Entry setCurrentViewURI={doSetCurrentViewURI} back={back} />;
      navigationStore.current.reset();
      break;
  };
  window.history.pushState('', '', currentViewURI);
  return view;
}

export default App

Trong mã nguồn này chúng ta:

  1. Khai báo một đối tượng navigationStore sử dụng useRef để tránh bị khởi tạo lãi mỗi lần thay đổi trạng thái.
  2. Khai báo hàm doSetCurrentViewURI để thay đổi uri hiện tại (chuyển trang) đồng thời đưa thông tin của cả trang hiện tại và trang mới vào navigationStore để lưu trữ thông qua hàm push.
  3. Khai báo hàm back để khôi phục lại trang trước mà người dung đã truy cập.
  4. Sử dụng useEffect để đăng ký nhận sự kiện người dùng nhấn vào nút back trên trình duyệt.
  5. Ở các trang chúng ta cũng sẽ bổ sung nút back để cho phép người dùng quay trở lại trang trước.

Các trang con

Mã nguồn của các trang con sẽ thay đổi như sau:

Trang Entry

import reactLogo from './assets/react.svg'
import { PageProps } from './common'
import viteLogo from '/vite.svg'

const Entry: React.FC<PageProps> = ({ setCurrentViewURI, back }) => {
  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCurrentViewURI("/page1")}>
          Move to Page 1
        </button>
        <button onClick={() => setCurrentViewURI("/page2")}>
          Move to Page 2
        </button>
        <button onClick={() => back()}>
          Back
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default Entry

Trang Page1

import { PageProps } from "./common"

const Page1: React.FC<PageProps> = ({ setCurrentViewURI, back }) => {
  return (
    <>
      <h1>Techmaster</h1>
      <div className="card">
        <p>Page 1</p>
        <button onClick={() => setCurrentViewURI("/entry")}>
          Back to Entry
        </button>
        <button onClick={() => setCurrentViewURI("/page2")}>
          Move to Page 2
        </button>
        <button onClick={() => back()}>
          Back
        </button>
      </div>
    </>
  )
}

export default Page1

Trang Page2

import { PageProps } from "./common"

const Page1: React.FC<PageProps> = ({ setCurrentViewURI, back }) => {
  return (
    <>
      <h1>Techmaster</h1>
      <div className="card">
        <p>Page 2</p>
        <button onClick={() => back()}>
          Back
        </button>
      </div>
    </>
  )
}

export default Page1

Khởi chạy chương trình

Khi chúng ta khởi chạy chương trình thông qua câu lệnh yarn dev và truy cập vào http://localhost:5173/ sau đó truy cập vào từng trang:

Sau đó chúng ta có thể nhấn vào nút back trên trình duyệt hoặc nút back trên giao diện chúng ta có thể di chuyển về trang trước đó.

Tổng kết

Như vậy chúng ta đã cùng nhau tạo ra một navigation tương đối phức tạp bằng cách sử dụng memento design pattern.


Cám ơn bạn đã quan tâm đến bài viết này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a