├── db ├── db.txt └── index.ts ├── src ├── app │ ├── favicon.ico │ ├── react-query │ │ ├── loading.tsx │ │ ├── Tags.tsx │ │ ├── Subjects.tsx │ │ ├── Books.tsx │ │ ├── BookEdit.tsx │ │ ├── page.tsx │ │ └── BookSearchForm.tsx │ ├── queryClient.ts │ ├── page.tsx │ ├── temp.ts │ ├── api │ │ ├── tags │ │ │ └── route.ts │ │ ├── subjects │ │ │ └── route.ts │ │ └── books │ │ │ ├── update │ │ │ └── route.ts │ │ │ └── route.ts │ ├── globals.css │ ├── serverActions.ts │ ├── rsc │ │ ├── Tags.tsx │ │ ├── Subjects.tsx │ │ ├── Books.tsx │ │ ├── BookEdit.tsx │ │ ├── page.tsx │ │ └── BookSearchForm.tsx │ ├── types.ts │ ├── components │ │ ├── TagsList.tsx │ │ ├── SubjectsList.tsx │ │ ├── BooksList.tsx │ │ └── BookCover.tsx │ ├── Providers.tsx │ ├── Nav.tsx │ └── layout.tsx └── data │ ├── tags.ts │ ├── subjects.ts │ └── books.ts ├── next.config.mjs ├── postcss.config.mjs ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json └── README.md /db/db.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arackaf/react-query-rsc-blog-post/HEAD/db/db.txt -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arackaf/react-query-rsc-blog-post/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /src/app/react-query/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return
Loading ...
; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const globalQueryClient = new QueryClient(); 4 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | export default function Home() { 6 | return null; 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/data/tags.ts: -------------------------------------------------------------------------------- 1 | export const tags = [ 2 | { id: 1, name: "Favorites" }, 3 | { id: 2, name: "Oversized" }, 4 | { id: 3, name: "Rare and Valuable" }, 5 | { id: 4, name: "Un-Categorized" }, 6 | ]; 7 | -------------------------------------------------------------------------------- /src/app/temp.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | const saveBook = (id: string, newTitle: string) => { 4 | fetch("localhost:3000/api/save-book", { 5 | method: "POST", 6 | body: JSON.stringify({ id, title: newTitle }), 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/data/subjects.ts: -------------------------------------------------------------------------------- 1 | export const subjects = [ 2 | { id: 1, name: "American History" }, 3 | { id: 2, name: "Economics" }, 4 | { id: 3, name: "Literature" }, 5 | { id: 4, name: "Math" }, 6 | { id: 5, name: "Science" }, 7 | { id: 6, name: "World History" }, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/api/tags/route.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "@/data/tags"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | export const GET = async () => { 6 | console.log("Fetching tags ..."); 7 | await new Promise((res) => setTimeout(res, 400)); 8 | console.log("Tags fetched"); 9 | 10 | return Response.json({ tags }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .overlay-holder { 6 | display: grid; 7 | grid-template-columns: 1fr; 8 | grid-template-rows: 1fr; 9 | } 10 | .overlay-holder > * { 11 | grid-row-start: 1; 12 | grid-row-end: 2; 13 | grid-column-start: 1; 14 | grid-column-end: 2; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/subjects/route.ts: -------------------------------------------------------------------------------- 1 | import { subjects } from "@/data/subjects"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | export const GET = async () => { 6 | console.log("Fetching subjects ..."); 7 | await new Promise((res) => setTimeout(res, 300)); 8 | console.log("Subjects fetched"); 9 | 10 | return Response.json({ subjects }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/serverActions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidateTag } from "next/cache"; 4 | 5 | export const saveBook = async (id: number, title: string) => { 6 | await fetch("http://localhost:3000/api/books/update", { 7 | method: "POST", 8 | body: JSON.stringify({ 9 | id, 10 | title, 11 | }), 12 | }); 13 | revalidateTag("books-query"); 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/rsc/Tags.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { TagsList } from "../components/TagsList"; 3 | 4 | export const Tags: FC<{}> = async () => { 5 | const tagsResp = await fetch("http://localhost:3000/api/tags", { 6 | next: { 7 | tags: ["tags-query"], 8 | }, 9 | }); 10 | const { tags } = await tagsResp.json(); 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/types.ts: -------------------------------------------------------------------------------- 1 | export type PreviewPacket = { w: number; h: number; b64: string }; 2 | 3 | export type BookImages = { 4 | smallImage: string | null; 5 | smallImagePreview: string | PreviewPacket | null; 6 | }; 7 | 8 | export type Book = { 9 | id: number; 10 | title: string; 11 | authors: string[]; 12 | } & BookImages; 13 | 14 | export type BookEditProps = { 15 | book: { 16 | id: number; 17 | title: string; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/app/components/TagsList.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | export const TagsList: FC<{ tags: any[] }> = ({ tags }) => { 4 | return ( 5 |
6 |

Tags

7 |
8 | {tags.map((tag) => ( 9 |
10 | {tag.name} 11 |
12 | ))} 13 |
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/rsc/Subjects.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { SubjectsList } from "../components/SubjectsList"; 3 | 4 | export const Subjects: FC<{}> = async () => { 5 | const subjectsResp = await fetch("http://localhost:3000/api/subjects", { 6 | next: { 7 | tags: ["subjects-query"], 8 | }, 9 | }); 10 | const { subjects } = await subjectsResp.json(); 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/components/SubjectsList.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | export const SubjectsList: FC<{ subjects: any[] }> = ({ subjects }) => { 4 | return ( 5 |
6 |

Subjects

7 |
8 | {subjects.map((subject) => ( 9 |
10 | {subject.name} 11 |
12 | ))} 13 |
14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/react-query/Tags.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { TagsList } from "../components/TagsList"; 5 | import { useSuspenseQuery } from "@tanstack/react-query"; 6 | 7 | export const Tags: FC<{}> = () => { 8 | const { data } = useSuspenseQuery({ 9 | queryKey: ["tags-query"], 10 | queryFn: () => fetch("http://localhost:3000/api/tags").then((resp) => resp.json()), 11 | }); 12 | 13 | const { tags } = data; 14 | 15 | return ( 16 |
17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/app/rsc/Books.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { BooksList } from "../components/BooksList"; 3 | import { BookEdit } from "./BookEdit"; 4 | 5 | export const Books: FC<{ search: string }> = async ({ search }) => { 6 | const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`, { 7 | next: { 8 | tags: ["books-query"], 9 | }, 10 | }); 11 | const { books } = await booksResp.json(); 12 | 13 | return ( 14 |
15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/react-query/Subjects.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { SubjectsList } from "../components/SubjectsList"; 5 | import { useSuspenseQuery } from "@tanstack/react-query"; 6 | 7 | export const Subjects: FC<{}> = () => { 8 | const { data } = useSuspenseQuery({ 9 | queryKey: ["subejcts-query"], 10 | queryFn: () => fetch("http://localhost:3000/api/subjects").then((resp) => resp.json()), 11 | }); 12 | 13 | const { subjects } = data; 14 | 15 | return ( 16 |
17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClientProvider } from "@tanstack/react-query"; 4 | import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; 5 | import { FC, PropsWithChildren, useEffect, useState } from "react"; 6 | import { globalQueryClient } from "./queryClient"; 7 | 8 | export const Providers: FC> = ({ children }) => { 9 | const [queryClient] = useState(globalQueryClient); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-query-rsc-blog-post", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "create-db": "npx tsx db/index.ts", 11 | "tsc": "tsc" 12 | }, 13 | "dependencies": { 14 | "@tanstack/react-query": "^5.36.2", 15 | "@tanstack/react-query-next-experimental": "^5.36.2", 16 | "next": "14.2.3", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "sqlite3": "^5.1.7", 20 | "tsx": "^4.10.5" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "postcss": "^8", 27 | "tailwindcss": "^3.4.1", 28 | "typescript": "^5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/Nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | import { FC } from "react"; 6 | 7 | export const Nav: FC<{}> = (props) => { 8 | const path = usePathname(); 9 | 10 | const isRscPath = path.includes("rsc"); 11 | const isReactQueryPath = path.includes("react-query"); 12 | 13 | return ( 14 |
15 | {isRscPath ? ( 16 | RSC Version 17 | ) : ( 18 | 19 | RSC Version 20 | 21 | )} 22 | {isReactQueryPath ? ( 23 | RSC and React Query Version 24 | ) : ( 25 | 26 | RSC and React Query Version 27 | 28 | )} 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/rsc/BookEdit.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC, useRef, useTransition } from "react"; 4 | import { saveBook } from "../serverActions"; 5 | import { BookEditProps } from "../types"; 6 | 7 | export const BookEdit: FC = (props) => { 8 | const { book } = props; 9 | const titleRef = useRef(null); 10 | const [saving, startSaving] = useTransition(); 11 | 12 | function doSave() { 13 | startSaving(async () => { 14 | await saveBook(book.id, titleRef.current!.value); 15 | }); 16 | } 17 | 18 | return ( 19 |
20 | 21 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Providers } from "./Providers"; 5 | import Link from "next/link"; 6 | import { Nav } from "./Nav"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Create Next App", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export const dynamic = "force-dynamic"; 16 | export const revalidate = 0; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 26 |
27 |
30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/react-query/Books.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSuspenseQuery } from "@tanstack/react-query"; 4 | import { FC } from "react"; 5 | import { BooksList } from "../components/BooksList"; 6 | import { BookEdit } from "./BookEdit"; 7 | import { useSearchParams } from "next/navigation"; 8 | 9 | export const Books: FC<{}> = () => { 10 | const params = useSearchParams(); 11 | const search = params.get("search") ?? ""; 12 | 13 | const { data } = useSuspenseQuery({ 14 | queryKey: ["books-query", search ?? ""], 15 | queryFn: async () => { 16 | const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`); 17 | const { books } = await booksResp.json(); 18 | 19 | return { books }; 20 | }, 21 | }); 22 | 23 | const { books } = data; 24 | 25 | return ( 26 |
27 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | import sqlite3Module from "sqlite3"; 2 | import { books } from "@/data/books"; 3 | 4 | const sqlite3 = sqlite3Module.verbose(); 5 | 6 | const db = new sqlite3.Database("db/db.txt", sqlite3Module.OPEN_READWRITE, async (error) => { 7 | if (error) { 8 | console.error({ error }); 9 | return; 10 | } 11 | 12 | await run("DROP TABLE IF EXISTS books"); 13 | 14 | await run("CREATE TABLE books (id INT PRIMARY KEY, title TEXT, authors TEXT, smallImage TEXT, smallImagePreview TEXT)"); 15 | 16 | for (const book of books) { 17 | await run("INSERT INTO books VALUES (?, ?, ?, ?, ?)", [ 18 | book.id, 19 | book.title, 20 | JSON.stringify(book.authors), 21 | book.smallImage, 22 | JSON.stringify(book.smallImagePreview), 23 | ]); 24 | } 25 | }); 26 | 27 | function run(command: string, params: unknown[] = []): Promise { 28 | return new Promise((res, rej) => { 29 | db.run(command, params, (err) => { 30 | if (err) { 31 | rej(err); 32 | } else { 33 | res(); 34 | } 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/rsc/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import { Books } from "./Books"; 3 | import { Subjects } from "./Subjects"; 4 | import { Tags } from "./Tags"; 5 | import { BookSearchForm } from "./BookSearchForm"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | export const revalidate = 0; 9 | 10 | export default function RSC(props: { searchParams: any }) { 11 | const search = props.searchParams.search || ""; 12 | 13 | return ( 14 |
15 |

Books page in RSC

16 | Loading...}> 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/BooksList.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { BookCover } from "./BookCover"; 3 | import { BookEditProps } from "../types"; 4 | 5 | type Props = { books: any[]; BookEdit: FC }; 6 | 7 | export const BooksList: FC = ({ books, BookEdit }) => { 8 | return ( 9 |
10 |

Books

11 | {books.length === 0 ? ( 12 | No results 13 | ) : ( 14 | books.map((book) => ( 15 |
16 |
17 | 18 |
19 |
20 | {book.title} 21 | {(book.authors ?? []).join(", ")} 22 | 23 |
24 |
25 | )) 26 | )} 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/api/books/update/route.ts: -------------------------------------------------------------------------------- 1 | import sqlite3Module from "sqlite3"; 2 | const sqlite3 = sqlite3Module.verbose(); 3 | 4 | export const dynamic = "force-dynamic"; 5 | export const revalidate = 0; 6 | 7 | export const POST = async (request: Request, context: any) => { 8 | const body = await request.json(); 9 | const { id, title } = body; 10 | await update(id, title); 11 | 12 | console.log("\n\nBook Updated! 🎉🎉🎉"); 13 | 14 | return Response.json({}); 15 | }; 16 | 17 | function update(id: number, title: string) { 18 | return new Promise((res) => { 19 | const db = new sqlite3.Database("db/db.txt", sqlite3Module.OPEN_READWRITE, async (error) => { 20 | try { 21 | if (error) { 22 | console.log({ error }); 23 | return res(null); 24 | } 25 | 26 | db.run("UPDATE books SET title = ? WHERE id = ?", [title, id], (error) => { 27 | if (error) { 28 | console.log({ error }); 29 | } 30 | return res(null); 31 | }); 32 | } catch (err) { 33 | console.log(err); 34 | } finally { 35 | try { 36 | db.close(); 37 | } catch (er) {} 38 | res(null); 39 | } 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/react-query/BookEdit.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC, useRef, useTransition } from "react"; 4 | import { BookEditProps } from "../types"; 5 | import { useQueryClient } from "@tanstack/react-query"; 6 | 7 | export const BookEdit: FC = (props) => { 8 | const { book } = props; 9 | const titleRef = useRef(null); 10 | const queryClient = useQueryClient(); 11 | const [saving, startSaving] = useTransition(); 12 | 13 | const saveBook = async (id: number, newTitle: string) => { 14 | startSaving(async () => { 15 | await fetch("/api/books/update", { 16 | method: "POST", 17 | body: JSON.stringify({ 18 | id, 19 | title: newTitle, 20 | }), 21 | }); 22 | 23 | await queryClient.invalidateQueries({ queryKey: ["books-query"] }); 24 | }); 25 | }; 26 | 27 | return ( 28 |
29 | 30 | 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/rsc/BookSearchForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC, FormEventHandler, useRef, useTransition } from "react"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | 6 | export const BookSearchForm: FC<{}> = () => { 7 | const searchRef = useRef(null); 8 | const currentPath = usePathname(); 9 | const searchParams = useSearchParams(); 10 | const currentSearch = searchParams.get("search")! || ""; 11 | const router = useRouter(); 12 | const [searching, startTransition] = useTransition(); 13 | 14 | const onSubmit: FormEventHandler = (evt) => { 15 | evt.preventDefault(); 16 | 17 | const searchParams = new URLSearchParams(); 18 | if (searchRef.current?.value) { 19 | searchParams.set("search", searchRef.current.value); 20 | } 21 | const queryString = searchParams.toString(); 22 | startTransition(() => { 23 | router.push(currentPath + (queryString ? "?" : "") + queryString); 24 | }); 25 | }; 26 | 27 | return ( 28 |
29 | 30 | {searching ? Searching ... : null} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/react-query/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import { Books } from "./Books"; 3 | import { Subjects } from "./Subjects"; 4 | import { Tags } from "./Tags"; 5 | import { BookSearchForm } from "./BookSearchForm"; 6 | import { globalQueryClient } from "../queryClient"; 7 | 8 | export const dynamic = "force-dynamic"; 9 | 10 | export default function ReactQuery({ searchParams: { search = "" } }: any) { 11 | console.log("\n\n\nPROPS", { search }); 12 | globalQueryClient.prefetchQuery({ 13 | queryKey: ["books-query", search ?? ""], 14 | queryFn: async () => { 15 | console.log("PREFETCHING BOOKS"); 16 | const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`); 17 | const { books } = await booksResp.json(); 18 | 19 | return { books }; 20 | }, 21 | }); 22 | 23 | return ( 24 |
25 |

Books page with RSC and react-query

26 | Loading...}> 27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /src/app/components/BookCover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { BookImages } from "../types"; 4 | import type { FC } from "react"; 5 | 6 | type BookImagesPassed = Partial; 7 | 8 | export const BookCover: FC<{ book: BookImagesPassed }> = (props) => { 9 | const { book } = props; 10 | const noCoverMessage = "No Cover"; 11 | const previewToUse = book.smallImagePreview!; 12 | 13 | const previewString = previewToUse == null ? "" : typeof previewToUse === "string" ? previewToUse : previewToUse.b64; 14 | const sizingStyle = previewToUse != null && typeof previewToUse === "object" ? { width: `${previewToUse.w}px`, height: `${previewToUse.h}px` } : {}; 15 | 16 | const urlToUse = book.smallImage; 17 | 18 | let noCoverClasses: string = ""; 19 | let noCoverCommonClasses = "bg-primary-4 text-primary-9 text-center"; 20 | 21 | noCoverClasses = noCoverCommonClasses + " "; 22 | noCoverClasses += "w-[50px] h-[65px] pt-2 text-sm"; 23 | 24 | return ( 25 |
26 | {previewString ? ( 27 | 28 | ) : null} 29 | {urlToUse ? ( 30 | Book cover 31 | ) : ( 32 |
33 |
{noCoverMessage}
34 |
35 | )} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/app/api/books/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import sqlite3Module from "sqlite3"; 3 | const sqlite3 = sqlite3Module.verbose(); 4 | 5 | export const dynamic = "force-dynamic"; 6 | export const revalidate = 0; 7 | 8 | export const GET = async (request: NextRequest) => { 9 | console.log("\n\nFetching books ..."); 10 | 11 | const search = request.nextUrl.searchParams.get("search"); 12 | 13 | await new Promise((res) => setTimeout(res, 400)); 14 | 15 | const books = await new Promise((res) => { 16 | const db = new sqlite3.Database("db/db.txt", sqlite3Module.OPEN_READWRITE, async (error) => { 17 | try { 18 | if (error) { 19 | return res(null); 20 | } 21 | 22 | const query = `SELECT * FROM books ${search ? ` WHERE title LIKE ? ` : ""} ORDER BY id DESC`; 23 | const params = search ? [`%${search}%`] : []; 24 | 25 | db.all(query, params, (err, rows) => { 26 | if (err) { 27 | return res(null); 28 | } 29 | 30 | const books = rows.map((row: any) => { 31 | const result = { ...row }; 32 | result.authors = JSON.parse(row.authors); 33 | result.smallImagePreview = JSON.parse(row.smallImagePreview); 34 | 35 | return result; 36 | }); 37 | return res(books); 38 | }); 39 | } catch (err: any) { 40 | return res(null); 41 | } finally { 42 | try { 43 | db.close(); 44 | } catch (er) {} 45 | } 46 | }); 47 | }); 48 | 49 | console.log("Books fetched"); 50 | return Response.json({ books }); 51 | }; 52 | -------------------------------------------------------------------------------- /src/app/react-query/BookSearchForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC, FormEventHandler, useRef, useTransition } from "react"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | import { useQueryClient } from "@tanstack/react-query"; 6 | 7 | export const BookSearchForm: FC<{}> = () => { 8 | const searchRef = useRef(null); 9 | const currentPath = usePathname(); 10 | const searchParams = useSearchParams(); 11 | const currentSearch = searchParams.get("search")! || ""; 12 | const router = useRouter(); 13 | const [searching, startTransition] = useTransition(); 14 | const queryClient = useQueryClient(); 15 | 16 | const onSubmit: FormEventHandler = (evt) => { 17 | evt.preventDefault(); 18 | 19 | const searchParams = new URLSearchParams(); 20 | if (searchRef.current?.value) { 21 | searchParams.set("search", searchRef.current.value); 22 | } 23 | const queryString = searchParams.toString(); 24 | startTransition(() => { 25 | const search = searchParams.get("search") ?? ""; 26 | // queryClient.prefetchQuery({ 27 | // queryKey: ["books-query", search ?? ""], 28 | // queryFn: async () => { 29 | // const booksResp = await fetch(`http://localhost:3000/api/books?search=${search}`); 30 | // const { books } = await booksResp.json(); 31 | 32 | // return { books }; 33 | // }, 34 | // }); 35 | 36 | router.push(currentPath + (queryString ? "?" : "") + queryString); 37 | }); 38 | }; 39 | 40 | return ( 41 |
42 | 43 | {searching ? Searching ... : null} 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/data/books.ts: -------------------------------------------------------------------------------- 1 | export const books = [ 2 | { 3 | id: 2015, 4 | dateAdded: "2024-05-05 03:08:03", 5 | title: "GODEL ESCHER BACH : ETERNAL GOLDEN BRAID / 20TH ANNIVERSARY ED", 6 | authors: ["Hofstadter, Douglas R."], 7 | isbn: "9780465026562", 8 | pages: 824, 9 | isRead: 0, 10 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/504fefb3-6bd7-4422-96cf-0d35d857dc6f.jpg", 11 | smallImagePreview: { 12 | h: 72, 13 | w: 50, 14 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMElEQVR4nGPgYWS1UVfmYWRlqA6x+/90SaaHJUOUo/H/g21u+moMgvy8EuJCYqLCAMT1CYV6y8gRAAAAAElFTkSuQmCC", 15 | }, 16 | }, 17 | { 18 | id: 2014, 19 | dateAdded: "2024-05-05 03:07:03", 20 | title: "The Innovators: How a Group of Hackers, Geniuses, and Geeks Created the Digital Revolution", 21 | authors: ["Isaacson, Walter"], 22 | isbn: "9781476708706", 23 | pages: 560, 24 | isRead: 0, 25 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/e48db076-6bca-41ad-b8db-0c0e8f38cbf2.jpg", 26 | smallImagePreview: { 27 | h: 76, 28 | w: 50, 29 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGNgcC9kCK1i0E9gYIjoYDBJYZD0YmDwLBcrmJu7/SkDg1+7Uc2Wfd//AwC+MQzJdYlOYAAAAABJRU5ErkJggg==", 30 | }, 31 | }, 32 | { 33 | id: 2013, 34 | dateAdded: "2024-05-05 03:06:03", 35 | title: "The Education of Henry Adams", 36 | authors: ["Adams, Henry"], 37 | isbn: "9780679640103", 38 | pages: 560, 39 | isRead: 0, 40 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/cc0c39e1-0754-4f55-903a-8d2d8c82b393.jpg", 41 | smallImagePreview: { 42 | h: 78, 43 | w: 50, 44 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGPYsaLp//+Pe1Y2MCxu85rT5F0U4cBQ6ClhpCIWaSfC4KDEwMXAEOWoCgByNA2yhZcgPwAAAABJRU5ErkJggg==", 45 | }, 46 | }, 47 | { 48 | id: 2012, 49 | dateAdded: "2024-05-05 03:04:03", 50 | title: "Winning Independence: The Decisive Years of the Revolutionary War, 1778-1781", 51 | authors: ["Ferling, John"], 52 | isbn: "9781635572766", 53 | pages: 736, 54 | isRead: 0, 55 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/b2439433-afd9-49fe-a319-40dca7aee72e.jpg", 56 | smallImagePreview: { 57 | h: 76, 58 | w: 50, 59 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAM0lEQVR4nAEoANf/AKGVhpiAcMCihgAgAABUNho/IwIA//i4/+Wg/+KlAKyac7Sje7akf2J2E3KzygNIAAAAAElFTkSuQmCC", 60 | }, 61 | }, 62 | { 63 | id: 2005, 64 | dateAdded: "2024-03-30 03:48:04", 65 | title: "The Lying Stones of Marrakech: Penultimate Reflections in Natural History", 66 | authors: ["Gould, Stephen Jay"], 67 | isbn: "9780609601426", 68 | pages: 384, 69 | isRead: 0, 70 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/828160e1-6149-44ce-a8ff-ab7b57e6b5e2.jpg", 71 | smallImagePreview: { 72 | h: 76, 73 | w: 50, 74 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAM0lEQVR4nAEoANf/AOPk5Pr+/t7j4wC5uLUMAADCwr4AwsG9HBcJx8S+AOHh4fD09Njb2wDKGYho9r4mAAAAAElFTkSuQmCC", 75 | }, 76 | }, 77 | { 78 | id: 2004, 79 | dateAdded: "2024-04-02 05:44:05", 80 | title: "The Power of Babel: A Natural History of Language", 81 | authors: ["McWhorter, John"], 82 | isbn: "9780060520854", 83 | pages: 352, 84 | isRead: 0, 85 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/f2a9cce8-ea65-4a59-98c7-4152f5672969.jpg", 86 | smallImagePreview: { 87 | h: 76, 88 | w: 50, 89 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAM0lEQVR4nAEoANf/ACMMAGtfSjMkEQDu7eC9vrf8++0A2trQra6l5ufYAPTx2BYEAPn23YTYFklb2i3iAAAAAElFTkSuQmCC", 90 | }, 91 | }, 92 | { 93 | id: 2003, 94 | dateAdded: "2024-04-02 05:44:05", 95 | title: "The Seventies: The Great Shift In American Culture, Society, And Politics", 96 | authors: ["Bruce J. Schulman"], 97 | isbn: "9780306811265", 98 | pages: 352, 99 | isRead: 0, 100 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/8b79de68-c41e-4e26-849e-10d70e1e403f.jpg", 101 | smallImagePreview: { 102 | h: 77, 103 | w: 50, 104 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGPwSWa49lYvp5WBYd42hjf/nSx8GBhyexhKpzNw8TMwyGgxBDYzKNgxAAAQ/AstPtK5OwAAAABJRU5ErkJggg==", 105 | }, 106 | }, 107 | { 108 | id: 1994, 109 | dateAdded: "2024-03-30 03:32:05", 110 | title: "Beethoven: The Universal Composer (Eminent Lives)", 111 | authors: ["Morris, Edmund"], 112 | isbn: "9780060759742", 113 | pages: 256, 114 | isRead: 0, 115 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/49a3f273-ec98-4300-bce3-a2b75d54333f.jpg", 116 | smallImagePreview: { 117 | h: 72, 118 | w: 50, 119 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGP4+vJifUm0ihwXw/8vN04vbbeyUGb4vW3m7+XtRUEODH1y4k4KCmwsLACKjBBr8WIw/wAAAABJRU5ErkJggg==", 120 | }, 121 | }, 122 | { 123 | id: 1993, 124 | dateAdded: "2024-03-30 03:32:05", 125 | title: "Machiavelli: Philosopher of Power (Eminent Lives)", 126 | authors: ["King, Ross"], 127 | isbn: "9780060817176", 128 | pages: 245, 129 | isRead: 0, 130 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/a47b9d56-9340-4a89-8430-dc45bda9da20.jpg", 131 | smallImagePreview: { 132 | h: 72, 133 | w: 50, 134 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAM0lEQVR4nAEoANf/AHqFVf/52tzAngB7hF3v0bHiuJwAjIJdt5Fpu4NfAEULAEAAADEAAL1+Ed7HObsSAAAAAElFTkSuQmCC", 135 | }, 136 | }, 137 | { 138 | id: 1991, 139 | dateAdded: "2024-03-30 03:18:05", 140 | title: "Finite and Infinite Games", 141 | authors: ["Carse, James"], 142 | isbn: "9781476731711", 143 | pages: 160, 144 | isRead: 0, 145 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/06aa36c4-b2cb-4a95-9425-17150455ad42.jpg", 146 | smallImagePreview: { 147 | h: 76, 148 | w: 50, 149 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAM0lEQVR4nAEoANf/AKd+cuTq68rKxwBYWDfU2tjt7OwAK0Upvb696enqAAASAIhpafr9+tcRFsiW7K5+AAAAAElFTkSuQmCC", 150 | }, 151 | }, 152 | { 153 | id: 1990, 154 | dateAdded: "2024-03-30 03:18:05", 155 | title: "The Second Day at Gettysburg: Essays on Confederate and Union Leadership", 156 | authors: [], 157 | isbn: "9780873384827", 158 | pages: 224, 159 | isRead: 0, 160 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/1ce88c95-e8df-4ff8-b17b-2b107e279210.jpg", 161 | smallImagePreview: { 162 | h: 75, 163 | w: 50, 164 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGP431X9/9qx/+2lDBkMTGYMDKVcwgyZmupd5WUHS/MZvnjY/c+I/O5lDwBYEhBcAil0CAAAAABJRU5ErkJggg==", 165 | }, 166 | }, 167 | { 168 | id: 1989, 169 | dateAdded: "2024-03-30 03:18:05", 170 | title: "The First Day at Gettysburg: Essays on Confederate and Union Leadership", 171 | authors: [], 172 | isbn: "9780873384575", 173 | pages: 184, 174 | isRead: 0, 175 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/f74f5ff2-3564-4e6f-9bb9-c363b26087dd.jpg", 176 | smallImagePreview: { 177 | h: 75, 178 | w: 50, 179 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGP4H2z0Py/ov7saw5M4t//PLv2fUsbgyMAQY6x1yMuc4SADw3IGhpMMDABwMQ9mldHuwgAAAABJRU5ErkJggg==", 180 | }, 181 | }, 182 | { 183 | id: 1982, 184 | dateAdded: "2024-02-27 20:50:04", 185 | title: "The American Museum of Natural History and How It Got That Way", 186 | authors: ["Davey, Colin"], 187 | isbn: "9780823289639", 188 | pages: 278, 189 | isRead: 0, 190 | smallImage: "https://my-library-cover-uploads.s3.amazonaws.com/medium-covers/573d1b97120426ef0078aa92/646c1657-15b6-42c0-ab76-b8af525848e0.jpg", 191 | smallImagePreview: { 192 | h: 75, 193 | w: 50, 194 | b64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAECAIAAADETxJQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAMUlEQVR4nGP49uDE/x+P/7+/ztAzY3pmXUdu60QGJRVlBl4VTjV9BllpVVUZEQExSQCpNw7UGm4kGwAAAABJRU5ErkJggg==", 195 | }, 196 | }, 197 | ]; 198 | --------------------------------------------------------------------------------