Ở bài viết giới thiệu React Hook, chúng ta đã hiểu phần nào Hooks là gì. Hooks có rất nhiều công dụng mạnh mẽ. Hôm nay, chúng ta sẽ khám phá cách quản lý state với React Hooks, tự tạo một custom Hooks để quản lý global state.
Với cách làm này, việc quản lý state sẽ đơn giản hơn rất nhiều so với dùng Redux, hay Context API.
Nội dung chính của bài viết
State là gì?
State được nhắc đến rất nhiều trong các ứng dụng React. Nhiều khi làm cho người mới học cảm thấy bối rối, khó hiểu.
Nhưng nếu nhìn thấu bản chất, bạn chỉ cần hiểu đơn giản như sau: State là lưu trữ các giá trị của component. Mỗi khi state thay đổi, component cũng được render lại để phù hợp với giá trị state mới.
Để theo dõi bài viết này dễ hơn, mời bạn đọc lại hai Hooks sẽ sử dụng trong bài này:
Sau khi hiểu rõ 2 Hooks trên, chúng ta bắt đầu vào phần chính bài viết này nhé.
Ý tưởng chia sẻ states
Chúng ta có thể thấy rằng, Hook state hoạt động giống hệt với state trong class componennt. Mỗi instance của component đều có state riêng của nó. Để chia sẻ state giữa component, chúng ta sẽ sử dụng giải pháp tạo một Custom Hook.
Ý tưởng là tạo mảng các listeners và một state object. Mỗi khi component nào đó thay đổi state, tất cả các component đã đăng ký sẽ gọi hàm setState()
để cập nhật theo.
Công việc này được thực hiện bằng cách gọi useState()
bên trong custom Hook của chúng ta. Tuy nhiên, thay vì trả về hàm setState()
, chúng ta thêm nó vào mảng các listeners, sau đó trả về một hàm để cập nhật state object và chạy tất cả các listeners functions.
Tạo Custom Hook
Dựa vào ý tưởng trên, chúng ta thực hiện implement để tạo một custom hook.
import { useState, useEffect } from 'react'; let listeners = []; let state = { counter: 0 }; const setState = (newState) => { state = { ...state, ...newState }; listeners.forEach((listener) => { listener(state); }); }; const useCustom = () => { const newListener = useState()[1]; useEffect(() => { listeners.push(newListener); }, []); return [state, setState]; }; export default useCustom;
Sau khi định nghĩa xong custom Hook, giờ thì sử dụng bằng cách gọi trong component. Đây là một ví dụ:
import React from 'react'; import useCustom from './customHook'; const Counter = () => { const [globalState, setGlobalState] = useCustom(); const add1Global = () => { const newCounterValue = globalState.counter + 1; setGlobalState({ counter: newCounterValue }); }; return ( <div> <p> counter: {globalState.counter} </p> <button type="button" onClick={add1Global}> +1 to global </button> </div> ); }; export default Counter;
Ở đoạn code trên, chúng ta có thể thêm bao nhiều Counter component vào ứng dụng cũng được. Tất cả sẽ có cùng chung giá trị state.
Tuy nhiên, nếu để ý kỹ thì cách thực hiện trên có một số hạn chế. Dưới đây là một số điểm mình muốn cải thiện:
- Nên xóa listener tương ứng khi component đã unmounted.
- Tổng quát hơn để có thể tái sử dụng ở nhiều dự án khác nhau.
- Có thể khởi tạo giá trị state thông qua các param.
Để giải quyết các vấn đề trên, chúng ta sẽ cùng nhau cải tiến mã nguồn custom hook nhé.
Xóa listener khi component unmounted
Như chúng ta đã biết, nếu chúng ta gọi useEffect
như sau:
useEffect(() => { // Bạn viết code xử lý logic tại đây }, []);
Với tham số thứ 2 là một mảng rỗng, cách viết này sẽ xử lý tương tự componentDidMount()
– được gọi một lần khi component được mounted.
Nhưng nếu function được sử dụng trong tham số đầu tiên mà return về một hàm khác. Thì hàm được return sẽ được kích hoạt ngay trước khi component bị unmounted – tương đương với callback componentWillUnmount()
useEffect(() => { return () => { // hàm được trả về sẽ được gọi khi component unmount // Bạn viết code xử lý logic tại đây khi component unmount. } }, [])
Ok, giờ thì chúng ta cải tiến một chút hàm useCustom()
để xóa listeners khi component unmounted.
const useCustom = () => { const newListener = useState()[1]; useEffect(() => { // Called just after component mount listeners.push(newListener); return () => { // Called just before the component unmount listeners = listeners.filter(listener => listener !== newListener); }; }, []); return [state, setState]; };
Vậy là xong một vấn đề, chúng ta tiếp tục cải tiến thêm nhé.
- Đặt React làm tham số, để không cần phải import nữa.
- Không cần export customHook nữa. Thay vào đó, export một functions mà return về một customHook theo thông số
initialState
. - Tạo một store object để lưu các giá trị state
- Sử dụng arrow function
setState()
vàuseCustom()
để chúng ta có thể bind giữastore
vàthis
.
Đây là kết quả:
function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { // Called just after component mount this.listeners.push(newListener); return () => { // Called just before the component unmount this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.setState]; } const useGlobalHook = (React, initialState) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); return useCustom.bind(store, React); }; export default useGlobalHook;
Bởi vì Hook mà chúng ta tạo đã tổng quát hơn lúc trước nên cần thiết lập nó trong store file, mục đích là để dễ đọc hơn.
import React from 'react'; import useGlobalHook from './useGlobalHook'; const initialState = { counter: 0 }; const useGlobal = useGlobalHook(React, initialState); export default useGlobal;
Tách action ra khỏi component
Nếu bạn đã từng làm việc với các thư viện quản lý state phức tạp, người ta không khuyến khích thao tác trực tiếp global state từ component.
Cách tốt nhất là tách business logic ra khỏi component bằng cách tạo các action để tương tác với global state.
Vì vậy, cải tiến cuối cùng cho customHook
của chúng ta là không cho phép component truy cập vào hàm setState()
, chỉ có thể thêm action vào thôi.
Để làm được điều đó, chúng ta sẽ truyền action vào hàm useGlobalHook()
. Có một vài điểm lưu ý:
- Action sẽ truy xuất vào store object. Do đó, các actions có thể đọc state thông qua
store.state
, ghi state thông quastore.setState()
. Thậm chí là gọi các actions khác thông quastate.actions
. - Về tổ chức mã nguồn, actions object có thể chứa action con.
Kết quả cuối cùng thu được sẽ như sau:
function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { this.listeners.push(newListener); return () => { this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.actions]; } function associateActions(store, actions) { const associatedActions = {}; Object.keys(actions).forEach((key) => { if (typeof actions[key] === 'function') { associatedActions[key] = actions[key].bind(null, store); } if (typeof actions[key] === 'object') { associatedActions[key] = associateActions(store, actions[key]); } }); return associatedActions; } const useGlobalHook = (React, initialState, actions) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); store.actions = associateActions(store, actions); return useCustom.bind(store, React); }; export default useGlobalHook;
Để bạn có thể dễ dàng sử dụng customHook
cho các dự án sau này, toàn bộ mã nguồn được đóng gói thành package và publish nên npm repository với tên là use-global-hook. Các bạn có thể sử dụng bằng cách cài đặt qua câu lệnh: npm i use-global-hook
Một vài ví dụ sử dụng global Hook để quản lý state
Sau khi đã đóng gói đoạn code trên thành package (tạm gọi là global Hook), về sau bạn chỉ cần cài đặt và gọi ra sử dụng như bất kỳ thư viện khác.
Ví dụ 1: Nhiều bộ đếm, một giá trị
Có rất nhiều Counter nhưng đều sử dụng chung một giá trị thời gian. Khi một Counter thêm một giá trị, tất cả Counter sẽ được render lại.
Ví dụ 2: Ajax request
Ví dụ này đơn giản là bạn tạo một ứng dụng, nó sẽ request tới github để tìm kiếm các repository theo tên người dùng.
Các request sẽ được xử lý theo kiểu bất đồng bộ dùng Async/Await api.
Tạm kết
Như vậy, ngoài sử dụng thư viện Redux cồng kềnh hay dùng Context API còn hạn chế. Giải pháp sử dụng Hooks không hề tồi chút nào.
Theo thông tin “vỉa hè”, nhà phát hành React đang tập trung rất nhiều để thúc đẩy Hook. Tương lai, Hook sẽ là xu hướng viết code mới.
Bạn thấy sao về giải pháp quản lý state với React Hooks này? Để lại bình luận bên dưới cho mình và mọi người biết nhé.
Nguồn: https://medium.com/javascript-in-plain-english/state-management-with-react-hooks-no-redux-or-context-api-8b3035ceecf8
Bình luận. Cùng nhau thảo luận nhé!