Skip to content

Commit 5510ea8

Browse files
committed
nextjs
1 parent 46fd105 commit 5510ea8

File tree

7 files changed

+461
-0
lines changed

7 files changed

+461
-0
lines changed

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: "Abbreviated Intro to Next.js"
3+
---
4+
5+
This is not an intro to Next.js course, intentionally. Frontend Masters has an amazing teacher for that, Scott Moss. We are going to use Next.js here, but only as a tool to teach about RSCs. We'll leave the rest of "how to write great Next.js code" to Scott and his Intro to Next.js Course. You'll learn some Next.js incidentally, but we'll be glossing over it.
6+
7+
Next.js is a fantastic React.js complete framework built by the fine folks at Vercel. Much of the React core team works at Vercel and therefore a lot of the newest changes show up in Next.js first because it's the same people working on both.
8+
9+
Next.js has gone through several iterations but the latest version makes RSCs a first class citizen where everything is an RSC by default and you have to opt into client-side components. I'll say this has some trade-offs - RSCs are more complicated to deal with, but it's definitely a good opinion for lots of apps out there. It's worth having as a tool in your tool belt.
10+
11+
You can use Next.js in one of two ways (in my opinion): it can be your entirely monolithic server (great answer for some apps) or you can have it as a "middle-end" server where it just serves your front-end and you have another server that's just your backend (this could be a Fastify, FastAPI, Flask, Sinatra, etc.) server.
12+
13+
Let's get our app started
14+
15+
```javascript
16+
npx create-next-app@15.1.7 --js --app --src-dir --turbopack
17+
```
18+
19+
You can call the app whatever you want. I'd omit ESLint and Tailwind (no big deal if you want to include them, we're just not going to use them today.)
20+
21+
Okay, let's shape this app to be what we need. We're going to continue on our path to building Note Passer, so let's install a few more depenencies
22+
23+
```javascript
24+
npm i doodle.css@0.0.2 promised-sqlite3@2.1.0 sqlite3@5.1.7
25+
```
26+
27+
Now let's create our DB. Here's a few options for you
28+
29+
- Download [this notes.db file][db] and put it in the root directory of your project
30+
- Download [this seed SQL file][seed] and use it to set up your SQLite DB
31+
32+
Either works. You can copy/paste that seed file into SQLite session or you can also run `.read seed.sql` (if seed is in the same directory as the notes.db file) to read the local file.
33+
34+
Next we'll modify src/page.js
35+
36+
```javascript
37+
import Link from "next/link";
38+
39+
export default function Home() {
40+
return (
41+
<div>
42+
<ul>
43+
<li>
44+
<Link href="/my">My Notes</Link>
45+
</li>
46+
<li>
47+
<Link href="/write">Write a Note</Link>
48+
</li>
49+
<li>
50+
<Link href="/teacher">Secret Teacher Feed</Link>
51+
</li>
52+
<li>
53+
<Link href="/who-am-i">Who Am I</Link>
54+
</li>
55+
</ul>
56+
</div>
57+
);
58+
}
59+
```
60+
61+
And now layout.js
62+
63+
```javascript
64+
import Link from "next/link";
65+
import "doodle.css/doodle.css";
66+
import "./globals.css";
67+
68+
export const metadata = {
69+
title: "Note Passer",
70+
description: "Example app for Frontend Masters",
71+
};
72+
73+
export default async function RootLayout({ children }) {
74+
return (
75+
<html lang="en">
76+
<body className="doodle">
77+
<nav>
78+
<h1>
79+
<Link href="/">Note Passer</Link>
80+
</h1>
81+
</nav>
82+
{children}
83+
</body>
84+
</html>
85+
);
86+
}
87+
```
88+
89+
Great! This should be all fairly non-interesting. The only thing here is that page.js will be `/` homepage for our app and layout.js will be the layout file that wraps all inner components. This is a Next.js thing with their App router. Again, no need to worry about any of that right now - you'll cover App router more in-depth in the Intro to Next.js class.
90+
91+
Lastly, update [the globals.css to be this][css]. Go ahead and delete page.module.css. CSS modules are awesome but not in scope for this class so I've just written all the CSS for you.
92+
93+
[seed]:
94+
[db]:
95+
[css]:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
So let's now do a server component where a user can read their own individual notes. This will look a lot like what we did with the no framework version!
2+
3+
Make a folder inside the app directory called `my`. Inside that directory, put page.js. This will make the route /my where this page will show up.
4+
5+
```javascript
6+
import { AsyncDatabase } from "promised-sqlite3";
7+
8+
// this page assumes that you are logged in as user 1
9+
export default async function MyNotes() {
10+
async function fetchNotes() {
11+
const db = await AsyncDatabase.open("./notes.db");
12+
const fromPromise = db.all(
13+
"SELECT n.id as id, n.note as note, f.name as from_user, t.name as to_user FROM notes n JOIN users f ON f.id = n.from_user JOIN users t ON t.id = n.to_user WHERE from_user = ?",
14+
["1"]
15+
);
16+
const toPromise = db.all(
17+
"SELECT n.id as id, n.note as note, f.name as from_user, t.name as to_user FROM notes n JOIN users f ON f.id = n.from_user JOIN users t ON t.id = n.to_user WHERE to_user = ?",
18+
["1"]
19+
);
20+
const [from, to] = await Promise.all([fromPromise, toPromise]);
21+
return {
22+
from,
23+
to,
24+
};
25+
}
26+
27+
const notes = await fetchNotes();
28+
29+
return (
30+
<div>
31+
<h1>My Notes</h1>
32+
<fieldset>
33+
<legend>Notes To You</legend>
34+
<table>
35+
<thead>
36+
<tr>
37+
<th>From</th>
38+
<th>To</th>
39+
<th>Note</th>
40+
</tr>
41+
</thead>
42+
<tbody>
43+
{notes.to.map(({ id, note, from_user, to_user }) => (
44+
<tr key={id}>
45+
<td>{from_user}</td>
46+
<td>{to_user}</td>
47+
<td>{note}</td>
48+
</tr>
49+
))}
50+
</tbody>
51+
</table>
52+
</fieldset>
53+
<fieldset>
54+
<legend>Notes From You</legend>
55+
<table>
56+
<thead>
57+
<tr>
58+
<th>From</th>
59+
<th>To</th>
60+
<th>Note</th>
61+
</tr>
62+
</thead>
63+
<tbody>
64+
{notes.from.map(({ id, note, from_user, to_user }) => (
65+
<tr key={id}>
66+
<td>{from_user}</td>
67+
<td>{to_user}</td>
68+
<td>{note}</td>
69+
</tr>
70+
))}
71+
</tbody>
72+
</table>
73+
</fieldset>
74+
</div>
75+
);
76+
}
77+
```
78+
79+
- We've built this to essentially the user is always logged in as user 1, brian. Feel free afterwards to add your own auth and make it a full-fledged app. [Neon Auth][neon] (which I helped build!) and [Clerk][clerk] are two great options here.
80+
- It's an async function
81+
- We're able to use the SQLite driver which is server-only normally
82+
- Again, we don't have to say `"use server";` because it's assumed that any componet without "use client" is a server component
83+
- Make sure to check out the network traffic! It's cool to see all the React Flight protocol stuff in action!
84+
85+
[neon]: https://neon.tech/blog/neon-auth-is-here-get-authentication-in-a-couple-of-clicks
86+
[clerk]: https://clerk.com
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
One of the most common interactions that users have with websites is some variation on submitting a form - whether that's a very traditional "fill out this form and hit submit" or something similar where the developer uses a form under the hood to handle inputs and then uses a submit event to fire off an API call.
2+
3+
The React team leaned into this with a feature called server actions. You can essentially create an RSC that has an action on the form that will cause an action to fire on the server. It makes it really simple to hook up a frontend form and a backend function to process it. Let's first craft our form.
4+
5+
Make a folder inside app called `write`. In there put page.js, so that we can have the route `/write`.
6+
7+
```javascript
8+
import { AsyncDatabase } from "promised-sqlite3";
9+
import postNote from "./postNote";
10+
11+
export default async function Write() {
12+
async function getUsers() {
13+
const db = await AsyncDatabase.open("./notes.db");
14+
return db.all("SELECT * FROM users");
15+
}
16+
const users = await getUsers();
17+
18+
return (
19+
<div>
20+
<fieldset className="note-fieldset">
21+
<legend>Write a new note</legend>
22+
<form action={postNote} className="note-form">
23+
<label>
24+
From
25+
<select name="from_user">
26+
{users.map((user) => (
27+
<option key={user.id} value={user.id}>
28+
{user.name}
29+
</option>
30+
))}
31+
</select>
32+
</label>
33+
<label>
34+
To
35+
<select defaultValue={2} name="to_user">
36+
{users.map((user) => (
37+
<option key={user.id} value={user.id}>
38+
{user.name}
39+
</option>
40+
))}
41+
</select>
42+
</label>
43+
<label>
44+
Note
45+
<textarea name="note" />
46+
</label>
47+
<button type="submit">Save</button>
48+
</form>
49+
</fieldset>
50+
</div>
51+
);
52+
}
53+
```
54+
55+
`<form action={postNote} className="note-form">` is the interesting part here. We're going to go code this up now but essentially it's allowing us to directly plug a backend function into the frontend form and React will take care of the transport of the data from frontend to backend. We get to skip writing all the `fetch` logic - React does it for us. We could have totally written this in the no-framework version of our app but it would have just been some extra steps in the server code. This is a React feature that Next.js implements.
56+
57+
Let's go write postNote.js in the same directory.
58+
59+
```javascript
60+
"use server";
61+
import { AsyncDatabase } from "promised-sqlite3";
62+
63+
export default async function postNote(formData) {
64+
console.log("postNote called", formData);
65+
66+
const from = formData.get("from_user");
67+
const to = formData.get("to_user");
68+
const note = formData.get("note");
69+
70+
if (!from || !to || !note) {
71+
throw new Error("All fields are required");
72+
}
73+
74+
const db = await AsyncDatabase.open("./notes.db");
75+
await db.run(
76+
"INSERT INTO notes (from_user, to_user, note) VALUES (?, ?, ?)",
77+
[from, to, note]
78+
);
79+
}
80+
```
81+
82+
_Now_ we need the `"use server";` directive at the top as this must be run on the server and we need to disambiguate that for React. Try commenting it out - the app will crash.
83+
84+
Pretty cool, right? I love not having to write the whole API handshake code and just to have it written for me.
85+
86+
Be sure to watch the network tab as well - it's cool to see the React Flight protocol handle it!
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
One question you should probably have by this point is "how do I mix server and client component?" Super valid - we're obviously going to need both in order to ship complete apps. So how do we do that? Just by being a little judicious of how nest things and using React's innate ability to nest components.
2+
3+
Let's say we have a secret teacher view that allows the teacher to see all the notes passed between everyone in their class. And we want that data to be consistently updated so a teacher can always see the latest notes. How would we do that? Let's do it with polling - we'll query the database for rows in the database and then we'll continually update it.
4+
5+
Now, we could just make it totally a client component but we're going to make it load with complete data for the first time and then we'll start polling.
6+
7+
So how do we do that? We'll have a server component loading the first page load's data, and then we'll have a child client component doing the polling and re-rendering. Best of both worlds!
8+
9+
Make a folder called `teacher` in the app directory and put a page.js file in there with this in it:
10+
11+
```javascript
12+
import TeacherClientPage from "./clientPage";
13+
import fetchNotes from "./fetchNotes";
14+
15+
export default async function TeacherView() {
16+
const initialNotes = await fetchNotes();
17+
return (
18+
<TeacherClientPage initialNotes={initialNotes} fetchNotes={fetchNotes} />
19+
);
20+
}
21+
```
22+
23+
This a server component that will load initial notes from a fetchNotes function and then feed it in as a prop. Let's go write that fetchNote.js file.
24+
25+
```javascript
26+
"use server";
27+
import { AsyncDatabase } from "promised-sqlite3";
28+
29+
export default async function fetchNotes(since) {
30+
const db = await AsyncDatabase.open("./notes.db");
31+
let rows;
32+
if (since) {
33+
rows = await db.all(
34+
"SELECT n.id as id, n.note as note, f.name as from_user, t.name as to_user FROM notes n JOIN users f ON f.id = n.from_user JOIN users t ON t.id = n.to_user WHERE n.id > ? LIMIT 50",
35+
[since]
36+
);
37+
} else {
38+
rows = await db.all(
39+
"SELECT n.id as id, n.note as note, f.name as from_user, t.name as to_user FROM notes n JOIN users f ON f.id = n.from_user JOIN users t ON t.id = n.to_user LIMIT 50"
40+
);
41+
}
42+
return rows;
43+
}
44+
```
45+
46+
Another server function to fetch our latest update of rows. It'll only grab whatever is newer than what the client had. What's nice is we can share this function between the inital payload and the update function in the client. Let's go make clientPage.js then.
47+
48+
```javascript
49+
"use client";
50+
import { useState, useEffect } from "react";
51+
52+
export default function TeacherClientPage({ fetchNotes, initialNotes }) {
53+
const [notes, setNotes] = useState(initialNotes ? initialNotes : []);
54+
55+
useEffect(() => {
56+
const interval = setInterval(async () => {
57+
let since;
58+
if (notes.length > 0) {
59+
since = notes[notes.length - 1]?.id ?? null;
60+
}
61+
const newNotes = await fetchNotes(since);
62+
setNotes([...notes, ...newNotes]);
63+
}, 5000);
64+
return () => clearInterval(interval);
65+
}, []);
66+
67+
return (
68+
<div>
69+
<h1>Teacher's View</h1>
70+
<ul>
71+
{notes.map((note) => (
72+
<li key={note.id}>
73+
<fieldset>
74+
<h2>
75+
from: {note.from_user} | to: {note.to_user}
76+
</h2>
77+
<p>{note.note}</p>
78+
</fieldset>
79+
</li>
80+
))}
81+
</ul>
82+
</div>
83+
);
84+
}
85+
```
86+
87+
Now we have a client component that is using a server function to poll for changes, and its initial payload is seeded by a server component. Pretty cool, right? Best of both worlds.

0 commit comments

Comments
 (0)