Server-Side Rendering
To work well with server-rendered pages, Amplify JS requires slight modifications from how you would use it in a client-only environment.
Amplify
Enabling SSR
When using the Amplify CLI, the aws-exports.js file gets created and updated automatically for you based upon the resources you have added and configured.
For client-only apps, Amplify.configure(awsExports)
is all you need.
To enable SSR support, also provide ssr: true
:
import { Amplify } from 'aws-amplify';import awsExports from '../src/aws-exports';
Amplify.configure({ ...awsExports, ssr: true });
By providing ssr: true
, Amplify persists credentials on the client in cookies so that subsequent requests to the server have access to them.
Note: Once vercel/next.js#16977 is resolved, you can hoist
Amplify.configure
into pages/_app.js. Until then, be sure that all pages/* runAmplify.configure({ ...awsExports, ssr: true })
.
withSSRContext
Once an application has been configured with ssr: true
, client-side credentials are passed to the server via cookies.
The withSSRContext
utility creates an instance of Amplify
scoped to a single request (req
) using those cookie credentials.
For client-side code rendered in the browser, your page should continue using top-level imports as usual:
import { Amplify, API } from "aws-amplify";import awsExports from "../src/aws-exports";
Amplify.configure({ ...awsExports, ssr: true });
export default function HomePage({ posts = [] }) { const [posts, setPosts] = useState(posts);
useEffect(() => { // Notice how the client correctly uses the top-level `API` import API.graphql({ query: listPosts }).then(({ data }) => setPosts(data.listPosts.items)); }, [])
return ( ... );}
When on the server, use withSSRContext({ req?: ServerRequest })
:
import { Amplify, API, withSSRContext } from 'aws-amplify';
export async function getServerSideProps({ req }) { // Notice how the server uses `API` from `withSSRContext`, instead of the top-level `API`. const SSR = withSSRContext({ req }); const { data } = await SSR.API.graphql({ query: listPosts });
return { props: { posts: data.listPosts.items } };}
Use Amplify with Next.js App Router ("app/" directory)
Amplify JavaScript can be used with the Next.js App Router (Next.js v13.4+) by applying the following changes:
1. Run Amplify.configure({ ...awsExports, ssr: true })
in both the client-side and server-side code
To use Amplify with Next.js App Router, you must run Amplify.configure()
in both Client and Server Components. The ssr
option must be enabled.
Example Next.js App Router Client Component containing your login flow (e.g. app/page.tsx
):
'use client';
import { Amplify } from 'aws-amplify';import awsExports from '@/aws-exports';
Amplify.configure({ ...awsExports, ssr: true });
export default function Home() { return { /* Render a login form for your users */ };}
Example Next.js App Router Server Component (e.g. app/posts/page.tsx
):
import { Amplify } from 'aws-amplify';import awsExports from '@/aws-exports';
Amplify.configure({ ...awsExports, ssr: true });
export default async function Page() { // ...}
2. Prepare a request object for withSSRContext
to perform server-side operations that require authentication
Example Next.js App Router Server Component that queries data from GraphQL (e.g. app/posts/page.tsx
):
import { Amplify, withSSRContext } from 'aws-amplify';import { headers } from 'next/headers';import { listPosts } from '@/graphql/queries';import awsExports from '@/aws-exports';
Amplify.configure({ ...awsExports, ssr: true });
export default async function Page() { // Construct a req object & prepare an SSR enabled version of Amplify const req = { headers: { cookie: headers().get('cookie') } }; const SSR = withSSRContext({ req });
const { data } = await SSR.API.graphql({ query: listPosts });
// ...}
DataStore
Serializing
For Next.js, returned props
from the server have to be valid JSON. Because DataStore.query(Model)
returns instances of Model
, you need the serializeModel
helper to convert it to JSON instead:
import { serializeModel } from '@aws-amplify/datastore/ssr';import { Amplify, withSSRContext } from "aws-amplify";
...
export async function getServerSideProps({ req }) { const SSR = withSSRContext({ req }); const posts = await SSR.DataStore.query(Post);
return { props: { // This converts Post instances into serialized JSON for the client posts: serializeModel(posts), }, };}
Deserializing
If your client-side code only reads from the server-side props and doesn't perform any updates to these models, then your client-side code won't need any changes.
However, if you receive models from the server and need to DataStore.delete(model)
or DataStore.save(...)
changes to them, you'll need the deserializeModel
utility to convert them from server-friendly JSON back into model instances:
import { deserializeModel } from '@aws-amplify/datastore/ssr';import { Amplify, withSSRContext } from "aws-amplify";
import { Post } from "../src/models";
export default function HomePage(initialData) { // This converts the serialized JSON back into Post instances const [posts, setPosts] = useState(deserializeModel(Post, initialData.posts));
...}
Example:
Using the following schema:
type Post @model @auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) { id: ID! title: String! content: String!}
Open pages/index.js and replace it with the following code:
// pages/index.jsimport Head from 'next/head';import styles from '../styles/Home.module.css';import { Amplify, Auth, AuthModeStrategyType, DataStore, withSSRContext} from 'aws-amplify';import { Authenticator } from '@aws-amplify/ui-react';import { Post } from '../src/models';import { serializeModel } from '@aws-amplify/datastore/ssr';import awsExports from '../src/aws-exports';
Amplify.configure({ ...awsExports, DataStore: { authModeStrategyType: AuthModeStrategyType.MULTI_AUTH }, ssr: true});
export async function getServerSideProps({ req }) { const SSR = withSSRContext({ req }); const posts = await SSR.DataStore.query(Post);
return { props: { // This converts Post instances into serialized JSON for the client posts: serializeModel(posts) } };}
async function handleCreatePost(event) { event.preventDefault();
const form = new FormData(event.target);
try { const post = await DataStore.save( new Post({ title: form.get('title'), content: form.get('content') }) );
window.location.href = `/posts/${post.id}`; } catch (error) { console.log(error); }}
export default function Home({ posts = [] }) { return ( <div className={styles.container}> <Head> <title>Amplify DataStore + Next.js</title> <link rel="icon" href="/favicon.ico" /> </Head>
<main className={styles.main}> <h1 className={styles.title}>Amplify DataStore + Next.js</h1> <p className={styles.description}> <code className={styles.code}>{posts.length}</code> posts </p>
<div className={styles.grid}> {posts.map((post) => ( <a className={styles.card} href={`/posts/${post.id}`} key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </a> ))}
<div className={styles.card}> <h3 className={styles.title}>New Post</h3>
<Authenticator> <form onSubmit={handleCreatePost}> <fieldset> <legend>Title</legend> <input defaultValue={`Today, ${new Date().toLocaleTimeString()}`} name="title" /> </fieldset>
<fieldset> <legend>Content</legend> <textarea defaultValue="I built an Amplify project with Next.js and DataStore!" name="content" /> </fieldset>
<button>Create Post</button> <button type="button" onClick={() => Auth.signOut()}> Sign out </button> </form> </Authenticator> </div> </div> </main> </div> );}