Wow! How to easily introduce GlobalState with AppRouter (RSC) and share the state across SC, CC, and ServerActions.
VTeacher
Posted on February 3, 2024
Hi,
I'm a front-end engineer and a back-end engineer.
I like choosing a combination of classic technology and slightly geeky technology.
More and more companies are adopting Next.js AppRouter (React Server Components)!
This time, we will introduce a method that we use at our company to share state across the server components (SC), client components (CC), and ServerActions!
In this way, we assume that you want to share the state from a client component at the end of one branch to a server component at another branch in an AppRouter (RSC) environment.
This is when you want to avoid a bucket brigade (chain of handoff from parent to child).
(Still... I'm a bucket brigade advocate...)
Introduction
There is a starter template for Next.js provided by Vercel.
Next.js starter templates and themes
Discover Next.js templates, starters, and themes to jumpstart your application or website build.
It's convenient 😳
This includes a demo that implements a chat UI similar to ChatGPT.
This time, we will introduce a global state to it (nextjs-ai-chatbot
).
Next.js AI Chatbot
A full-featured, hackable Next.js AI chatbot built by Vercel
The skill sets and functions are as follows, and include the minimum elements necessary for general service development.
NextAuth.js is used for the sign-in/sign-out function.
- Next.js App Router
- React Server Components (RSCs), Suspense, and Server Actions
- Vercel AI SDK for streaming chat UI
- Support for OpenAI (default), Anthropic, Cohere, Hugging Face, or custom AI chat models and/or LangChain
-shadcn/ui
- Styling with Tailwind CSS
- Radix UI for headless component primitives
- Icons from Phosphor Icons
- Chat History, rate limiting, and session storage with Vercel KV
- NextAuth.js for authentication
Also, this is provided by the team at Vercel. In other words, it is the implementation code of the original people!
This library is created by Vercel and Next.js team members, with contributions from:
Jared Palmer (@jaredpalmer) - Vercel
Shu Ding (@shuding_) - Vercel
shadcn (@shadcn) - Vercel
local environment
Repository
Please import it using git clone
or Fork.
Environment variables (.env)
If you want to see the code and actually see it working, you'll need a key to OpenAI and Vercel KV (free plan is fine). You will also need a GitHub account for the sign-in feature.
Create a .env
file by copying .env.example
.
- GitHub https://github.com/settings/applications/2435178
- OpenAI https://platform.openai.com/
- Vercel https://vercel.com/storage/kv
*AUTH_SECRET is the value displayed when accessing https://generate-secret.vercel.app/32.
boot
Prepare the Node.js environment and install it.
*For pnpm
pnpm install
pnpm dev
open
You can sign in with your GitHub account. If you successfully sign in, the following screen will appear.
Enter your question in the text field labeled Send a message
, click the Send button, and watch the OpenAI API reply back.
- If you want to change the Large-Scale Language Model (LLM)
It is written to use gpt-3.5-turbo
, so if you want to change to 4 series, rewrite app/api/chat/route.ts
.
const res = await openai.chat.completions.create({
// model: 'gpt-3.5-turbo',
// model: 'gpt-4',
model: 'gpt-4-turbo-preview',
messages,
Temperature: 0.7,
stream: true
})
*OpenAI API is a billing system, so please check the usage status on the Administration screen.
Folder structure
The folder structure of nextjs-ai-chatbot
is as follows.
Project
├── app
│ ├── (chat)
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ └── actions.ts '<--------- * Server Actions (use server)'
│
├── components '<------------ Component storage area'
│ ├── chat.tsx '<--------- * Client Component (use client)'
│ ├── sidebar-list.tsx '<-- * Server Component'
│ └── prompt-form.tsx '<--- * Client Component (unmarked)'
│
├── lib
│ └── hooks '<------------- Custom hook storage area'
├── middleware.ts
└── public '<---------------- Resource file storage area'
Although it is not directly related to this demo, you will want to take advantage of middleware
, which is very convenient when using Vercel.
By the way, regarding the part marked "unmarked", even if use client
is not explicitly stated, the caller (ancestor) is a client.
side, the code effectively functions as a ClientComponent as well. If you are not planning on using it like SharedComponent, it is better to clearly write use client
at the beginning.
Introduce global state
Will begin the main subject. Let's introduce global state to a project configured with this RSC (Next.js AppRouter).
Due to the nature of RSC, traditional global state management strategies such as Recoil cannot be applied. Therefore, it is necessary to use a global state management library specialized for RSC.
This time, we will use nrstate
to implement a global state.
https://www.npmjs.com/package/nrstate
nrstate
represents the following elements
-N:Next.js
- R: React Server Components
- State: State management
This is a global state management library specialized for Next.js' AppRouter.
Install nrstate
*For pnpm
pnpm install nrstate --save-dev
pnpm install nrstate-client --save-dev
Determine the state of the entire page
nrstate
has a concept called PageState. This means considering the state of the entire page. This allows you to retrieve state across components even if the page is made up of multiple components.
In nextjs-ai-chatbot
, I think it is appropriate to treat the only value input by the user (message
) as a global state, so I will use this as PageState.
-
Official site
-
points
- Establish global state considering the entire page (applies across multiple components).
- Setting (setting) global state is only possible on the client side.
- Global state can be retrieved on the client side or server side.
- Refresh processing is done automatically.
- The basic principle of RSC is that it starts with the server component.
Now let's implement it.
Create PageState
Define PageState by creating a file like this:
-
app/PageStateChat.tsx
(*Create a new one)
export type PageStateChat = {
message: string
}
export const initialPageStateChat = {
message:''
} satisfies PageStateChat
export const pathChat = '/'
-
PageStateChat
defines the type of this PageState. -
initialPageStateChat
specifies the default value. -
pathChat
indicates the target path, and this time it is set to the root (/
). This allows PageState to be referenced everywhere under the root.
Implement a provider for PageState.
Since there is no page.tsx in the root of nextjs-ai-chatbot
, implement PageStateProvider
in layout.tsx as an alternative.
Be sure to implement
PageStateProvider
in a server component (I think page.tsx and layout.tsx are usually made into server components, so I don't think there will be any problems...)app/layout.tsx
import.
import { currentPageState } from 'nrstate/PageStateServer'
import PageStateProvider from 'nrstate-client/PageStateProvider'
import {
PageStateChat,
initialPageStateChat,
pathChat
} from '@/app/PageStateChat'
For render, just surround the existing code with PageStateProvider.
export default function RootLayout({ children }: RootLayoutProps) {
return (
<PageStateProvider
current={currentPageState<PageStateChat>(initialPageStateChat, pathChat)}
>
<html lang="en" suppressHydrationWarning>
...(omitted)...
</html>
</PageStateProvider>
)
}
This completes the setup. It's easy.
Set the value for PageState
- The area under
components
is where general-purpose components are placed, but this time we will modify it for convenience.
ClientComponent
PageState can only be set from client components (CC).
Update PageState using the change event of the defined global state message
.
components/prompt-form.tsx
prompt-form.tsx
does not have use client
written at the beginning, but its caller (ancestor) is a client component (CC), so it is running as a client component.
Import PageState.
import { usePageState } from 'nrstate-client/PageStateClient'
import { PageStateChat, pathChat } from '@/app/PageStateChat'
Use the PageState hook.
export function PromptForm({...(omitted)...}) {
const [pageState, setPageState] = usePageState<PageStateChat>()
...(omitted)...
}
After inputting, when you click the submit button (onSubmit event), update PageState using the input value (input
).
<form
onSubmit={async e => {
...(omitted)...
setPageState(
{
...pageState,
message: input
},
pathChat
)
...(omitted)...
Get value from PageState
ClientComponent
components/chat.tsx
Import PageState.
import { usePageState } from 'nrstate-client/PageStateClient'
import { PageStateChat, pathChat } from '@/app/PageStateChat'
You can get the PageState (message) using the PageState hook.
export function Chat(...(omitted)...) {
const [pageState] = usePageState<PageStateChat>()
const { message } = pageState
...(omitted)...
}
Let's display the global state on the screen for debugging.
<p className="text-sm text-blue-500">
ClientComponent(use client): message={message}
</p>
ServerComponent
Next is the server side.
The value of nrstate
can only be changed on the client side, and the value can only be referenced on the server side.
components/sidebar-list.tsx
Import PageState.
*On the server side, use getPageState
.
import { getPageState } from 'nrstate/PageStateServer'
import {
PageStateChat,
initialPageStateChat,
pathChat
} from '@/app/PageStateChat'
Get the PageState using the PageState hook.
export async function SidebarList(...(omitted)...) {
const pageState = getPageState<PageStateChat>(initialPageStateChat, pathChat)
const { message } = pageState
console.log('ServerComponent', message)
...(omitted)...
}
Displays PageState on screen for debugging.
<p className="text-sm text-orange-500">
ServerComponent message={message}
</p>
ServerActions
ServerActions
is also a server-side function, so we treat it in a similar way to server components (SCs).
app/actions.ts
Import PageState.
On the server side, use getPageState
.
import { getPageState } from 'nrstate/PageStateServer'
import {
PageStateChat,
initialPageStateChat,
pathChat
} from '@/app/PageStateChat'
Get the PageState using the PageState hook.
export async function removeChat(...(omitted)...) {
const pageState = getPageState<PageStateChat>(initialPageStateChat, pathChat)
const { message } = pageState
console.log('ServerActions(use server) message=', message)
...(omitted)...
*There is an original bug in nextjs-ai-chatbot
on line 66, so please fix it as follows.
if (String(uid) !== session?.user?.id) {
...(omitted)...
}
Explicitly remove PageState
If you want to explicitly remove PageState, do the following:
(possible with client component)
import { clearPageState } from 'nrstate-client/PageStateClient'
import { pathChat } from '@/app/demo/PageStateChat'
clearPageState(pathChat)
Operation confirmation
Now, let's try out the chat function!
As shown in the following video, the global state (value of PageState
) can be shared across server components, client components, and ServerActions
.
https://www.youtube.com/watch?v=Lzyxijqf6Ik
*For cases where debug information cannot be displayed on the screen, mainly server-side logs, please check from the terminal.
lastly
Here is the current repository.
Posted on February 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.