Quản lý trạng thái trong Next.js

theanh

Administrator
Nhân viên
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!

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:
  1. Context
    Dữ liệu được chuyển tiếp từ Provider đến Consumer.
  2. Context Provider
    Thành phần mà dữ liệu bắt nguồn.
  3. Context Consumer
    Thành phần sẽ sử dụng dữ liệu nhận được.
Provider luôn là tổ tiên của thành phần consumer, nhưng có khả năng không là tổ tiên trực tiếp. Sau đó, API bỏ qua tất cả các liên kết khác trong chuỗi và chuyển dữ liệu (ngữ cảnh) trực tiếp cho consumer. Đây là toàn bộ API Context, truyền dữ liệu. Nó liên quan đến dữ liệu nhiều như bưu điện liên quan đến thư của bạn.

Trong ứng dụng React nguyên bản, dữ liệu có thể được quản lý bởi 2 API khác: useStateuseReducer. 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.
Thực tế là Prop Drilling và Data Management trong React bị nhầm lẫn một cách sai lầm là một mô hình một phần thuộc về lỗi cố hữu trong Legacy Content API. Khi một thành phần được hiển thị lại bị chặn bởi 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à 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 , định nghĩa toàn cục, cài đặt ứng dụng, v.v.
Để rõ ràng hơn, các nhà cung cấp Context được áp dụng ở đây, ví dụ:
Mã:
// _app.jsx hoặc _app.tsximport { AppStateProvider } from './my-context'export default function MyApp({ Component, pageProps }) { return (    )}
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 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! 🎉 
 )}
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 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}    )}
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 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} )
Lại là mẫu 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ền props 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}       )}
Đây là những gì chúng tôi gọi là Provider Hell. Và nó có thể tệ hơn: nếu 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}  )}
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.

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:
  1. lấy dữ liệu
  2. kết hợp dữ liệu vào trạng thái của ứng dụng
Khi yêu cầu dữ liệu từ phía máy khách, điều quan trọng là phải lưu ý một số điều sau:
  1. kết nối mạng của người dùng: tránh lấy lại dữ liệu đã có sẵn
  2. cần làm gì trong khi chờ phản hồi của máy chủ
  3. 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)
  4. 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.)
Và bây giờ là lúc mọi thứ bắt đầu trở nên thú vị. Dấu đầu tiên, Mục 1, rõ ràng liên quan đến trạng thái đang tìm nạp, trong khi Mục 2 chuyển dần sang trạng thái quản lý. Mục 3 và 4 chắc chắn nằm trong phạm vi trạng thái quản lý, nhưng cả hai đều phụ thuộc vào hành động tìm nạp và tích hợp máy chủ. Ranh giới chắc chắn là không rõ ràng. Việc xử lý tất cả các phần chuyển động này rất phức tạp và đây là các mẫu không thay đổi nhiều từ ứng dụng này sang ứng dụng khác. Bất cứ khi nào và bất cứ cách nào chúng ta tìm nạp dữ liệu, chúng ta phải xử lý 4 tình huống đó.

May mắn thay, nhờ các thư viện như React-QuerySWR, 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}    )}
Như bạn có thể thấy ở trên, hook 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
Với 2 điều kiện, chúng ta có thể cung cấp các lần trả về sớm trong thành phần của mình khi yêu cầu không thành công (lỗi) hoặc khi chuyến khứ hồi đến máy chủ chưa hoàn tất (đang tải). Vì những lý do này, các thư viện gần giống với các thư viện Quản lý trạng thái. Mặc dù chúng không thực sự là quản lý người dùng, nhưng chúng tích hợp tốt và cung cấp cho chúng ta đủ công cụ để đơn giản hóa việc quản lý các trạng thái bất đồng bộ phức tạp này.

Đ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 (useStateuseReducer);
  • 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.
Có ba sự đánh đổi quan trọng mà chúng ta cần lưu ý khi lựa chọn các kỹ thuật này:
  1. 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.
  2. 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.
Cân nhắc kỹ lưỡng những điểm này sẽ rất quan trọng, ngoài ra, tất cả các biện pháp thực hành tốt khi xử lý trạng thái trong ứng dụng React phía máy khách vẫn hữu ích trên ứng dụng Next.js. Lớp máy chủ có thể cung cấp khả năng tăng hiệu suất và bản thân điều này có thể giảm thiểu một số vấn đề về tính toán. Nhưng nó cũng sẽ được hưởng lợi từ việc tuân thủ các biện pháp thực hành tốt nhất phổ biến khi nói đến hiệu suất hiển thị trên ứng dụng.

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​

Đọ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
 
Back
Bên trên