-
Notifications
You must be signed in to change notification settings - Fork 1k
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
kevinold
merged 21 commits into
main
from
kevold/add-gen2-quickstart-nextjs-app-router-server-components
Jan 26, 2024
Merged
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 b79251d
Merge branch 'main' into kevold/add-gen2-quickstart-nextjs-app-router…
kevinold 166183f
wip - iterate on app router server component quickstart
kevinold e8dfd5a
wip - iterate on middleware for RSC quickstart
kevinold 4b6b6b2
wip - add amplify utils for server configuration
kevinold 1aad2c9
wip - finalize todos section
kevinold 106f045
update to conditionally redirect if user
kevinold dd80efc
fix fragment path
kevinold 5c0f60b
add signout button to server components example
kevinold d7551d9
Import AuthUser to avoid TypeScript error
kevinold 99f8a9a
add installation of amplify next.js adapter
kevinold 6e18355
Group imports into logical sections
kevinold 97e524c
update additional section imports in logicial groups
kevinold cc9f678
add custom <Authenticator> accordion
kevinold 94584de
revert signout functionality from server components guide
kevinold 7d89719
fix typo for AuthUser import
kevinold e72781a
add separate Login component and update Login page
kevinold 4147474
updates per internal feedback
kevinold ba0cc98
updates to add a login page section
kevinold 4f9c194
Clarify that Server Page implementation does not need the Login clien…
kevinold 3ea14a3
Add link for Authenticator customization
kevinold File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
351 changes: 351 additions & 0 deletions
351
src/pages/gen2/start/quickstart/nextjs-app-router-server-components/index.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
|
||
<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 }} /> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.