Skip to content

Commit 7d13094

Browse files
committed
rsc optimistic updates
1 parent 8466152 commit 7d13094

File tree

8 files changed

+334
-105
lines changed

8 files changed

+334
-105
lines changed

apps/7-nextjs-rsc/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
"@tabler/icons-react": "^3.13.0",
1515
"mantine-react-table": "^2.0.0-beta.6",
1616
"next": "14.2.6",
17-
"react": "^18",
18-
"react-dom": "^18"
17+
"react": "19.0.0-rc-b57d2823-20240822",
18+
"react-dom": "19.0.0-rc-b57d2823-20240822"
1919
},
2020
"devDependencies": {
2121
"@next/bundle-analyzer": "^14.2.6",
2222
"@types/node": "^22",
23-
"@types/react": "^18",
24-
"@types/react-dom": "^18",
23+
"@types/react": "^18.3.4",
24+
"@types/react-dom": "^18.3.0",
2525
"postcss": "^8.4.41",
2626
"postcss-preset-mantine": "1.17.0",
2727
"postcss-simple-vars": "^7.0.1",

apps/7-nextjs-rsc/src/api-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ export interface IComment {
3535
name: string;
3636
email: string;
3737
body: string;
38+
sending?: boolean;
3839
}

apps/7-nextjs-rsc/src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import "@mantine/core/styles.css";
2-
import React, { Suspense } from "react";
2+
import React from "react";
33
import { MantineProvider, ColorSchemeScript } from "@mantine/core";
44
import { theme } from "../theme";
55
import { AppLayout } from "@/components/AppLayout";

apps/7-nextjs-rsc/src/app/posts/[id]/CommentForm.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"use client";
2+
3+
import { IComment } from "@/api-types";
4+
import {
5+
ActionIcon,
6+
Button,
7+
Card,
8+
Loader,
9+
Stack,
10+
Text,
11+
Textarea,
12+
Title,
13+
} from "@mantine/core";
14+
import { IconTrash } from "@tabler/icons-react";
15+
import { deleteComment, submitPostComment } from "./actions";
16+
import { useState, useOptimistic, useRef, LegacyRef } from "react";
17+
import { useFormStatus } from "react-dom";
18+
19+
interface CommentSectionProps {
20+
comments: IComment[];
21+
postId: number;
22+
}
23+
24+
export default function CommentSection({
25+
comments,
26+
postId,
27+
}: CommentSectionProps) {
28+
const [commentText, setCommentText] = useState("");
29+
const { pending: isPostingComment } = useFormStatus();
30+
31+
const formRef = useRef<HTMLFormElement>();
32+
33+
// wrap our submitPostComment server action with client side optimistic update logic
34+
async function optimisticallyPostComment(formData: FormData) {
35+
addOptimisticComment({
36+
postId,
37+
id: 0,
38+
name: formData.get("name") as string,
39+
email: formData.get("email") as string,
40+
body: formData.get("body") as string,
41+
sending: true,
42+
});
43+
formRef.current?.reset();
44+
await submitPostComment(formData);
45+
}
46+
47+
const [optimisticComments, addOptimisticComment] = useOptimistic(
48+
comments,
49+
(currentComments: IComment[], newComment: IComment) => {
50+
return [...currentComments, newComment];
51+
}
52+
);
53+
54+
return (
55+
<Stack gap="xl">
56+
{optimisticComments?.map((comment) => (
57+
<Card
58+
opacity={comment.sending ? 0.5 : 1}
59+
withBorder
60+
key={comment.id + comment.email}
61+
>
62+
{comment.email === "user@mailinator.com" ? (
63+
<ActionIcon
64+
color="red"
65+
pos="absolute"
66+
right={10}
67+
top={10}
68+
variant="subtle"
69+
onClick={() => deleteComment(comment)}
70+
>
71+
<IconTrash />
72+
</ActionIcon>
73+
) : null}
74+
<Title order={4}>{comment.name}</Title>
75+
<Title order={5}>{comment.email}</Title>
76+
<Text>{comment.body}</Text>
77+
</Card>
78+
))}
79+
<form
80+
action={optimisticallyPostComment}
81+
ref={formRef as LegacyRef<HTMLFormElement>}
82+
>
83+
<Stack gap="md">
84+
<input type="hidden" name="postId" value={postId} />
85+
<input type="hidden" name="email" value="user@mailinator.com" />
86+
<input type="hidden" name="name" value="User" />
87+
<Textarea
88+
value={commentText}
89+
onChange={(event) => setCommentText(event.currentTarget.value)}
90+
name="body"
91+
disabled={isPostingComment}
92+
label="Post a Comment"
93+
/>
94+
<Button
95+
type="submit"
96+
leftSection={
97+
isPostingComment ? (
98+
<Loader variant="oval" color="white" size="xs" />
99+
) : null
100+
}
101+
>
102+
Post Comment
103+
</Button>
104+
</Stack>
105+
</form>
106+
</Stack>
107+
);
108+
}

apps/7-nextjs-rsc/src/app/posts/[id]/DeleteCommentButton.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.

apps/7-nextjs-rsc/src/app/posts/[id]/page.tsx

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Box, Card, Flex, Stack, Text, Title } from "@mantine/core";
1+
import { Box, Flex, Stack, Text, Title } from "@mantine/core";
22
import { IComment, IPost, IUser } from "../../../api-types";
33
import Link from "next/link";
4-
import CommentForm from "./CommentForm";
5-
import DeleteCommentButton from "./DeleteCommentButton";
4+
import CommentSection from "./CommentSection";
65

76
const fetchPostAndComments = async (postId: number) => {
87
const [post, comments] = await Promise.all([
@@ -22,14 +21,18 @@ const fetchPostAndComments = async (postId: number) => {
2221
};
2322
};
2423

24+
interface PostPageProps {
25+
params: { id: string };
26+
}
27+
2528
// Server Component
26-
export default async function PostPage({ params }: { params: { id: string } }) {
29+
export default async function PostPage({ params }: PostPageProps) {
2730
const { id: postId } = params;
2831

2932
const { post, user, comments } = await fetchPostAndComments(+postId);
3033

3134
return (
32-
<Stack>
35+
<Stack mb="500px">
3336
<Box>
3437
<Title order={1}>Post: {post?.id}</Title>
3538
<Title order={2}>{post?.title}</Title>
@@ -48,19 +51,8 @@ export default async function PostPage({ params }: { params: { id: string } }) {
4851
Comments on this Post
4952
</Title>
5053
</Flex>
51-
<Stack gap="xl">
52-
{comments?.map((comment) => (
53-
<Card withBorder key={comment.id + comment.email}>
54-
{/* Client Component */}
55-
<DeleteCommentButton comment={comment} />
56-
<Title order={4}>{comment.name}</Title>
57-
<Title order={5}>{comment.email}</Title>
58-
<Text>{comment.body}</Text>
59-
</Card>
60-
))}
61-
{/* Client Component */}
62-
<CommentForm postId={+postId} />
63-
</Stack>
54+
{/* Client Component */}
55+
<CommentSection comments={comments} postId={post.id} />
6456
</Stack>
6557
);
6658
}

0 commit comments

Comments
 (0)