Component phần 2: Tách biệt giữa dữ liệu và giao diện

Video ở bài trước giải quyết được vấn đề mà đề bài đó đưa ra, và đó cũng là cách tự nhiên khi tiếp xúc querySelector và DOM properties. Giờ chúng ta sẽ thử đưa ra thêm nhiều yêu cầu.

Bài tập: tiếp tục từ đề bài ở bài viết trước đó:

  • Thêm bộ đếm hiển thị số đếm tổng số các task, và tổng số các task đã thực hiện xong.

  • Thêm tính năng lưu lại để khi reload trình duyệt thì không bị mất trắng. Gợi ý: sử dụng localStorage

Hãy cố gắng hoàn thành bài tập trước khi move sang phần tiếp theo dưới đây.

Phân tích sau bài tập:

  • Các yêu cầu mới khiến bạn phải có cách nhìn tổng quan về toàn bộ dữ liệu của mình.

  • Bạn có thể đang query DOM để tìm các thông tin tổng hợp. VD: đếm tất cả các task dùng document.querySelectorAll('.task-item').length và đếm tất cả các task đã xong dùng document.querySelectorAll('.task-item input:checked').length

  • Đều đó đúng, nhưng cách được khuyên dùng sẽ là tạo biến javascript lưu toàn bộ thông tin dữ liệu của web của bạn. Và ở mọi trường hợp dữ liệu ở đây phải luôn là mới nhất. DOM luôn dùng để hiển thị thông tin lên giao diện, không dùng DOM là 1 cách để lưu trữ giữ liệu.

  • Đó chính là nội dung của bài này, khi bạn lưu ra toàn bộ dữ liệu ra biến, thì bạn mới có thể dễ dàng phát triển thêm tính năng. VD như tính năng lưu dữ liệu bằng localStorage kể trên, bạn tạo 1 biến array dạng: const tasks =[{ task: "This is content of task", isCompleted: true }] và lưu vào bằng localStorage.setItem('tasks', JSON.stringify(tasks))

Hệ thống hóa cách phân tách giao diện và dữ liệu.

Dưới đây là 1 số các bước giúp bạn hệ thống hóa. (Tham khảo từ React)

  • tạo 1 biến state là một object để lưu nhiều thông tin khác nhau.

  • tạo 1 function render và function này sẽ có nhiệm vụ vẽ giao diện theo đúng các giá trị trong state.

  • tạo 1 function setState sẽ update state và đồng thời chạy render. Điểm mấu chốt ở đây sẽ là không update state trực tiếp VD state.abc = "def"; Làm như vậy sẽ khó để trigger update giao diện.

      const setState = (obj) => {
          if (state === obj) return;
          state = obj;
          render();
      };
    

VD: Làm 1 cái bộ đếm và khi click vào nút thì sẽ tăng 1 đơn vị.

let state = {
  count: 0,
};

const render = () => {
  document.body.innerHTML = `
    <div>
      <p>Count: ${state.count}</p>
      <button>click</button>
    </div>
  `;

  const buttonElement = document.querySelector('button');
  if (buttonElement) {
    buttonElement.onclick = function() {
      setState({
        count: state.count +1, 
      });
    };
  }
}

const setState = (obj) => {
  if (state === obj) return;
  state = obj;
  console.log('log state', state);
  render();
};

// render the first time
render();
  • function render sẽ được gọi đi gọi lại nhiều lần mỗi khi bạn gọi setState. Như vậy state luôn luôn có dữ liệu mới nhất (không phải lấy dữ liệu từ DOM ra). và giao diện sẽ luôn được cập nhật khi state thay đổi.

  • giao diện của bạn sẽ chủ yếu đc vẽ thông qua javascript. sử dụng template string giúp bạn chèn biến vào string dễ dàng.

  • trong render cần phải có khai báo listener. tránh dùng addEventListner vì sẽ tạo thừa listener, hãy dùng như cách dùng ở VD trên. (set onclick = function(){} )

  • việc phân chia tách biệt giữa staterender thường sẽ khiến bạn phải viết nhiều code hơn bình thường. nhưng nó đáp ứng tốt hơn rất nhiều cho các yêu cầu phức tạp.

Cập nhật, trang bị thêm 1 số kiến thức Javascript

Nếu bạn là người mới, thì cách viết Javascript bên trên có thể sẽ khiến bạn bỡ ngỡ. Các tài liệu bạn đọc có thể chỉ nhắc đến var, function() {}. Đó là phiên bản Javascript cũ ES5. Hiện nay, hầu như tất cả các browser hiện đại đã được nâng cấp lên để có thể sử dụng ES6.

Một số tính năng và cách dùng từ khi có ES6:

Dùng let và const thay cho var: let để khai báo biến có thể thay đổi giá trị, const dùng cho biến không đổi giá trị. Lưu ý: nếu biến là object thì việc dùng const vẫn có thể thay đổi giá trị biên trong. VD : const a = { b: 1}; a.b = 2; console.log(a);

letconst chỉ tồn tại ở bên trong ngoặc. Do đó tránh việc nhầm lẫn không đáng có.

Dùng arrow function: Thay vì viết function abc() {} thì bạn có thể viết const abc = () => {}; và nếu function đó trả về giá trị luôn thì có thể không cần dùng ngoặc.

VD: const sum = (a, b) => a + b;

Bạn có thể chuyển sang dùng toàn bộ arrow function, cách viết gọn hơn.

Dùng intermediate anomyous function: Kết hợp giữa arrow func và let const, thì việc dùng intermediate anomyous function giúp code của bạn dễ đọc hơn và tránh lỗi.

VD: cho 2 array, mỗi array sẽ bao gồm các phần tử là số, hoặc là object chứa key value mang giá trị số. Hãy tìm số có giá trị lớn nhất xuất hiện trong cả 2 array.

const arr1 = [1,2,3,4,5, { value: 10 }];
const arr2 = [10,9,8, { value: 6 },7,6];
const highestValue = (() => {
  let highestValue1 = arr1[0]; 
  let highestValue2 = arr2[0];
  arr1.forEach((v) => {
    if (typeof v === 'number') highestValue1 = Math.max(v, highestValue1);
    else if (typeof v === 'object' && typeof v.value === 'number') {
      highestValue1 = Math.max(v.value, highestValue1);
    } 
  });
  arr2.forEach((v) => {
    if (typeof v === 'number') highestValue2 = Math.max(v, highestValue2);
    else if (typeof v === 'object' && typeof v.value === 'number') {
      highestValue2 = Math.max(v.value, highestValue2);
    } 
  });
  return Math.max(highestValue1, highestValue2);
})();

Cách làm trên dùng const highestValue = (() => { ......code.... })(); đó chính là intermediate anomyous function. Nôm na là khai báo function không tên, xong sau đó cho nó chạy ngay lập tức. Cách viết này biểu thị ý đồ ngay tại bước khai báo const highestValue = là tìm giá trị lớn nhất, và các biến phụ tạo ra để chạy cho mục đích đó sẽ không bị "leak" ra ngoài, không sợ bị khai báo trùng biến.

Dùng spearding operator: tham khảo bài viết này. dùng cái này sẽ thấy việc viết code, gọi biến tiện hơn nhiều.

Viết lại bài ở VD trên:

const arr1 = [1,2,3,4,5, { value: 10 }];
const arr2 = [10,9,8, { value: 6 },7,6];
const highestValue = (() => {
  const join2Arrays = [
     ...arr1,
     ...arr2,
  ];
  let highestValue = join2Arrays[0];
  join2Arrays.forEach((v) => {
    if (typeof v === 'number') highestValue = Math.max(v, highestValue);
    else if (typeof v === 'object' && typeof v.value === 'number') {
      highestValue = Math.max(v.value, highestValue);
    } 
  });
  return highestValue;
})();

Loop qua array bằng map và forEach: Đây không phải là kiến thức mới, mà đã có từ ES5. Tuy nhiên nó rất quan trọng và bạn nên ghi nhớ để sử dụng nhiều.

Viết lại bài ở VD trên:

const arr1 = [1,2,3,4,5, { value: 10 }];
const arr2 = [10,9,8, { value: 6 },7,6];
const highestValue = (() => {
  const joinAndSortArray = [
     ...arr1,
     ...arr2,
  ]
  .map(v => typeof v === 'number' ? v : (v.value || 0))
  .sort((a, b) => a > b ? -1 : 1)
  return joinAndSortArray[0];
})();

Đóng gói state, setState và render

Vận dụng các kiến thức bên trên, chúng ta sẽ "đóng gói" lại để tái sử dụng.

Vì sao phải đóng gói?: Mình đã đề cập là cái state và setState này lấy idea từ React. do đó đóng gói lại cho nó "nhìn giống React" sẽ giúp các bạn khi chuyển sang React sẽ thấy quen. Ngoài ra, đóng gói sẽ giúp bạn tránh việc phải viết đi viết lại function setState.

class StateRender {
   constructor(initialState) {
    this.state = initialState;
   }
   state = {};
   render;
   setState(obj) {
     if (this.state === obj) return;
     this.state = obj;
     if (!this.render) return;
     this.render(this.state);
   }

}

const useState = (initialState) => {
    const stateRender = new StateRender(initialState);
    const render = (fn) => {
        stateRender.render = fn;
    };
    const getState = () => stateRender.state;
    return [getState, stateRender.setState, render];
}

Vì sẽ dùng đi dùng lại nên chúng ta có thể tách phần này vào 1 file js riêng để chèn <script /> vào dùng ở nhiều trang.

Áp dụng vào bài toán bộ đếm bên trên thì sẽ trở thành:

const [getState, setState, render] = useState({
  count: 0,
});
render((state) => {
  document.body.innerHTML = `
    <div>
      <p>Count: ${state.count}</p>
      <button>click</button>
    </div>
  `;

  const buttonElement = document.querySelector('button');
  if (buttonElement) {
    buttonElement.onclick = function() {
      setState({
        count: state.count +1, 
      });
    };
  }
});

Bài tập: Viết lại toàn bộ bài tập ở bài trước + các yêu cầu mới ở đầu bài viết này theo hướng dùng useState.