Compare commits

..

13 Commits

Author SHA1 Message Date
4251a58aa5 Experiment with React-Query 2025-04-02 16:17:17 +02:00
94346dd242 New Data handler with lowdb 2025-04-02 12:06:12 +02:00
a54d7b57ee Some notes on Server Components in edit/page.tsx 2025-04-01 22:30:10 +02:00
b73c9311f5 Complete "Updating Contacts with FormData" 2025-04-01 17:58:39 +02:00
434b7c8dc8 Remove caching. That caching is breaking the program in case of change.
Note: "use server" is not good because force the fetch to be action/static so the client doesn't call them again explicitly during render (I think?)
2025-04-01 17:58:11 +02:00
800c231580 Loading is not easily possible here. I will see later. 2025-04-01 17:21:15 +02:00
aa81948f29 Edit Contact but without loading? 2025-04-01 17:14:43 +02:00
a4536d20b4 Complete "Creating Contacts". 2025-04-01 16:40:35 +02:00
75337be90c Complete "URL Params in Loaders" and "Throwing Responses". 2025-04-01 14:10:20 +02:00
298d25a579 Complete "Layout Routes". 2025-04-01 12:17:08 +02:00
276bea81b0 Remove unused "Hydrate Fallback".
Note: As already said, this loading appear even when the app/layout.tsx is loading. This is not the behaviour I want. In fact the behaviour I wanted is impossible to be needed with Next.js and his "SSR first" approach.
2025-04-01 11:42:49 +02:00
595520296c Complete "Hydrate Fallback".
Note: I checked in React DevTool that the Suspend and the fallback is present, but the skeleton of the page is already rendered into the server so that Suspend is impossible to trigger!
You can see it as it is during long load, o I will probably remove it in the next commit.
2025-04-01 11:41:33 +02:00
4330157491 Complete "Loading Data" 2025-04-01 11:32:54 +02:00
19 changed files with 781 additions and 310 deletions

View File

@@ -1,6 +1,7 @@
import { dirname } from "path"; import { dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc"; import { FlatCompat } from "@eslint/eslintrc";
import pluginQuery from '@tanstack/eslint-plugin-query';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -11,6 +12,7 @@ const compat = new FlatCompat({
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.extends("next/core-web-vitals", "next/typescript"),
...pluginQuery.configs['flat/recommended'],
]; ];
export default eslintConfig; export default eslintConfig;

View File

@@ -9,16 +9,20 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.71.3",
"lowdb": "^7.0.1",
"match-sorter": "^8.0.0", "match-sorter": "^8.0.0",
"next": "15.2.4", "next": "15.2.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"server-only": "^0.0.1",
"sort-by": "^1.2.0", "sort-by": "^1.2.0",
"tiny-invariant": "^1.3.3" "tiny-invariant": "^1.3.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@next/eslint-plugin-next": "^15.2.4", "@next/eslint-plugin-next": "^15.2.4",
"@tanstack/eslint-plugin-query": "^5.68.0",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

75
pnpm-lock.yaml generated
View File

@@ -8,6 +8,12 @@ importers:
.: .:
dependencies: dependencies:
'@tanstack/react-query':
specifier: ^5.71.3
version: 5.71.3(react@19.1.0)
lowdb:
specifier: ^7.0.1
version: 7.0.1
match-sorter: match-sorter:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0 version: 8.0.0
@@ -20,6 +26,9 @@ importers:
react-dom: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
server-only:
specifier: ^0.0.1
version: 0.0.1
sort-by: sort-by:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
@@ -33,6 +42,9 @@ importers:
'@next/eslint-plugin-next': '@next/eslint-plugin-next':
specifier: ^15.2.4 specifier: ^15.2.4
version: 15.2.4 version: 15.2.4
'@tanstack/eslint-plugin-query':
specifier: ^5.68.0
version: 5.68.0(eslint@9.23.0)(typescript@5.8.2)
'@types/node': '@types/node':
specifier: ^20 specifier: ^20
version: 20.17.29 version: 20.17.29
@@ -318,6 +330,19 @@ packages:
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tanstack/eslint-plugin-query@5.68.0':
resolution: {integrity: sha512-w/+y5LILV1GTWBB2R/lKfUzgocKXU1B7O6jipLUJhmxCKPmJFy5zpfR1Vx7c6yCEsQoKcTbhuR/tIy+1sIGaiA==}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
'@tanstack/query-core@5.71.3':
resolution: {integrity: sha512-SW7PXYpKiQCZU5pPOXG2UiHX1oSAxLyxoJiYbQ3aLYiABJW0UnfBZ9PVnO/x4ZCIVK30wfDn0h6Mrsr10q922Q==}
'@tanstack/react-query@5.71.3':
resolution: {integrity: sha512-4GZxKqiMX+CH4CxXiUBNipbqzk2V5jUU8Z4d6J+lSDu8OejcLtjx1hEDJzabFZHwd+ZHCsYLAJmM54kCS1/ekA==}
peerDependencies:
react: ^18 || ^19
'@tybys/wasm-util@0.9.0': '@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
@@ -1118,6 +1143,10 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
lowdb@7.0.1:
resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==}
engines: {node: '>=18'}
match-sorter@8.0.0: match-sorter@8.0.0:
resolution: {integrity: sha512-bGJ6Zb+OhzXe+ptP5d80OLVx7AkqfRbtGEh30vNSfjNwllu+hHI+tcbMIT/fbkx/FKN1PmKuDb65+Oofg+XUxw==} resolution: {integrity: sha512-bGJ6Zb+OhzXe+ptP5d80OLVx7AkqfRbtGEh30vNSfjNwllu+hHI+tcbMIT/fbkx/FKN1PmKuDb65+Oofg+XUxw==}
@@ -1348,6 +1377,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
set-function-length@1.2.2: set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1401,6 +1433,10 @@ packages:
stable-hash@0.0.5: stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
steno@4.0.2:
resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
engines: {node: '>=18'}
streamsearch@1.1.0: streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -1762,6 +1798,21 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@tanstack/eslint-plugin-query@5.68.0(eslint@9.23.0)(typescript@5.8.2)':
dependencies:
'@typescript-eslint/utils': 8.29.0(eslint@9.23.0)(typescript@5.8.2)
eslint: 9.23.0
transitivePeerDependencies:
- supports-color
- typescript
'@tanstack/query-core@5.71.3': {}
'@tanstack/react-query@5.71.3(react@19.1.0)':
dependencies:
'@tanstack/query-core': 5.71.3
react: 19.1.0
'@tybys/wasm-util@0.9.0': '@tybys/wasm-util@0.9.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -2247,8 +2298,8 @@ snapshots:
'@typescript-eslint/parser': 8.29.0(eslint@9.23.0)(typescript@5.8.2) '@typescript-eslint/parser': 8.29.0(eslint@9.23.0)(typescript@5.8.2)
eslint: 9.23.0 eslint: 9.23.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.23.0) eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@9.23.0) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0))(eslint@9.23.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.23.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.23.0)
eslint-plugin-react: 7.37.4(eslint@9.23.0) eslint-plugin-react: 7.37.4(eslint@9.23.0)
eslint-plugin-react-hooks: 5.2.0(eslint@9.23.0) eslint-plugin-react-hooks: 5.2.0(eslint@9.23.0)
@@ -2267,7 +2318,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.23.0): eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.0 debug: 4.4.0
@@ -2278,22 +2329,22 @@ snapshots:
tinyglobby: 0.2.12 tinyglobby: 0.2.12
unrs-resolver: 1.3.3 unrs-resolver: 1.3.3
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@9.23.0) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0))(eslint@9.23.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.23.0): eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0))(eslint@9.23.0):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.29.0(eslint@9.23.0)(typescript@5.8.2) '@typescript-eslint/parser': 8.29.0(eslint@9.23.0)(typescript@5.8.2)
eslint: 9.23.0 eslint: 9.23.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.23.0) eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@9.23.0): eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0))(eslint@9.23.0):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.8 array-includes: 3.1.8
@@ -2304,7 +2355,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.23.0 eslint: 9.23.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.23.0) eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0))(eslint@9.23.0))(eslint@9.23.0)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -2758,6 +2809,10 @@ snapshots:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
lowdb@7.0.1:
dependencies:
steno: 4.0.2
match-sorter@8.0.0: match-sorter@8.0.0:
dependencies: dependencies:
'@babel/runtime': 7.27.0 '@babel/runtime': 7.27.0
@@ -2996,6 +3051,8 @@ snapshots:
semver@7.7.1: {} semver@7.7.1: {}
server-only@0.0.1: {}
set-function-length@1.2.2: set-function-length@1.2.2:
dependencies: dependencies:
define-data-property: 1.1.4 define-data-property: 1.1.4
@@ -3092,6 +3149,8 @@ snapshots:
stable-hash@0.0.5: {} stable-hash@0.0.5: {}
steno@4.0.2: {}
streamsearch@1.1.0: {} streamsearch@1.1.0: {}
string.prototype.includes@2.0.1: string.prototype.includes@2.0.1:

View File

@@ -1,57 +1,60 @@
"use client"; "use client";
import Form from "next/form";
import { ContactRecord } from "@/app/data"; import { ContactRecord } from "@/app/data";
import Form from "next/form";
import { use } from "react";
import { useRouter } from "next/navigation";
export default function Contact() { export default function Contact({
const contact = { contact
first: "Your", }: {
last: "Name", contact: Promise<ContactRecord>
avatar: "https://placecats.com/200/200", }) {
twitter: "your_handle", const c = use(contact);
notes: "Some notes", const router = useRouter();
favorite: true,
};
return ( return (
<div id="contact"> <div id="contact">
<div> <div>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
alt={`${contact.first} ${contact.last} avatar`} alt={`${c.first} ${c.last} avatar`}
key={contact.avatar} key={c.avatar}
src={contact.avatar} src={c.avatar}
/> />
</div> </div>
<div> <div>
<h1> <h1>
{contact.first || contact.last ? ( {c.first || c.last ? (
<> <>
{contact.first} {contact.last} {c.first} {c.last}
</> </>
) : ( ) : (
<i>No Name</i> <i>No Name</i>
)} )}
<Favorite contact={contact}/> <Favorite contact={c}/>
</h1> </h1>
{contact.twitter ? ( {c.twitter ? (
<p> <p>
<a <a
href={`https://twitter.com/${contact.twitter}`} href={`https://twitter.com/${c.twitter}`}
> >
{contact.twitter} {c.twitter}
</a> </a>
</p> </p>
) : null} ) : null}
{contact.notes ? <p>{contact.notes}</p> : null} {c.notes ? <p>{c.notes}</p> : null}
<div> <div>
<Form action="edit"> <button
<button type="submit">Edit</button> type="button"
</Form> onClick={() => router.push(`/contacts/${c.id}/edit`)}
>
Edit
</button>
<Form <Form
action="destroy" action="destroy"

View File

@@ -0,0 +1,85 @@
import Form from "next/form";
import { fetchContact } from "@/app/(sidebar)/loaders";
import { notFound, redirect } from "next/navigation";
import { updateContact } from "@/app/data";
async function editContact(contactId: string, formData: FormData) {
"use server";
const updates = Object.fromEntries(formData);
await updateContact(contactId, updates);
redirect(`/contacts/${contactId}`); // This makes the server component data be reloaded.
/*
* The server component architecture is a HTMX-like structure where the component rendered is STATE-LESS
* and that means that they don't automagically update.
* How do you say a server component to "re-render"? YOU REFRESH THE PAGE! THIS IS AS INTENDED!!!
* Do you want a component that re-render interactively from a "signal" client-side?
* That's a classical Client Component you dummy!! Go fetch clientside library and use that instead of Server Comps.
*/
}
export default async function EditContactPage({
params
}: {
params: Promise<{ contactId: string }>
}) {
const { contactId } = await params;
const contact = await fetchContact(contactId).then((c) => {
if (c === null) notFound();
return c;
});
const editThisContact = editContact.bind(null, contactId);
return (
<Form key={contact.id} id="contact-form" action={editThisContact}>
<p>
<span>Name</span>
<input
aria-label="First name"
defaultValue={contact.first}
name="first"
placeholder="First"
type="text"
/>
<input
aria-label="Last name"
defaultValue={contact.last}
name="last"
placeholder="Last"
type="text"
/>
</p>
<label>
<span>Twitter</span>
<input
defaultValue={contact.twitter}
name="twitter"
placeholder="@jack"
type="text"
/>
</label>
<label>
<span>Avatar URL</span>
<input
aria-label="Avatar URL"
defaultValue={contact.avatar}
name="avatar"
placeholder="https://example.com/avatar.jpg"
type="text"
/>
</label>
<label>
<span>Notes</span>
<textarea
defaultValue={contact.notes}
name="notes"
rows={6}
/>
</label>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
}

View File

@@ -0,0 +1,5 @@
export default function ContactLoading() {
return (
<div>Loading Contact...</div>
);
}

View File

@@ -0,0 +1,8 @@
export default function NotFound() {
return (
<main id="error-page">
<h1>404</h1>
<p>The requested contact could not be found.</p>
</main>
);
}

View File

@@ -0,0 +1,19 @@
import { notFound } from "next/navigation";
import Contact from "@/app/(sidebar)/contacts/[contactId]/Contact";
import { fetchContact } from "@/app/(sidebar)/loaders";
export default async function ContactPage({
params
}: {
params: Promise<{ contactId: string }>
}) {
const { contactId } = await params;
const contact = fetchContact(contactId).then((c) => {
if (c === null) notFound();
return c;
});
return (
<Contact contact={contact}/>
);
}

View File

@@ -0,0 +1,71 @@
import React, { Suspense } from "react";
import Link from "next/link";
import Form from "next/form";
import ContactList from "./sidebar-contacts";
import { createEmptyContact, getContacts } from "@/app/data";
import { revalidatePath } from "next/cache";
import { fetchContacts } from "@/app/(sidebar)/loaders";
import {
QueryClient,
dehydrate,
HydrationBoundary,
} from "@tanstack/react-query";
async function newContact() {
"use server";
await createEmptyContact();
revalidatePath('/');
}
export default async function SidebarRootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['contacts'],
queryFn: () => getContacts(),
});
/*
* The problem here was that the `fetch` that Next uses inside function called server-side is the Node one, where you
* cannot use Relative Paths!!!
* So the most simple solution was to bypass the fetch and directly call the data function here.
* Basically the API is used only for the data refresh.
*/
return (
<>
<div id="sidebar">
<h1>
<Link href="/about">React Router Contacts</Link>
</h1>
<div>
<Form id="search-form" role="search" action="." formMethod="get">
<input
aria-label="Search contacts"
id="q"
name="q"
placeholder="Search"
type="search"/>
<div
aria-hidden
hidden={true}
id="search-spinner"/>
</Form>
<Form action={newContact}>
<button type="submit">New</button>
</Form>
</div>
<nav>
<HydrationBoundary state={dehydrate(queryClient)}>
<ContactList/>
</HydrationBoundary>
</nav>
</div>
<div id={'detail'}>
{children}
</div>
</>
);
}

View File

@@ -0,0 +1,21 @@
import { ContactRecord } from "@/app/data";
export async function fetchContacts() {
console.log("fetchContacts()");
const response = await fetch('/api/contacts');
if (!response.ok) {
console.log("ERROR!?");
throw new Error(`Network Error ${response.status}: ${response.statusText}`);
}
const data: { contacts: ContactRecord[] } = await response.json();
return data.contacts;
}
export async function fetchContact(contactId: string) {
console.log("fetchContacts()");
const response = await fetch(`/api/contacts/${contactId}`);
if (!response.ok) {
throw new Error(`Network Error ${response.status}: ${response.statusText}`);
}
return (await response.json())['contact'] as ContactRecord;
}

View File

@@ -0,0 +1,15 @@
import React from "react";
export default function Home() {
return (
<p id="index-page">
This is a demo for React Router.
<br/>
Check out{" "}
<a href="https://reactrouter.com">
the docs at reactrouter.com
</a>
.
</p>
);
}

View File

@@ -0,0 +1,34 @@
"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { fetchContacts } from "@/app/(sidebar)/loaders";
export default function ContactList() {
const { data } = useQuery({
queryKey: ['contacts'],
queryFn: () => fetchContacts()
});
if (data === undefined) return <div>Error during prefetch...</div>;
return (
<ul>
{data.map((c) => (
<li key={c.id}>
<Link href={`/contacts/${c.id}`}>
{c.first || c.last ? (
<>
{c.first} {c.last}
</>
) : (
<i>No Name</i>
)}
{c.favorite ? (
<span></span>
) : null}
</Link>
</li>
))}
</ul>
);
}

45
src/app/about/page.tsx Normal file
View File

@@ -0,0 +1,45 @@
import Link from "next/link";
export default function About() {
return (
<div id="about">
<Link href="/"> Go to demo</Link>
<h1>About React Router Contacts</h1>
<div>
<p>
This is a demo application showing off some of the
powerful features of React Router, including
dynamic routing, nested routes, loaders, actions,
and more.
</p>
<h2>Features</h2>
<p>
Explore the demo to see how React Router handles:
</p>
<ul>
<li>
Data loading and mutations with loaders and
actions
</li>
<li>
Nested routing with parent/child relationships
</li>
<li>URL-based routing with dynamic segments</li>
<li>Pending and optimistic UI</li>
</ul>
<h2>Learn More</h2>
<p>
Check out the official documentation at{" "}
<a href="https://reactrouter.com">
reactrouter.com
</a>{" "}
to learn more about building great web
applications with React Router.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server';
import { getContact } from "@/app/data";
export async function GET(
{ params }: { params: Promise<{ contactId: string }> }
) {
const { contactId } = await params;
console.log(`fetch contact ${contactId}`);
const contact = await getContact(contactId);
return NextResponse.json({ contact: contact });
}

View File

@@ -0,0 +1,8 @@
import { NextResponse } from 'next/server';
import { getContacts } from "@/app/data";
export async function GET() {
console.log("fetch contacts");
const contacts = await getContacts();
return NextResponse.json({ contacts: contacts });
}

View File

@@ -2,11 +2,16 @@
// 🛑 Nothing in here has anything to do with React Router, it's just a fake database // 🛑 Nothing in here has anything to do with React Router, it's just a fake database
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// noinspection UnnecessaryLocalVariableJS,JSUnusedGlobalSymbols // noinspection UnnecessaryLocalVariableJS,JSUnusedGlobalSymbols
import "server-only";
import { matchSorter } from "match-sorter"; import { matchSorter } from "match-sorter";
// @ts-expect-error - no types, but it's a tiny function // @ts-expect-error - no types, but it's a tiny function
import sortBy from "sort-by"; import sortBy from "sort-by";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
import { join } from "path";
import { mkdir } from "fs/promises";
type ContactMutation = { type ContactMutation = {
id?: string; id?: string;
@@ -23,42 +28,97 @@ export type ContactRecord = ContactMutation & {
createdAt: string; createdAt: string;
}; };
//////////////////////////////////////////////////////////////////////////////// // Define the database schema
// This is just a fake DB table. In a real app you'd be talking to a real db or type Schema = {
// fetching from an existing API. records: Record<string, ContactRecord>;
const fakeContacts = { };
records: {} as Record<string, ContactRecord>,
// Set up lowdb
const initDb = async (): Promise<Low<Schema>> => {
// Ensure the db directory exists
const dbDir = join(process.cwd(), '.db');
await mkdir(dbDir, { recursive: true });
const file = join(dbDir, 'contacts.json');
const adapter = new JSONFile<Schema>(file);
const defaultData: Schema = { records: {} };
const db = new Low<Schema>(adapter, defaultData);
// Load existing data
await db.read();
// Initialize if needed
if (!db.data) {
db.data = defaultData;
}
return db;
};
// Create a singleton instance of the database
let dbPromise: Promise<Low<Schema>> | null = null;
const getDb = () => {
if (!dbPromise) {
dbPromise = initDb();
}
return dbPromise;
};
////////////////////////////////////////////////////////////////////////////////
// This is a DB wrapper using lowdb for persistence
const fakeContacts = {
async getAll(): Promise<ContactRecord[]> { async getAll(): Promise<ContactRecord[]> {
return Object.keys(fakeContacts.records) const db = await getDb();
.map((key) => fakeContacts.records[key]) return Object.keys(db.data.records)
.map((key) => db.data.records[key])
.sort(sortBy("-createdAt", "last")); .sort(sortBy("-createdAt", "last"));
}, },
async get(id: string): Promise<ContactRecord | null> { async get(id: string): Promise<ContactRecord | null> {
return fakeContacts.records[id] || null; const db = await getDb();
return db.data.records[id] || null;
}, },
async create(values: ContactMutation): Promise<ContactRecord> { async create(values: ContactMutation): Promise<ContactRecord> {
const db = await getDb();
const id = values.id || Math.random().toString(36).substring(2, 9); const id = values.id || Math.random().toString(36).substring(2, 9);
const createdAt = new Date().toISOString(); const createdAt = new Date().toISOString();
const newContact = { id, createdAt, ...values }; const newContact = { id, createdAt, ...values };
fakeContacts.records[id] = newContact; db.data.records[id] = newContact;
await db.write();
return newContact; return newContact;
}, },
async set(id: string, values: ContactMutation): Promise<ContactRecord> { async set(id: string, values: ContactMutation): Promise<ContactRecord> {
const contact = await fakeContacts.get(id); const db = await getDb();
const contact = await this.get(id);
invariant(contact, `No contact found for ${id}`); invariant(contact, `No contact found for ${id}`);
const updatedContact = { ...contact, ...values }; const updatedContact = { ...contact, ...values };
fakeContacts.records[id] = updatedContact; db.data.records[id] = updatedContact;
await db.write();
return updatedContact; return updatedContact;
}, },
destroy(id: string): null { async destroy(id: string): Promise<null> {
delete fakeContacts.records[id]; const db = await getDb();
delete db.data.records[id];
await db.write();
return null; return null;
}, },
// New reset function to restore the initial data
async reset(): Promise<void> {
const db = await getDb();
db.data.records = {};
await db.write();
// Restore initial data
const initialContacts = getInitialContacts();
for (const contact of initialContacts) {
await this.create(contact);
}
}
}; };
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -93,228 +153,243 @@ export async function updateContact(id: string, updates: ContactMutation) {
} }
export async function deleteContact(id: string) { export async function deleteContact(id: string) {
fakeContacts.destroy(id); await fakeContacts.destroy(id);
} }
[ // New function to reset the database to initial state
{ export async function resetDatabase() {
avatar: console.log("RESET DATABASE");
"https://sessionize.com/image/124e-400o400o2-wHVdAuNaxi8KJrgtN3ZKci.jpg", await fakeContacts.reset();
first: "Shruti", }
last: "Kapoor",
twitter: "@shrutikapoor08", // Helper function to get initial contacts data
}, function getInitialContacts() {
{ return [
avatar: {
"https://sessionize.com/image/1940-400o400o2-Enh9dnYmrLYhJSTTPSw3MH.jpg", avatar:
first: "Glenn", "https://sessionize.com/image/124e-400o400o2-wHVdAuNaxi8KJrgtN3ZKci.jpg",
last: "Reyes", first: "Shruti",
twitter: "@glnnrys", last: "Kapoor",
}, twitter: "@shrutikapoor08",
{ },
avatar: {
"https://sessionize.com/image/9273-400o400o2-3tyrUE3HjsCHJLU5aUJCja.jpg", avatar:
first: "Ryan", "https://sessionize.com/image/1940-400o400o2-Enh9dnYmrLYhJSTTPSw3MH.jpg",
last: "Florence", first: "Glenn",
}, last: "Reyes",
{ twitter: "@glnnrys",
avatar: },
"https://sessionize.com/image/d14d-400o400o2-pyB229HyFPCnUcZhHf3kWS.png", {
first: "Oscar", avatar:
last: "Newman", "https://sessionize.com/image/9273-400o400o2-3tyrUE3HjsCHJLU5aUJCja.jpg",
twitter: "@__oscarnewman", first: "Ryan",
}, last: "Florence",
{ },
avatar: {
"https://sessionize.com/image/fd45-400o400o2-fw91uCdGU9hFP334dnyVCr.jpg", avatar:
first: "Michael", "https://sessionize.com/image/d14d-400o400o2-pyB229HyFPCnUcZhHf3kWS.png",
last: "Jackson", first: "Oscar",
}, last: "Newman",
{ twitter: "@__oscarnewman",
avatar: },
"https://sessionize.com/image/b07e-400o400o2-KgNRF3S9sD5ZR4UsG7hG4g.jpg", {
first: "Christopher", avatar:
last: "Chedeau", "https://sessionize.com/image/fd45-400o400o2-fw91uCdGU9hFP334dnyVCr.jpg",
twitter: "@Vjeux", first: "Michael",
}, last: "Jackson",
{ },
avatar: {
"https://sessionize.com/image/262f-400o400o2-UBPQueK3fayaCmsyUc1Ljf.jpg", avatar:
first: "Cameron", "https://sessionize.com/image/b07e-400o400o2-KgNRF3S9sD5ZR4UsG7hG4g.jpg",
last: "Matheson", first: "Christopher",
twitter: "@cmatheson", last: "Chedeau",
}, twitter: "@Vjeux",
{ },
avatar: {
"https://sessionize.com/image/820b-400o400o2-Ja1KDrBAu5NzYTPLSC3GW8.jpg", avatar:
first: "Brooks", "https://sessionize.com/image/262f-400o400o2-UBPQueK3fayaCmsyUc1Ljf.jpg",
last: "Lybrand", first: "Cameron",
twitter: "@BrooksLybrand", last: "Matheson",
}, twitter: "@cmatheson",
{ },
avatar: {
"https://sessionize.com/image/df38-400o400o2-JwbChVUj6V7DwZMc9vJEHc.jpg", avatar:
first: "Alex", "https://sessionize.com/image/820b-400o400o2-Ja1KDrBAu5NzYTPLSC3GW8.jpg",
last: "Anderson", first: "Brooks",
twitter: "@ralex1993", last: "Lybrand",
}, twitter: "@BrooksLybrand",
{ },
avatar: {
"https://sessionize.com/image/5578-400o400o2-BMT43t5kd2U1XstaNnM6Ax.jpg", avatar:
first: "Kent C.", "https://sessionize.com/image/df38-400o400o2-JwbChVUj6V7DwZMc9vJEHc.jpg",
last: "Dodds", first: "Alex",
twitter: "@kentcdodds", last: "Anderson",
}, twitter: "@ralex1993",
{ },
avatar: {
"https://sessionize.com/image/c9d5-400o400o2-Sri5qnQmscaJXVB8m3VBgf.jpg", avatar:
first: "Nevi", "https://sessionize.com/image/5578-400o400o2-BMT43t5kd2U1XstaNnM6Ax.jpg",
last: "Shah", first: "Kent C.",
twitter: "@nevikashah", last: "Dodds",
}, twitter: "@kentcdodds",
{ },
avatar: {
"https://sessionize.com/image/2694-400o400o2-MYYTsnszbLKTzyqJV17w2q.png", avatar:
first: "Andrew", "https://sessionize.com/image/c9d5-400o400o2-Sri5qnQmscaJXVB8m3VBgf.jpg",
last: "Petersen", first: "Nevi",
}, last: "Shah",
{ twitter: "@nevikashah",
avatar: },
"https://sessionize.com/image/907a-400o400o2-9TM2CCmvrw6ttmJiTw4Lz8.jpg", {
first: "Scott", avatar:
last: "Smerchek", "https://sessionize.com/image/2694-400o400o2-MYYTsnszbLKTzyqJV17w2q.png",
twitter: "@smerchek", first: "Andrew",
}, last: "Petersen",
{ },
avatar: {
"https://sessionize.com/image/08be-400o400o2-WtYGFFR1ZUJHL9tKyVBNPV.jpg", avatar:
first: "Giovanni", "https://sessionize.com/image/907a-400o400o2-9TM2CCmvrw6ttmJiTw4Lz8.jpg",
last: "Benussi", first: "Scott",
twitter: "@giovannibenussi", last: "Smerchek",
}, twitter: "@smerchek",
{ },
avatar: {
"https://sessionize.com/image/f814-400o400o2-n2ua5nM9qwZA2hiGdr1T7N.jpg", avatar:
first: "Igor", "https://sessionize.com/image/08be-400o400o2-WtYGFFR1ZUJHL9tKyVBNPV.jpg",
last: "Minar", first: "Giovanni",
twitter: "@IgorMinar", last: "Benussi",
}, twitter: "@giovannibenussi",
{ },
avatar: {
"https://sessionize.com/image/fb82-400o400o2-LbvwhTVMrYLDdN3z4iEFMp.jpeg", avatar:
first: "Brandon", "https://sessionize.com/image/f814-400o400o2-n2ua5nM9qwZA2hiGdr1T7N.jpg",
last: "Kish", first: "Igor",
}, last: "Minar",
{ twitter: "@IgorMinar",
avatar: },
"https://sessionize.com/image/fcda-400o400o2-XiYRtKK5Dvng5AeyC8PiUA.png", {
first: "Arisa", avatar:
last: "Fukuzaki", "https://sessionize.com/image/fb82-400o400o2-LbvwhTVMrYLDdN3z4iEFMp.jpeg",
twitter: "@arisa_dev", first: "Brandon",
}, last: "Kish",
{ },
avatar: {
"https://sessionize.com/image/c8c3-400o400o2-PR5UsgApAVEADZRixV4H8e.jpeg", avatar:
first: "Alexandra", "https://sessionize.com/image/fcda-400o400o2-XiYRtKK5Dvng5AeyC8PiUA.png",
last: "Spalato", first: "Arisa",
twitter: "@alexadark", last: "Fukuzaki",
}, twitter: "@arisa_dev",
{ },
avatar: {
"https://sessionize.com/image/7594-400o400o2-hWtdCjbdFdLgE2vEXBJtyo.jpg", avatar:
first: "Cat", "https://sessionize.com/image/c8c3-400o400o2-PR5UsgApAVEADZRixV4H8e.jpeg",
last: "Johnson", first: "Alexandra",
}, last: "Spalato",
{ twitter: "@alexadark",
avatar: },
"https://sessionize.com/image/5636-400o400o2-TWgi8vELMFoB3hB9uPw62d.jpg", {
first: "Ashley", avatar:
last: "Narcisse", "https://sessionize.com/image/7594-400o400o2-hWtdCjbdFdLgE2vEXBJtyo.jpg",
twitter: "@_darkfadr", first: "Cat",
}, last: "Johnson",
{ },
avatar: {
"https://sessionize.com/image/6aeb-400o400o2-Q5tAiuzKGgzSje9ZsK3Yu5.JPG", avatar:
first: "Edmund", "https://sessionize.com/image/5636-400o400o2-TWgi8vELMFoB3hB9uPw62d.jpg",
last: "Hung", first: "Ashley",
twitter: "@_edmundhung", last: "Narcisse",
}, twitter: "@_darkfadr",
{ },
avatar: {
"https://sessionize.com/image/30f1-400o400o2-wJBdJ6sFayjKmJycYKoHSe.jpg", avatar:
first: "Clifford", "https://sessionize.com/image/6aeb-400o400o2-Q5tAiuzKGgzSje9ZsK3Yu5.JPG",
last: "Fajardo", first: "Edmund",
twitter: "@cliffordfajard0", last: "Hung",
}, twitter: "@_edmundhung",
{ },
avatar: {
"https://sessionize.com/image/6faa-400o400o2-amseBRDkdg7wSK5tjsFDiG.jpg", avatar:
first: "Erick", "https://sessionize.com/image/30f1-400o400o2-wJBdJ6sFayjKmJycYKoHSe.jpg",
last: "Tamayo", first: "Clifford",
twitter: "@ericktamayo", last: "Fajardo",
}, twitter: "@cliffordfajard0",
{ },
avatar: {
"https://sessionize.com/image/feba-400o400o2-R4GE7eqegJNFf3cQ567obs.jpg", avatar:
first: "Paul", "https://sessionize.com/image/6faa-400o400o2-amseBRDkdg7wSK5tjsFDiG.jpg",
last: "Bratslavsky", first: "Erick",
twitter: "@codingthirty", last: "Tamayo",
}, twitter: "@ericktamayo",
{ },
avatar: {
"https://sessionize.com/image/c315-400o400o2-spjM5A6VVfVNnQsuwvX3DY.jpg", avatar:
first: "Pedro", "https://sessionize.com/image/feba-400o400o2-R4GE7eqegJNFf3cQ567obs.jpg",
last: "Cattori", first: "Paul",
twitter: "@pcattori", last: "Bratslavsky",
}, twitter: "@codingthirty",
{ },
avatar: {
"https://sessionize.com/image/eec1-400o400o2-HkvWKLFqecmFxLwqR9KMRw.jpg", avatar:
first: "Andre", "https://sessionize.com/image/c315-400o400o2-spjM5A6VVfVNnQsuwvX3DY.jpg",
last: "Landgraf", first: "Pedro",
twitter: "@AndreLandgraf94", last: "Cattori",
}, twitter: "@pcattori",
{ },
avatar: {
"https://sessionize.com/image/c73a-400o400o2-4MTaTq6ftC15hqwtqUJmTC.jpg", avatar:
first: "Monica", "https://sessionize.com/image/eec1-400o400o2-HkvWKLFqecmFxLwqR9KMRw.jpg",
last: "Powell", first: "Andre",
twitter: "@indigitalcolor", last: "Landgraf",
}, twitter: "@AndreLandgraf94",
{ },
avatar: {
"https://sessionize.com/image/cef7-400o400o2-KBZUydbjfkfGACQmjbHEvX.jpeg", avatar:
first: "Brian", "https://sessionize.com/image/c73a-400o400o2-4MTaTq6ftC15hqwtqUJmTC.jpg",
last: "Lee", first: "Monica",
twitter: "@brian_dlee", last: "Powell",
}, twitter: "@indigitalcolor",
{ },
avatar: {
"https://sessionize.com/image/f83b-400o400o2-Pyw3chmeHMxGsNoj3nQmWU.jpg", avatar:
first: "Sean", "https://sessionize.com/image/cef7-400o400o2-KBZUydbjfkfGACQmjbHEvX.jpeg",
last: "McQuaid", first: "Brian",
twitter: "@SeanMcQuaidCode", last: "Lee",
}, twitter: "@brian_dlee",
{ },
avatar: {
"https://sessionize.com/image/a9fc-400o400o2-JHBnWZRoxp7QX74Hdac7AZ.jpg", avatar:
first: "Shane", "https://sessionize.com/image/f83b-400o400o2-Pyw3chmeHMxGsNoj3nQmWU.jpg",
last: "Walker", first: "Sean",
twitter: "@swalker326", last: "McQuaid",
}, twitter: "@SeanMcQuaidCode",
{ },
avatar: {
"https://sessionize.com/image/6644-400o400o2-aHnGHb5Pdu3D32MbfrnQbj.jpg", avatar:
first: "Jon", "https://sessionize.com/image/a9fc-400o400o2-JHBnWZRoxp7QX74Hdac7AZ.jpg",
last: "Jensen", first: "Shane",
twitter: "@jenseng", last: "Walker",
}, twitter: "@swalker326",
].forEach((contact) => { },
fakeContacts.create({ {
avatar:
"https://sessionize.com/image/6644-400o400o2-aHnGHb5Pdu3D32MbfrnQbj.jpg",
first: "Jon",
last: "Jensen",
twitter: "@jenseng",
},
].map(contact => ({
...contact, ...contact,
id: `${contact.first id: `${contact.first
.toLowerCase() .toLowerCase()
.split(" ") .split(" ")
.join("_")}-${contact.last.toLocaleLowerCase()}`, .join("_")}-${contact.last.toLowerCase()}`,
}); }));
}); }
// Initialize with seed data on first load
(async () => {
const db = await getDb();
if (Object.keys(db.data.records).length === 0) {
await resetDatabase();
}
})();

View File

@@ -1,9 +1,8 @@
import type { Metadata } from "next";
import React from "react"; import React from "react";
import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import Form from "next/form"; import Providers from "@/app/providers";
import Link from "next/link";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Next.js Address Book", title: "Next.js Address Book",
@@ -18,42 +17,8 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
<div id="sidebar"> <Providers>{children}</Providers>
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search" action="." formMethod="get">
<input
aria-label="Search contacts"
id="q"
name="q"
placeholder="Search"
type="search"
/>
<div
aria-hidden
hidden={true}
id="search-spinner"
/>
</Form>
<Form action="." formMethod="post">
<button type="submit">New</button>
</Form>
</div>
<nav>
<ul>
<li>
<Link href={`/contacts/1`}>Your Name</Link>
</li>
<li>
<Link href={`/contacts/2`}>Your Friend</Link>
</li>
</ul>
</nav>
</div>
<div id={'detail'}>
{children}
</div>
</body> </body>
</html> </html>
); );
} }

View File

@@ -1,8 +0,0 @@
import Form from "next/form";
import React from "react";
export default function Home() {
return (
<div></div>
);
}

49
src/app/providers.tsx Normal file
View File

@@ -0,0 +1,49 @@
'use client';
// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import React from "react";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}