├── 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 |
28 | {children}
29 |
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 |
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 |

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 |
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 |
--------------------------------------------------------------------------------