Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement Gen2 Next.js React Server Components (RSC) Quickstart #6776

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c7a6502
wip - cut page for app router server components
kevinold Jan 9, 2024
b79251d
Merge branch 'main' into kevold/add-gen2-quickstart-nextjs-app-router…
kevinold Jan 11, 2024
166183f
wip - iterate on app router server component quickstart
kevinold Jan 12, 2024
e8dfd5a
wip - iterate on middleware for RSC quickstart
kevinold Jan 12, 2024
4b6b6b2
wip - add amplify utils for server configuration
kevinold Jan 12, 2024
1aad2c9
wip - finalize todos section
kevinold Jan 12, 2024
106f045
update to conditionally redirect if user
kevinold Jan 16, 2024
dd80efc
fix fragment path
kevinold Jan 17, 2024
5c0f60b
add signout button to server components example
kevinold Jan 19, 2024
d7551d9
Import AuthUser to avoid TypeScript error
kevinold Jan 19, 2024
99f8a9a
add installation of amplify next.js adapter
kevinold Jan 19, 2024
6e18355
Group imports into logical sections
kevinold Jan 19, 2024
97e524c
update additional section imports in logicial groups
kevinold Jan 19, 2024
cc9f678
add custom <Authenticator> accordion
kevinold Jan 22, 2024
94584de
revert signout functionality from server components guide
kevinold Jan 24, 2024
7d89719
fix typo for AuthUser import
kevinold Jan 25, 2024
e72781a
add separate Login component and update Login page
kevinold Jan 25, 2024
4147474
updates per internal feedback
kevinold Jan 26, 2024
ba0cc98
updates to add a login page section
kevinold Jan 26, 2024
4f9c194
Clarify that Server Page implementation does not need the Login clien…
kevinold Jan 26, 2024
3ea14a3
Add link for Authenticator customization
kevinold Jan 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/directory/directory.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1934,7 +1934,10 @@ export const directory = {
path: 'src/pages/gen2/start/quickstart/nextjs-pages-router/index.mdx'
},
{
path: 'src/pages/gen2/start/quickstart/nextjs-app-router/index.mdx'
path: 'src/pages/gen2/start/quickstart/nextjs-app-router-client-components/index.mdx'
},
{
path: 'src/pages/gen2/start/quickstart/nextjs-app-router-server-components/index.mdx'
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const meta = {
title: 'Next.js App Router',
title: 'Next.js App Router (Client Components)',
description: 'Get started with AWS Amplify (Gen 2) using the Next.js App Router.'
};

Expand All @@ -11,7 +11,7 @@ export function getStaticProps(context) {
};
}

This Quickstart guide will walk you through how to build a task list application with TypeScript, Next.js **App Router**, and React. If you are new to these technologies, we recommend you go through the official [React](https://react.dev/learn/tutorial-tic-tac-toe), [Next.js](https://nextjs.org/docs/getting-started/installation), and [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) tutorials first.
This Quickstart guide will walk you through how to build a task list application with TypeScript, Next.js **App Router with Client Components**, and React. If you are new to these technologies, we recommend you go through the official [React](https://react.dev/learn/tutorial-tic-tac-toe), [Next.js](https://nextjs.org/docs/getting-started/installation), and [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) tutorials first.

import prerequisites from 'src/fragments/gen2/quickstart/prerequisites.mdx';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
export const meta = {
title: 'Next.js App Router (Server Components)',
description: 'Get started with AWS Amplify (Gen 2) using the Next.js App Router using Server Components.'
};

export function getStaticProps(context) {
return {
props: {
meta
}
};
}

This Quickstart guide will walk you through how to build a task list application with TypeScript, Next.js **App Router with Server Components**, and React. If you are new to these technologies, we recommend you go through the official [React](https://react.dev/learn/tutorial-tic-tac-toe), [Next.js](https://nextjs.org/docs/getting-started/installation), and [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) tutorials first.

import prerequisites from 'src/fragments/gen2/quickstart/prerequisites.mdx';
Copy link
Member

@reesscot reesscot Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non blocking: thought for furture fragment usage, maybe we should just wrap the import and Fragment in a React component, so we could import these with easier to understand component names.


<Fragments fragments={{ javascript: prerequisites, nextjs: prerequisites }} />

import createProject from 'src/fragments/gen2/quickstart/create-nextjs-app-router-project.mdx';

<Fragments fragments={{ javascript: createProject, nextjs: createProject }} />

import buildABackend from 'src/fragments/gen2/quickstart/build-a-backend.mdx';

<Fragments fragments={{ javascript: buildABackend, nextjs: buildABackend }} />


## Build UI

Let's add UI that connects to the backend data and auth resources.


### Configure Amplify Client Side

First, install the Amplify UI component library:

```bash
npm install @aws-amplify/ui-react
```

Next, create a `components` folder in the root of your project and the contents below to a file called `ConfigureAmplify.tsx`.

```ts title="components/ConfigureAmplify.tsx"
// components/ConfigureAmplify.tsx
"use client";

import { Amplify } from "aws-amplify";

import config from "@/amplifyconfiguration.json";

Amplify.configure(config, { ssr: true });

export default function ConfigureAmplifyClientSide() {
return null;
}
```

Update `app/layout.tsx` to import and render `<ConfigureAmplifyClientSide />`. This client component will configure Amplify for client pages in our application.

```ts title="app/layout.tsx"
// app/layout.tsx
import "@aws-amplify/ui-react/styles.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

import ConfigureAmplifyClientSide from "@/components/ConfigureAmplify";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<ConfigureAmplifyClientSide />
{children}
</body>
</html>
);
}
```

### Add a login page

kevinold marked this conversation as resolved.
Show resolved Hide resolved
First, create a client side Login component in the `components` folder that will be wrapped in `withAuthenticator`. If the user is logged in, they will be redirected to the index route, otherwise the [Amplify UI Authenticator component](https://ui.docs.amplify.aws/react/connected-components/authenticator) will be rendered.

```ts title="components/Login.tsx"
// components/Login.tsx
"use client";

import { withAuthenticator } from "@aws-amplify/ui-react";
kevinold marked this conversation as resolved.
Show resolved Hide resolved
import { AuthUser } from "aws-amplify/auth";
import { redirect } from "next/navigation";
import { useEffect } from "react";

function Login({ user }: { user?: AuthUser }) {
useEffect(() => {
if (user) {
redirect("/");
}
}, [user]);
return null;
}

export default withAuthenticator(Login);
```

Next, create a new route under `app/login/page.tsx` to render the `Login` component.

```ts title="app/login/page.tsx"
// app/login/page.tsx

import Login from "@/components/Login";

export default function LoginPage() {
return <Login />;
}
```

<Accordion title="Custom <Authenticator> example">

Some applications require more customization for the `<Authenticator>` component. The following example shows how to add a custom Header to the `<Authenticator>`.
kevinold marked this conversation as resolved.
Show resolved Hide resolved

```ts title="app/login/page.tsx"
// app/login/page.tsx - Custom <Authenticator>

"use client";

import {
Authenticator,
Text,
View,
useAuthenticator,
} from "@aws-amplify/ui-react";
import { redirect } from "next/navigation";
import { useEffect } from "react";

const components = {
Header() {
return (
<View textAlign="center">
<Text><span style={{color: "white"}}>Authenticator Header</span></Text>
</View>
);
},
};

function CustomAuthenticator() {
const { user } = useAuthenticator((context) => [context.user]);

useEffect(() => {
if (user) {
redirect("/");
}
}, [user]);

return <Authenticator components={components} />;
}

export default function Login() {
return (
<Authenticator.Provider>
<CustomAuthenticator />
</Authenticator.Provider>
);
}

```

</Accordion>


### Configure Amplify Server Side

First, install the Amplify Next.js Adapter:

```bash
npm install @aws-amplify/adapter-nextjs
```

Next, create a `utils/amplify-utils.ts` file from the root of the project and paste the code below. `runWithAmplifyServerContext` and `cookiesClient` are declared here and will be used to gain access to Amplify assets from the server.


```ts title="utils/amplify-utils.ts"
// utils/amplify-utils.ts
import { cookies } from "next/headers";

import { createServerRunner } from "@aws-amplify/adapter-nextjs";
kevinold marked this conversation as resolved.
Show resolved Hide resolved
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";

import { type Schema } from "@/amplify/data/resource";
import config from "@/amplifyconfiguration.json";

export const { runWithAmplifyServerContext } = createServerRunner({
config,
});

export const cookiesClient = generateServerClientUsingCookies<Schema>({
config,
cookies,
});
```

### Add middleware for server-side redirect

Create `middleware.ts` in the root of the project with the contents below.

This middleware runs `fetchAuthSession` wrapped in `runWithAmplifyServerContext` and will redirect to `/login` when a user is not logged in.


```ts title="middleware.ts"
// middleware.ts
import { NextRequest, NextResponse } from "next/server";

import { fetchAuthSession } from "aws-amplify/auth/server";

import { runWithAmplifyServerContext } from "@/utils/amplify-utils";

export async function middleware(request: NextRequest) {
const response = NextResponse.next();

const authenticated = await runWithAmplifyServerContext({
nextServerContext: { request, response },
operation: async (contextSpec) => {
try {
const session = await fetchAuthSession(contextSpec, {});
return session.tokens !== undefined;
} catch (error) {
console.log(error);
return false;
}
},
});

if (authenticated) {
return response;
}

return NextResponse.redirect(new URL("/login", request.url));
}

export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - login
*/
"/((?!api|_next/static|_next/image|favicon.ico|login).*)",
],
};
```

Run your application with `npm run dev` and navigate to `http://localhost:3000`. You should now see the authenticator, which is already configured and ready for your first sign-up! Create a new user account, confirm the account through email, and then sign in.

### View list of to-do items

Now, let's display data on our app's frontend.

The code below uses the `cookiesClient` to provide access to the `Todo` model defined in the backend.

Modify your app's home page file, `app/page.tsx`, with the following code:

```ts title="app/page.tsx"
// app/page.tsx

import { cookiesClient } from "@/utils/amplify-utils";

async function App() {
const { data: todos } = await cookiesClient.models.Todo.list();

return (
<>
<h1>Hello, Amplify 👋</h1>
<ul>
{todos && todos.map((todo) => <li key={todo.id}>{todo.content}</li>)}
</ul>
</>
);
}

export default App;
```

Once you save the file and navigate back to `http://localhost:3000`, you should see "Hello, Amplify" with a blank page for now because you have only an empty list of to-dos.

### Create a new to-do item

Let's update the component to have a form for prompting the user for the title for creating a new to-do list item and run the `addTodo` method on form submission. In a production app, the additional fields of the `Todo` model would be added to the form.

After creating a todo, `revalidatePath` is run to clear the Next.js cache for this route to instantly update the results from the server without a full page reload.

```ts title="app/page.tsx"
// app/page.tsx

import { revalidatePath } from "next/cache";

import { cookiesClient } from "@/utils/amplify-utils";

async function App() {
const { data: todos } = await cookiesClient.models.Todo.list();

async function addTodo(data: FormData) {
"use server";
const title = data.get("title") as string;
await cookiesClient.models.Todo.create({
content: title,
done: false,
priority: "medium",
});
revalidatePath("/");
}

return (
<>
<h1>Hello, Amplify 👋</h1>
<form action={addTodo}>
<input type="text" name="title" />
<button type="submit">Add Todo</button>
</form>

<ul>
{todos && todos.map((todo) => <li key={todo.id}>{todo.content}</li>)}
</ul>
</>
);
}

export default App;
```

### Terminate dev server

Go to `localhost` in the browser to make sure you can now log in and create and list to-dos. You can end your development session by shutting down the frontend dev server and cloud sandbox. The sandbox prompts you to delete your backend resources. While you can retain your backend, we recommend deleting all resources so you can start clean again next time.

import deployAndHost from 'src/fragments/gen2/quickstart/deploy-and-host.mdx';

<Fragments fragments={{ javascript: deployAndHost, nextjs: deployAndHost }} />
Loading