Quản lý state trong React không đơn giản chỉ dùng useState hay userReducer,... Quản lý state là một trong những khía cạnh quan trọng trong một ứng dụng React, đây có thể được coi là một bài toán phức tạp và khó khăn. Chúng ta có thể tìm kiếm cụm từ “state” trên npm và kết quả tìm kiếm cho chúng ta hàng nghìn package có sẵn để hỗ trợ cho việc này như Redux, React Context, Recoil,.... Mỗi package được tạo ra để giải quyết một vấn đề cụ thể của các nhà phát triển. State có nhiều loại, với mỗi loại chúng ta cần quản lý như thế nào cho hợp lý, cùng mình tìm hiểu trong bài hôm nay nhé.
State
State là một đối tượng React tích hợp được sử dụng để chứa dữ liệu hoặc thông tin về component, bao gồm các kiểu dữ liệu như string, number, boolean, array, function, object,… State trong một component có thể thay đổi và bất cứ khi nào nó thay đổi component sẽ render lại.
Ta có thể chia thành 4 loại state trong React mà bạn cần quản lý trong ứng dụng của mình: local state, global state, server state, URL state. Vậy đâu là điểm khác biệt giữa các loại state này, mình cùng nhau đi qua từng loại state.
Local state(UI state)
Là những data được lưu trữ để chúng ta quản lý trạng thái của một hoặc một vài component và không được lưu xuống server. Và những dữ liệu này sẽ mất khi chúng ta reload page.
Để dễ hiểu hơn về locale state, chúng ta hãy quan sát màn hình sau:
Trong hình trên, chúng ta có một side menu được điều khiển bật tắt bởi một nút bên cạnh(được tô đỏ trong hình). Khi chúng ta nhấn vào nút này thì side menu này sẽ bật lên, và khi chúng ta click bên ngoài thì side menu này sẽ biến mất. Việc quản lý bật và tắt side menu này được do một state isOpenModal quyết định chẳng hạn, và chúng ta cũng không cần lưu trữ chúng tại server. Khi reload lại page thì dữ liệu biến này sẽ trở về mặc định ban đầu. Đó chính là local state.
Global state:
Là state chung hoặc toàn cục mà có thể được truy cập và chia sẻ qua nhiều components khác nhau trong ứng dụng của bạn (chứ không phải một component đâu nhé). Global state cần khi chúng ta muốn nhận và cập nhật trạng thái của state ở bất kỳ đâu trong ứng dụng của mình. Nó thường được sử dụng để lưu trữ các thông tin quan trọng và dữ liệu chung mà cần được sử dụng hoặc cập nhật từ nhiều thành phần khác nhau.
Một ví dụ đơn giản về global state là thông tin của người dùng khi đăng nhập(chẳng hạn như tên, địa chỉ, email,...). Trong trường hợp này, global state cho phép bạn lưu trữ và quản lý thông tin quan trọng mà cần được truy cập và cập nhật từ nhiều thành phần khác nhau trong ứng dụng React của bạn.
Server state:
Là những dữ liệu được lấy từ server và được hiển thị trên giao diện người dùng. Gần như mọi ứng dụng React đều tìm nạp dữ liệu từ máy chủ thông qua lệnh gọi HTTP tới dịch vụ web. Server state là một khái niệm đơn giản, nhưng có thể khó quản lý cùng với tất cả trạng thái giao diện người dùng.
Một số đặc điểm nổi bật của server state là:
- Nó nằm ở server và bạn không thể điều khiển trực tiếp được nó.
- Cần một
async request
để lấy và cập nhật - Nó có thể bị thay đổi bởi một ai đó mà bạn không biết
- Nó bị cũ (stale) hay lỗi thời (outdated) trong quá trình sử dụng app
Một số ví dụ về server state trong ứng dụng React bao gồm:
- Danh sách sản phẩm: Thông tin về các sản phẩm, bao gồm tên sản phẩm, giá, mô tả và hình ảnh, thường được truy xuất từ máy chủ để hiển thị trên trang web mua sắm.
- Dữ liệu bài viết: Dữ liệu bài viết trên một trang web blog hoặc diễn đàn thường được lấy từ máy chủ để hiển thị cho người dùng.
URL state
URL state trong React là trạng thái ứng dụng được lưu trữ trong URL của trình duyệt. Nó thường được sử dụng để theo dõi và chia sẻ trạng thái của ứng dụng thông qua URL, cho phép người dùng sao chép, chia sẻ hoặc lưu trữ một liên kết cụ thể để truy cập trạng thái ứng dụng cụ thể.
Ví dụ phổ biến nhất về URL state là khi bạn thấy các tham số trên URL, ví dụ:
Trong URL này: https://example.com/products?category=electronics&sort=price
category=electronics
vàsort=price
là các tham số được sử dụng để định rõ trạng thái của trang web.category
vàsort
là các khóa (keys).electronics
vàprice
là các giá trị (values) tương ứng với các khóa.
Quản lí state trong React như thế nào cho hợp lý?
State tồn tại ở khắp các component lớn nhỏ trong chương trình của mình, nên tất nhiên chúng ta cũng cần quản lý nó. Quản lý trạng thái state là một kỹ năng quan trọng trong React, vì nó được sử dụng cho mọi thứ, từ theo dõi các dữ liệu nhập liệu của form đến các dữ liệu được lấy về từ API. Nếu chúng ta không có các rule cơ bản hoặc suy nghĩ về cách quản lý state trong ứng dụng của mình thì nó sẽ dẫn đến nhiều hậu quả, sự rối rắm trong việc quản lý state,… mà sau này bạn sẽ phải hối hận đấy.
Chúng ta hãy cùng tìm hiểu với từng loại state thì chúng ta quản lý nó như thế nào nhé.
Local state(UI state)
Đối với local state đơn giản nhất là chúng ta sử dụng useState
. Nó có thể chấp nhận bất kỳ giá trị dữ liệu hợp lệ nào, bao gồm cả giá trị nguyên thủy và object
Ví dụ:
import { useState } from "react";
function SideMenu() {
const [isSidebarOpen, setSidebarOpen] = useState(false);
return (
<>
<Sidebar isSidebarOpen={isSidebarOpen} closeSidebar={() => setSidebarOpen(false)} />
{/* ... */}
</>
);
}
Ngoài ra chúng ta còn một cách khác để quản lý local state nữa đó là sử dụng useReducer, hoặc một thư viện như là XState để quản lý nó trong trường hợp các state này phức tạp. Chuyển đổi giữa các trạng thái được khai báo thông qua useReducer hoặc các công cụ như XState có thể được định nghĩa một cách riêng biệt.
Ví dụ thêm về XState:
XState là thư viện quản lý trạng thái dành cho JavaScript. Ý tưởng đằng sau nó hơi khác một chút so với Redux hoặc React Context vì không cần thiết phải sử dụng global store hoặc gói toàn bộ ứng dụng trong một nhà cung cấp. Nó cung cấp một cách thức cấu trúc và quản lý trạng thái và sự kiện một cách rõ ràng.
Chúng ta hãy thử tìm hiểu về nó thông qua một ví dụ đơn giản như sau:
import React from 'react';
import { Machine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';
const toggleMachine = Machine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
function ToggleApp() {
const [current, send] = useMachine(toggleMachine);
return (
<div>
<h1>XState Toggle Example</h1>
<p>Current State: {current.value}</p>
<button onClick={() => send('TOGGLE')}>Toggle</button>
</div>
);
}
const service = interpret(toggleMachine).start();
export default function App() {
return (
<div>
<ToggleApp />
</div>
);
}
Trong ví dụ này:
- Chúng ta định nghĩa một máy trạng thái XState đơn giản với hai trạng thái là "inactive" và "active", và một sự kiện "TOGGLE" để chuyển đổi giữa chúng.
- Trong thành phần
ToggleApp
, chúng ta sử dụng hookuseMachine
để quản lý trạng thái và sự kiện của máy trạng thái XState. Chúng ta hiển thị trạng thái hiện tại và một nút để gửi sự kiện "TOGGLE". - Chúng ta tạo một máy trạng thái thực tế bằng cách sử dụng
interpret
và bắt đầu nó bên ngoài thành phầnApp
.
Khi bạn chạy ứng dụng này, bạn sẽ thấy rằng trạng thái của ứng dụng sẽ chuyển đổi giữa "inactive" và "active" mỗi khi bạn nhấn vào nút "Toggle". XState giúp bạn quản lý trạng thái ứng dụng và tương tác với nó một cách rất mạnh mẽ và có cấu trúc.
Global state:
Khi bạn quản lý nhiều state sử dụng cho nhiều component, mọi thứ sẽ phức tạp hơn một chút.
Sẽ tới một thời điểm mà ứng dụng của bạn sẽ đạt đến trạng thái “lifting state up“ hoặc bạn sẽ phải truyền các component từ cha xuống con thông qua nhiều cấp, dẫn đến sẽ có nhiều props trong các component này.
Lifting State Up được định nghĩa như sau:
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.
Tuy nhiên kỹ thuật này chỉ có thể sử dụng khi chúng ta truyền data từ component cha sang component con và ngược lại. Tức là bạn chỉ có thể truyền dữ liệu từ 1 component quan hệ cha con. Nếu các bạn để ý thì mình đoán các bạn sẽ có một số câu hỏi, vậy với cây componet mà đến vài chục node thì chẳng lẽ chúng ta sẽ phải chuyền nó qua bằng ấy component. Trong trường hợp này, bạn muốn chia sẻ dữ liệu state sang component khác, chúng ta sẽ có những cách khác, cho phép chúng ta tạo ra global state, giúp việc chia sẻ dữ liệu giữa các component được dễ dàng hơn.
Để giải quyết vấn đề trên, chúng ta sẽ sử dụng các third-party để giải quyết chúng như: Zustand, Jotai, và Recoil. Redux cũng là một công cụ tuyệt vời, nhưng bạn có thể thử Redux Toolkit. Ngoài ra bạn có thể sử dụng các built-in trong React để giải quyết chúng như React Context.
Chúng ta hãy thử một ví dụ với Zustand:
Để sử dụng Zustand, hãy chạy npm install zustand
. Sau đó, tạo một tệp hoặc thư mục lưu trữ và tạo store của bạn:
import create from 'zustand'
const useStore = create(set => ({
votes: 0,
upvote: () => set(state => ({ votes: state.votes + 1 })),
downvote: () => set(state => ({ votes: state.votes - 1 })),
}))
function VoteCounter() {
const { votes, upvote, downvote } = useStore();
return (
<>
Current Votes: {votes}
<button onClick={()=>upvote()}>Upvote</button>
<button onClick={downvote}>Downvote</button>
</>
);
}
export default VoteCounter;
Một lý do bạn nên sử dụng Zustand thay vì thư viện như Redux là vì:
- Cấu trúc đơn giản, dễ dàng implement và sử dụng.
- Zustand quản lý state tập trung ở một chỗ, điểm này tương đồng với Redux, nhưng khác với Redux phải tạo action, reducer và dispatch action để handle thay đổi state thì Zustand lại làm nó một cách dễ dàng hơn, chỉ cần store là đủ.
- Sử dụng hooks để tạo store, phù hợp với cấu trúc dự án sử dụng Hooks ở hiện tại.
Server state(Remote state):
Server state có vẻ như sẽ là một thử thách khó hơn. Nhiều ứng dụng lưu trữ state này dưới dạng local state hoặc thông qua context nếu nó được sử dụng global.
Để làm việc với server state trong React, ta thường phải sử dụng các kỹ thuật như AJAX hoặc Fetch API để tải dữ liệu từ máy chủ. Thông thường, bạn sẽ sử dụng các thư viện hoặc framework như Axios hoặc fetch để thực hiện các yêu cầu HTTP để lấy dữ liệu từ máy chủ. Sau đó sẽ lưu trữ nó tại state trong component hoặc một global state
Đầu tiên chúng ta cần fetch data về để hiển thị trên trang web của mình. Trong khi đó, bạn cần phải hiển thị một spinner trong khi chờ kết quả trả về. Sau đó chúng ta cần phải handle các trường hợp lỗi xảy ra, và sau đó là hiển thị dữ liệu lên.
Chuyện gì sẽ xảy ra khi thao tác lấy dữ liệu bị lỗi? Chúng ta liệu có cần thiết phải fetch lại data nhiều lần, mặc dù trước đó ta đã từng lấy data này về nếu data từ server không đổi. Bởi vì thường có một nhu cầu là chúng ta muốn cache data của server trên client (để lấy nhanh hơn khi cần, và không còn gọi API lại).
Nhưng ngày nay, các thư viện hiện đại của bên thứ ba chuyên xử lý cho server state. Để giải quyết vấn đề này, một vài thư viện đã ra đời, nổi bật trong đó là SWR và react-query(TanStack Query là tên mới của nó). Với react-query, nó không những fetch data và còn xử lí phần caching, đồng bộ và cập nhật data giữa client và server. React-query và SWR không chỉ dành cho việc sử dụng REST. Nó cũng hoạt động với GraphQL. Mặc dù vậy, nếu bạn đang làm việc trong GraphQL, bạn có thể thích các thư viện như Apollo hoặc Urql, những thư viện này cung cấp cách lưu trữ caching riêng biệt.Chúng không chỉ cung cấp cho chúng ta một hook thuận tiện để lấy và thay đổi dữ liệu từ API mà còn theo dõi tất cả các trạng thái cần thiết và lưu trữ dữ liệu cho chúng tôi.
Chúng ta có thể đã sai khi kết hợp local state với server state này chung với nhau trên Redux hay Context. Việc quản lí, cập nhật và đồng bộ hay cache những dữ liệu từ server nên phải được tách riêng ra và xử lí tốt hơn.
Ví dụ về cách dùng react-query:
Để sử dụng react-query chúng ta sẽ cài đặt nó như sau:
npm install @tanstack/react-query && npm install @tanstack/react-query-devtools -f
Bạn đã cài đặt React-query-devtools vì rất khó để gỡ lỗi việc tìm nạp dữ liệu, đó là lý do tại sao bạn cần cài đặt React-query-devtools. Nó sẽ giúp bạn xem và hiểu cách React Query tìm nạp dữ liệu.
Chúng ta viết ở App như sau:
// App.js
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import Todo from "./Todo";
const queryClient = new QueryClient({});
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Todo />
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
);
};
export default App;
Và chúng ta sử dụng nó như sau:
// components/Todo.jsx
import React, { Fragment } from "react";
import axios from "axios";
import { useQuery } from "@tanstack/react-query";
const Todo = () => {
const { isLoading, isError, data, error, refetch } = useQuery(["repo"], () =>
axios
.get("https://dummyjson.com/todos")
.then((res) => res.data)
);
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error.message;
return (
<>
{data?.todos.map(todo => {
return (
<Fragment
key={todo.id}
>
<ul>
<li>
{todo.todo}
</li>
</ul>
</Fragment>
)
})}
<button type="button" onClick={refetch}>
Fetch again
</button>
</>
);
};
export default Todo;
Khi bạn chạy ứng dụng React này, nó sẽ tải danh sách người dùng từ API và hiển thị nó sau khi tải xong. React Query cung cấp nhiều tính năng mạnh mẽ như caching, tự động làm mới dữ liệu, và xử lý lỗi dễ dàng.
URL state
React không cung cấp một cách tiêu chuẩn để quản lý URL state mà bạn cần phải sử dụng các thư viện hoặc phương pháp riêng để thực hiện điều này. Trạng thái URL phần lớn đã được quản lý cho bạn nếu bạn đang sử dụng một framework như Next.js hoặc phiên bản hiện tại của React Router
Nếu bạn sử dụng React Router, bạn có thể lấy tất cả các thông tin cần thiết trên thông qua các hook được cung cấp sẵn như useNavigate
Ví dụ:
import React from 'react';
import {useNavigate} from "react-router-dom"
const Home = () => {
const navigate = useNavigate();
return (
<>
<button onClick={()=>navigate("/about")}>About</button>
</>
)
};
export default Home;
Hơn nữa, nếu bạn cần lấy thông tin của bất kỳ route params nào, bạn có thể sử dụng useParams
import { useParams } from 'react-router-dom';
function UserProfile() {
const { userId } = useParams();
const { avatarUrl, isLoading, isError } = useUser(roomId);
// ...
}
Khi quản lý URL state, hãy chắc chắn tuân thủ các nguyên tắc RESTful và giữ cho URL thể hiện trạng thái ứng dụng một cách rõ ràng và dễ đọc. Điều này giúp bạn dễ dàng theo dõi và hiểu được trạng thái của ứng dụng thông qua URL và cải thiện khả năng tìm kiếm và chia sẻ.
Tổng kết
Chúng ta muốn tạo các ứng dụng React hiệu quả và đáng tin cậy phải sử dụng quản lý trạng thái React. Bằng cách xử lý và quản lý trạng thái của ứng dụng React, cả giải pháp tích hợp sẵn và bên thứ ba đều giữ cho chương trình đồng bộ với giao diện người dùng. Có rất nhiều tùy chọn, bạn chọn tùy chọn nào sẽ phụ thuộc vào yêu cầu của dự án và quy mô nhóm phát triển của bạn. Hi vọng qua bài viết trên sẽ giúp các bạn có thể hiểu hơn về quản lý state trong component và có thể giúp bạn quản lý hiệu quả nó hơn.