Wow! How to easily introduce GlobalState with AppRouter (RSC) and share the state across SC, CC, and ServerActions.
Published: 2024-02-03 02:38:06

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!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3vbq0hp4o7vpqk2kbiat.png)

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.
- https://vercel.com/templates/next.js

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
- https://vercel.com/templates/next.js/nextjs-ai-chatbot

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

- https://github.com/vercel/ai-chatbot

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
- http://localhost:3000/

You can sign in with your GitHub account. If you successfully sign in, the following screen will appear.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vd4v9gkf3q8bru4e4i9f.png)

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](https://platform.openai.com/account/billing/overview).


# 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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vd4v9gkf3q8bru4e4i9f.png)

- Official site
   - https://www.npmjs.com/package/nrstate

- 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.

- https://github.com/vteacher-online/ai-chatbot