Bài viết này nhận được sự hỗ trợ nhiệt tình của những người bạn thân mến của chúng tôi tại Netlify, một nhóm gồm những tài năng đáng kinh ngạc đến từ khắp nơi trên thế giới và cung cấp nền tảng cho các nhà phát triển web giúp tăng năng suất. Cảm ơn bạn!

Bài viết này có mục đích sử dụng như một bài học cơ bản để quản lý các trạng thái phức tạp trong ứng dụng Next.js. Thật không may, khuôn khổ này quá linh hoạt để chúng tôi có thể đề cập đến tất cả các trường hợp sử dụng có thể có trong bài viết này. Nhưng các chiến lược này sẽ phù hợp với phần lớn các ứng dụng xung quanh với ít hoặc không cần điều chỉnh. Nếu bạn tin rằng có một mô hình liên quan cần được xem xét, tôi mong được gặp bạn trong phần bình luận!
Khi ứng dụng phát triển về độ phức tạp và các nhánh của cây kết xuất, nhiều lớp sẽ xuất hiện. Đôi khi, cần phải truyền dữ liệu xuống nhiều lớp thành phần cha cho đến khi cuối cùng đạt đến thành phần mà dữ liệu dành cho, đây được gọi là Prop Drilling.
Như người ta có thể dự đoán: Prop Drilling có thể trở thành một mô hình cồng kềnh và dễ xảy ra lỗi khi ứng dụng phát triển. Để giải quyết vấn đề này, có API Context. API Context thêm 3 phần tử vào phương trình này:
Trong ứng dụng React nguyên bản, dữ liệu có thể được quản lý bởi 2 API khác:
Để kiểm tra sự so sánh giữa các thư viện hữu ích nhất, tôi có thể giới thiệu cho bạn bài đăng này về React State Management.
Next.js là một khuôn khổ React. Vì vậy, bất kỳ giải pháp nào được mô tả cho ứng dụng React đều có thể áp dụng cho ứng dụng Next.js. Một số sẽ yêu cầu flex lớn hơn để thiết lập, một số sẽ có sự đánh đổi được phân phối lại dựa trên các chức năng riêng của Next.js. Nhưng mọi thứ đều có thể sử dụng 100%, bạn có thể tự do lựa chọn.
Đối với phần lớn các trường hợp sử dụng phổ biến, sự kết hợp của Context và State/Reducer là đủ. Chúng ta sẽ xem xét điều này trong bài viết này và không đi sâu vào sự phức tạp của các trạng thái phức tạp. Tuy nhiên, chúng ta sẽ cân nhắc rằng hầu hết các ứng dụng Jamstack đều dựa vào dữ liệu bên ngoài và đó cũng là trạng thái.
Mỗi khi một tuyến đường mới được truy cập, các trang của chúng ta có thể khai thác
Bạn có thể kiểm tra triển khai thực tế của mẫu ContextAPI này trong kho lưu trữ demo của chúng tôi.
Nếu bạn có nhiều phần trạng thái được xác định trong một ngữ cảnh duy nhất, bạn có thể bắt đầu gặp phải các vấn đề về hiệu suất. Lý do là vì khi React thấy một bản cập nhật trạng thái, nó sẽ thực hiện tất cả các lần kết xuất lại cần thiết cho DOM. Nếu trạng thái đó được chia sẻ trên nhiều thành phần (như khi sử dụng Context API), nó có thể gây ra các lần kết xuất lại không cần thiết, điều mà chúng ta không mong muốn. Hãy sáng suốt với các biến trạng thái mà bạn chia sẻ trên các thành phần!
Một điều bạn có thể làm để duy trì sự ngăn nắp khi chia sẻ trạng thái là tạo nhiều phần của Context (và do đó là các Context Providers khác nhau) để chứa các phần trạng thái khác nhau. Ví dụ: bạn có thể chia sẻ xác thực trong một Context, tùy chọn quốc tế hóa trong một Context khác và chủ đề trang web trong một Context khác.
Next.js cũng cung cấp một mẫu
Với mẫu này, bạn có thể tạo nhiều Context Providers và giữ chúng được định nghĩa rõ ràng trong một thành phần Layout cho toàn bộ ứng dụng. Ngoài ra, hàm
Hiện tại, không có giải pháp cấu hình thấp cho mẫu này trong Next.js, nhưng từ các ví dụ trên, chúng ta có thể đưa ra giải pháp. Hãy lấy đoạn trích này trực tiếp từ tài liệu:
Lại là mẫu
Cuối cùng, ứng dụng của chúng tôi phát triển và số lượng
Đây là những gì chúng tôi gọi là Provider Hell. Và nó có thể tệ hơn: nếu
Với vấn đề khủng khiếp này đang được chú ý, Jōtai đã xuất hiện. Đây là một thư viện quản lý trạng thái có chữ ký rất giống với
Nhờ cách tiếp cận từ dưới lên, chúng ta có thể định nghĩa atom của Jōtai (lớp dữ liệu của mỗi thành phần kết nối với store) ở cấp thành phần và thư viện sẽ đảm nhiệm việc liên kết chúng với provider. Tiện ích
Jōtai cũng cung cấp các phương pháp tiếp cận khác để dễ dàng biên soạn và suy ra các định nghĩa trạng thái từ nhau. Nó chắc chắn có thể giải quyết các vấn đề về khả năng mở rộng theo cách gia tăng.
Đối với trạng thái phía máy khách, lại có hai quy trình công việc khác nhau cần được xác nhận:
May mắn thay, nhờ các thư viện như React-Query và SWR, mọi mẫu được hiển thị cho trạng thái cục bộ đều được áp dụng trơn tru cho dữ liệu bên ngoài. Các thư viện như thế này xử lý bộ đệm cục bộ, do đó, bất cứ khi nào trạng thái đã khả dụng, chúng có thể tận dụng định nghĩa cài đặt để gia hạn dữ liệu hoặc sử dụng từ bộ đệm cục bộ. Hơn nữa, họ thậm chí có thể cung cấp cho người dùng dữ liệu cũ trong khi họ làm mới nội dung và nhắc cập nhật giao diện bất cứ khi nào có thể.
Ngoài ra, nhóm React đã minh bạch ngay từ giai đoạn đầu về các API sắp ra mắt nhằm mục đích cải thiện trải nghiệm của người dùng và nhà phát triển trên mặt trận đó (xem tài liệu Suspense được đề xuất tại đây). Nhờ đó, các tác giả thư viện đã chuẩn bị cho thời điểm các API như vậy xuất hiện và các nhà phát triển có thể bắt đầu làm việc với cú pháp tương tự ngay từ hôm nay.
Bây giờ, hãy thêm trạng thái bên ngoài vào bố cục
Như bạn có thể thấy ở trên, hook
Điều quan trọng cần nhấn mạnh ở đây là: một lợi thế lớn của việc có một ứng dụng đẳng cấu là lưu các yêu cầu cho phía back-end. Việc thêm các yêu cầu bổ sung vào ứng dụng của bạn sau khi ứng dụng đã ở phía máy khách sẽ ảnh hưởng đến hiệu suất được nhận thức. Có một bài viết tuyệt vời (và sách điện tử!) về chủ đề này tại đây đi sâu hơn nhiều.
Mẫu này không nhằm mục đích thay thế

Trong trường hợp bạn muốn thứ gì đó ít mang tính chủ quan hơn hoặc chỉ đang nghĩ đến việc bắt đầu với Next.js, thì có dự án khởi đầu tuyệt vời này để giúp bạn bắt đầu dễ dàng triển khai lên Netlify. Một lần nữa, Netlify giúp bạn dễ dàng sao chép nó vào kho lưu trữ của riêng bạn và triển khai:

Bài viết này có mục đích sử dụng như một bài học cơ bản để quản lý các trạng thái phức tạp trong ứng dụng Next.js. Thật không may, khuôn khổ này quá linh hoạt để chúng tôi có thể đề cập đến tất cả các trường hợp sử dụng có thể có trong bài viết này. Nhưng các chiến lược này sẽ phù hợp với phần lớn các ứng dụng xung quanh với ít hoặc không cần điều chỉnh. Nếu bạn tin rằng có một mô hình liên quan cần được xem xét, tôi mong được gặp bạn trong phần bình luận!
API React Core cho dữ liệu
Chỉ có một cách để ứng dụng React truyền dữ liệu: truyền dữ liệu từ các thành phần cha xuống các thành phần con. Bất kể ứng dụng quản lý dữ liệu của mình như thế nào, ứng dụng đó phải truyền dữ liệu từ trên xuống dưới.Khi ứng dụng phát triển về độ phức tạp và các nhánh của cây kết xuất, nhiều lớp sẽ xuất hiện. Đôi khi, cần phải truyền dữ liệu xuống nhiều lớp thành phần cha cho đến khi cuối cùng đạt đến thành phần mà dữ liệu dành cho, đây được gọi là Prop Drilling.
Như người ta có thể dự đoán: Prop Drilling có thể trở thành một mô hình cồng kềnh và dễ xảy ra lỗi khi ứng dụng phát triển. Để giải quyết vấn đề này, có API Context. API Context thêm 3 phần tử vào phương trình này:
- Context
Dữ liệu được chuyển tiếp từ Provider đến Consumer. - Context Provider
Thành phần mà dữ liệu bắt nguồn. - Context Consumer
Thành phần sẽ sử dụng dữ liệu nhận được.
Trong ứng dụng React nguyên bản, dữ liệu có thể được quản lý bởi 2 API khác:
useState
và useReducer
. Bài viết này sẽ không đề cập đến việc khi nào nên sử dụng cái này hay cái kia, vì vậy chúng ta hãy đơn giản hóa bằng cách nói rằng:-
useState
Cấu trúc dữ liệu đơn giản và các điều kiện đơn giản. -
useReducer
Cấu trúc dữ liệu phức tạp và/hoặc các điều kiện đan xen.
shouldComponentUpdate
, nó sẽ ngăn ngữ cảnh tiếp tục xuống mục tiêu của nó. Vấn đề này khiến các nhà phát triển phải dùng đến các thư viện của bên thứ ba khi tất cả những gì họ cần là tránh prop drilling.Để kiểm tra sự so sánh giữa các thư viện hữu ích nhất, tôi có thể giới thiệu cho bạn bài đăng này về React State Management.
Next.js là một khuôn khổ React. Vì vậy, bất kỳ giải pháp nào được mô tả cho ứng dụng React đều có thể áp dụng cho ứng dụng Next.js. Một số sẽ yêu cầu flex lớn hơn để thiết lập, một số sẽ có sự đánh đổi được phân phối lại dựa trên các chức năng riêng của Next.js. Nhưng mọi thứ đều có thể sử dụng 100%, bạn có thể tự do lựa chọn.
Đối với phần lớn các trường hợp sử dụng phổ biến, sự kết hợp của Context và State/Reducer là đủ. Chúng ta sẽ xem xét điều này trong bài viết này và không đi sâu vào sự phức tạp của các trạng thái phức tạp. Tuy nhiên, chúng ta sẽ cân nhắc rằng hầu hết các ứng dụng Jamstack đều dựa vào dữ liệu bên ngoài và đó cũng là trạng thái.
Truyền trạng thái cục bộ thông qua ứng dụng
Ứng dụng Next.js có 2 thành phần quan trọng để xử lý tất cả các trang và chế độ xem trong ứng dụng của chúng ta:-
_document.{t,j}sx
Thành phần này được dùng để định nghĩa đánh dấu tĩnh. Tệp này được hiển thị trên máy chủ và không được hiển thị lại trên máy khách. Sử dụng nó để tác động đến các thẻvà
và siêu dữ liệu khác. Nếu bạn không muốn tùy chỉnh những thứ này, bạn có thể tùy chọn đưa chúng vào ứng dụng của mình.
-
_app.{t,j}sx
Thành phần này được dùng để định nghĩa logic sẽ lan tỏa khắp ứng dụng. Bất kỳ thứ gì cần có trên mọi chế độ xem của ứng dụng đều thuộc về đây. Sử dụng nó cho
Mã:
// _app.jsx hoặc _app.tsximport { AppStateProvider } from './my-context'export default function MyApp({ Component, pageProps }) { return ( )}
AppStateContext
và có các định nghĩa của chúng được truyền xuống dưới dạng props
. Khi ứng dụng của chúng ta đủ đơn giản, nó chỉ cần một định nghĩa để được trải ra như thế này, mẫu trước đó là đủ. Ví dụ:
Mã:
export default function ConsumerPage() { const { state } = useAppStatecontext() return (
{state} ở đây! 🎉
)}
Nếu bạn có nhiều phần trạng thái được xác định trong một ngữ cảnh duy nhất, bạn có thể bắt đầu gặp phải các vấn đề về hiệu suất. Lý do là vì khi React thấy một bản cập nhật trạng thái, nó sẽ thực hiện tất cả các lần kết xuất lại cần thiết cho DOM. Nếu trạng thái đó được chia sẻ trên nhiều thành phần (như khi sử dụng Context API), nó có thể gây ra các lần kết xuất lại không cần thiết, điều mà chúng ta không mong muốn. Hãy sáng suốt với các biến trạng thái mà bạn chia sẻ trên các thành phần!
Một điều bạn có thể làm để duy trì sự ngăn nắp khi chia sẻ trạng thái là tạo nhiều phần của Context (và do đó là các Context Providers khác nhau) để chứa các phần trạng thái khác nhau. Ví dụ: bạn có thể chia sẻ xác thực trong một Context, tùy chọn quốc tế hóa trong một Context khác và chủ đề trang web trong một Context khác.
Next.js cũng cung cấp một mẫu
mà bạn có thể sử dụng cho mục đích như thế này, để trừu tượng hóa toàn bộ logic này ra khỏi tệp _app
, giúp tệp sạch sẽ và dễ đọc.
Mã:
// _app.jsx hoặc _app.tsximport { DefaultLayout } from './layout'export default function MyApp({ Component, pageProps }) { const getLayout = Component.getLayout || (trang => {trang} ) trả về getLayout()}// layout.jsxnhập { AppState_1_Provider } từ '../context/context-1'nhập { AppState_2_Provider } từ '../context/context-2'xuất const DefaultLayout = ({ children }) => { return ( {children} )}
getLayout
sẽ cho phép bạn ghi đè các định nghĩa Layout mặc định trên cơ sở từng trang, do đó, mỗi trang có thể có sự thay đổi riêng về những gì được cung cấp.Tạo một Hierarchy giữa các Routes
Tuy nhiên, đôi khi mẫu Layout có thể không đủ. Khi các ứng dụng ngày càng phức tạp hơn, nhu cầu thiết lập mối quan hệ nhà cung cấp/người tiêu dùng giữa các tuyến đường có thể xuất hiện. Một tuyến đường sẽ bao bọc các tuyến đường khác và do đó cung cấp cho chúng các định nghĩa chung thay vì khiến các nhà phát triển phải sao chép mã. Với suy nghĩ này, có một Đề xuất bao bọc trong các cuộc thảo luận của Next.js để cung cấp trải nghiệm mượt mà cho nhà phát triển để đạt được điều này.Hiện tại, không có giải pháp cấu hình thấp cho mẫu này trong Next.js, nhưng từ các ví dụ trên, chúng ta có thể đưa ra giải pháp. Hãy lấy đoạn trích này trực tiếp từ tài liệu:
Mã:
import Layout from '../components/layout'import NestedLayout from '../components/nested-layout'export default function Page() { return { /** Nội dung của bạn */ }}Page.getLayout = (page) => ( {page} )
getLayout
! Bây giờ nó được cung cấp như một thuộc tính của đối tượng Page
. Nó lấy tham số page
giống như một thành phần React lấy prop children
và chúng ta có thể bao bọc nhiều lớp tùy ý. Tóm tắt điều này thành một mô-đun riêng biệt và bạn chia sẻ logic này với một số tuyến đường nhất định:
Mã:
// routes/user-management.jsxexport const MainUserManagement = (page) => ( {page} )// user-dashboard.jsximport { MainUserManagement } from '../routes/user-management'export const UserDashboard = (props) => ()UserDashboard.getLayout = MainUserManagement
Nỗi đau ngày càng lớn lại tấn công: Địa ngục của nhà cung cấp
Nhờ có API ngữ cảnh của React, chúng tôi đã tránh được Prop Drilling, đây là vấn đề mà chúng tôi đặt ra để giải quyết. Bây giờ chúng tôi có mã dễ đọc và chúng tôi có thể truyềnprops
xuống các thành phần của mình chỉ chạm vào các lớp cần thiết.Cuối cùng, ứng dụng của chúng tôi phát triển và số lượng
props
phải truyền xuống tăng lên với tốc độ ngày càng nhanh. Nếu chúng ta đủ cẩn thận để cô lập và loại bỏ các lần kết xuất lại không cần thiết, rất có thể chúng ta sẽ thu thập được một lượng
không thể đếm được ở gốc bố cục của mình.
Mã:
export const DefaultLayout = ({ children }) => { return ( {children} )}
SpecialProvider
chỉ nhắm vào một trường hợp sử dụng cụ thể thì sao? Bạn có thêm nó vào thời gian chạy không? Việc thêm cả Provider và Consumer trong thời gian chạy không hẳn là đơn giản.Với vấn đề khủng khiếp này đang được chú ý, Jōtai đã xuất hiện. Đây là một thư viện quản lý trạng thái có chữ ký rất giống với
useState
. Về cơ bản, Jōtai cũng sử dụng Context API, nhưng nó trừu tượng hóa Provider Hell khỏi mã của chúng ta và thậm chí cung cấp chế độ "Provider-less" trong trường hợp ứng dụng chỉ yêu cầu một store.Nhờ cách tiếp cận từ dưới lên, chúng ta có thể định nghĩa atom của Jōtai (lớp dữ liệu của mỗi thành phần kết nối với store) ở cấp thành phần và thư viện sẽ đảm nhiệm việc liên kết chúng với provider. Tiện ích
trong Jōtai mang một số chức năng bổ sung trên Context.Provider
mặc định từ React. Nó sẽ luôn cô lập các giá trị từ mỗi nguyên tử, nhưng sẽ sử dụng thuộc tính initialValues
để khai báo một mảng các giá trị mặc định. Vì vậy, ví dụ về Provider Hell ở trên sẽ trông như thế này:
Mã:
import { Provider } from 'jotai'import { AuthAtom, UserAtom, ThemeAtom, SpecialAtom, JustAnotherAtom, VerySpecificAtom} from '@atoms'const DEFAULT_VALUES = [ [AuthAtom, 'value1'], [UserAtom, 'value2'], [ThemeAtom, 'value3'], [SpecialAtom, 'value4'], [JustAnotherAtom, 'value5'], [VerySpecificAtom, 'value6']]export const DefaultLayout = ({ children }) => { return (
{children} )}
Lấy trạng thái
Cho đến nay, chúng tôi đã tạo ra các mẫu và ví dụ để quản lý trạng thái nội bộ trong ứng dụng. Nhưng chúng ta không nên ngây thơ, hầu như không bao giờ có trường hợp ứng dụng không cần lấy nội dung hoặc dữ liệu từ API bên ngoài.Đối với trạng thái phía máy khách, lại có hai quy trình công việc khác nhau cần được xác nhận:
- lấy dữ liệu
- kết hợp dữ liệu vào trạng thái của ứng dụng
- kết nối mạng của người dùng: tránh lấy lại dữ liệu đã có sẵn
- cần làm gì trong khi chờ phản hồi của máy chủ
- cách xử lý khi dữ liệu không khả dụng (lỗi máy chủ hoặc không có dữ liệu)
- cách khôi phục nếu tích hợp bị hỏng (điểm cuối không khả dụng, tài nguyên thay đổi, v.v.)
May mắn thay, nhờ các thư viện như React-Query và SWR, mọi mẫu được hiển thị cho trạng thái cục bộ đều được áp dụng trơn tru cho dữ liệu bên ngoài. Các thư viện như thế này xử lý bộ đệm cục bộ, do đó, bất cứ khi nào trạng thái đã khả dụng, chúng có thể tận dụng định nghĩa cài đặt để gia hạn dữ liệu hoặc sử dụng từ bộ đệm cục bộ. Hơn nữa, họ thậm chí có thể cung cấp cho người dùng dữ liệu cũ trong khi họ làm mới nội dung và nhắc cập nhật giao diện bất cứ khi nào có thể.
Ngoài ra, nhóm React đã minh bạch ngay từ giai đoạn đầu về các API sắp ra mắt nhằm mục đích cải thiện trải nghiệm của người dùng và nhà phát triển trên mặt trận đó (xem tài liệu Suspense được đề xuất tại đây). Nhờ đó, các tác giả thư viện đã chuẩn bị cho thời điểm các API như vậy xuất hiện và các nhà phát triển có thể bắt đầu làm việc với cú pháp tương tự ngay từ hôm nay.
Bây giờ, hãy thêm trạng thái bên ngoài vào bố cục
MainUserManagement
của chúng ta bằng SWR
:
Mã:
import { useSWR } from 'swr'import { UserInfoProvider } from '../context/user-info'import { ExtDataProvider } from '../context/external-data-provider'import { UserNavigationLayout } from '../layouts/user-navigation'import { ErrorReporter } from '../components/error-reporter'import { Loading } from '../components/loading'export const MainUserManagement = (page) => { const { dữ liệu, lỗi } = useSWR('/api/endpoint') nếu (lỗi) => nếu (!data) => return ( {page} )}
useSWR
cung cấp rất nhiều sự trừu tượng:- một trình lấy mặc định
- lớp bộ đệm zero-config
- trình xử lý lỗi
- trình xử lý tải
Điều quan trọng cần nhấn mạnh ở đây là: một lợi thế lớn của việc có một ứng dụng đẳng cấu là lưu các yêu cầu cho phía back-end. Việc thêm các yêu cầu bổ sung vào ứng dụng của bạn sau khi ứng dụng đã ở phía máy khách sẽ ảnh hưởng đến hiệu suất được nhận thức. Có một bài viết tuyệt vời (và sách điện tử!) về chủ đề này tại đây đi sâu hơn nhiều.
Mẫu này không nhằm mục đích thay thế
getStaticProps
hoặc getServerSideProps
trên các ứng dụng Next.js. Đây là một công cụ khác mà nhà phát triển cần có khi xây dựng các tình huống đặc biệt.Những cân nhắc cuối cùng
Khi kết thúc các mẫu này, điều quan trọng là phải nhấn mạnh một số cảnh báo có thể khiến bạn lo lắng nếu không chú ý khi triển khai chúng. Trước tiên, chúng ta hãy tóm tắt lại những gì đã đề cập trong bài viết này:- Context như một cách tránh Prop Drilling;
- API cốt lõi của React để quản lý trạng thái (
useState
vàuseReducer
); - Truyền trạng thái phía máy khách trong suốt ứng dụng Next.js;
- Cách ngăn một số tuyến truy cập trạng thái;
- Cách xử lý việc truy xuất dữ liệu ở phía máy khách cho các ứng dụng Next.js.
- Sử dụng các phương thức phía máy chủ để tạo nội dung tĩnh thường được ưu tiên hơn so với việc truy xuất trạng thái từ phía máy khách.
- API Context có thể dẫn đến nhiều lần kết xuất lại nếu bạn không cẩn thận về nơi diễn ra các thay đổi trạng thái.
Tự mình thử
Bạn có thể kiểm tra các mẫu được mô tả trong bài viết này trực tiếp trên nextjs-layout-state.netlify.app hoặc xem mã trên github.com/atilafassina/nextjs-layout-state. Bạn thậm chí có thể chỉ cần nhấp vào nút này để sao chép ngay lập tức vào nhà cung cấp Git bạn đã chọn và triển khai nó vào Netlify:Trong trường hợp bạn muốn thứ gì đó ít mang tính chủ quan hơn hoặc chỉ đang nghĩ đến việc bắt đầu với Next.js, thì có dự án khởi đầu tuyệt vời này để giúp bạn bắt đầu dễ dàng triển khai lên Netlify. Một lần nữa, Netlify giúp bạn dễ dàng sao chép nó vào kho lưu trữ của riêng bạn và triển khai:
Tài liệu tham khảo
- Context và Redux: sự khác biệt
- Next.js Đề xuất Wrapper
- Bố cục Next.js
- Jōtai
- Sử dụng React Context để quản lý trạng thái trong Next.js
Đọc thêm
- Hướng dẫn tối ưu hóa hình ảnh trên các trang Jamstack
- Mẫu mới cho Jamstack: Kết xuất phân đoạn
- GraphQL toàn ngăn xếp với Next.js, Neo4j AuraDB Và Vercel
- Cách Xây Dựng Một Trang Web Đa Ngôn Ngữ Với Nuxt.js