diff --git a/examples/react/bi-direction-infinite-query/.gitignore b/examples/react/bi-direction-infinite-query/.gitignore
new file mode 100644
index 0000000..8f322f0
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/react/bi-direction-infinite-query/README.md b/examples/react/bi-direction-infinite-query/README.md
new file mode 100644
index 0000000..0dc9ea2
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/README.md
@@ -0,0 +1,36 @@
+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).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+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.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/examples/react/bi-direction-infinite-query/app/api/projects/route.js b/examples/react/bi-direction-infinite-query/app/api/projects/route.js
new file mode 100644
index 0000000..f00bd78
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/app/api/projects/route.js
@@ -0,0 +1,24 @@
+import { NextResponse } from 'next/server';
+
+export async function GET(request) {
+ const searchParams = request.nextUrl.searchParams;
+
+ const cursor = Number(searchParams.get('cursor') || 0);
+ const pageSize = 5;
+
+ const data = Array(pageSize)
+ .fill(0)
+ .map((_, i) => {
+ return {
+ name: 'Project ' + (i + cursor) + ` (server time: ${Date.now()})`,
+ id: i + cursor,
+ };
+ });
+
+ const nextId = cursor < 10 ? data[data.length - 1].id + 1 : null;
+ const previousId = cursor > -10 ? data[0].id - pageSize : null;
+
+ await new Promise((r) => setTimeout(r, 1000)); // Simulate delay
+
+ return NextResponse.json({ data, nextId, previousId });
+}
diff --git a/examples/react/bi-direction-infinite-query/app/layout.js b/examples/react/bi-direction-infinite-query/app/layout.js
new file mode 100644
index 0000000..cd29ec9
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/app/layout.js
@@ -0,0 +1,11 @@
+export const metadata = {
+ title: 'Dependent Queries | Floppy Disk',
+};
+
+export default function RootLayout({ children }) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/examples/react/bi-direction-infinite-query/app/page.js b/examples/react/bi-direction-infinite-query/app/page.js
new file mode 100644
index 0000000..fde7aa6
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/app/page.js
@@ -0,0 +1,60 @@
+'use client';
+
+import { createBiDirectionQuery } from 'floppy-disk';
+
+const fetchProjects = async (cursor) => {
+ const res = await fetch(`/api/projects?cursor=${cursor}`);
+ const resJson = await res.json();
+ if (res.ok) return resJson;
+ throw resJson;
+};
+
+const useProjectsQuery = createBiDirectionQuery(
+ (queryKey, { pageParam }, direction) => fetchProjects(pageParam || 0),
+ {
+ select: (response, { data = [] }, direction) => {
+ return direction === 'prev' ? response.data.concat(data) : data.concat(response.data);
+ },
+ getPrevPageParam: (response) => response.previousId,
+ getNextPageParam: (response) => response.nextId,
+ },
+);
+
+export default function BiDirectionPage() {
+ const {
+ data,
+ fetchPrevPage,
+ hasPrevPage,
+ isWaitingPrevPage,
+ fetchNextPage,
+ hasNextPage,
+ isWaitingNextPage,
+ } = useProjectsQuery();
+
+ return (
+
+ Bi-Direction Infinite Query
+
+
+
+
+ {data.map((item) => (
+ - {JSON.stringify(item)}
+ ))}
+
+
+
+ );
+}
diff --git a/examples/react/bi-direction-infinite-query/next.config.js b/examples/react/bi-direction-infinite-query/next.config.js
new file mode 100644
index 0000000..767719f
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/examples/react/bi-direction-infinite-query/package.json b/examples/react/bi-direction-infinite-query/package.json
new file mode 100644
index 0000000..73e5fd8
--- /dev/null
+++ b/examples/react/bi-direction-infinite-query/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "floppy-disk-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "floppy-disk": "^2.6.0",
+ "next": "13.5.2",
+ "react": "18.2.0",
+ "react-dom": "18.2.0"
+ }
+}
diff --git a/examples/react/dependent-queries/.gitignore b/examples/react/dependent-queries/.gitignore
new file mode 100644
index 0000000..8f322f0
--- /dev/null
+++ b/examples/react/dependent-queries/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/react/dependent-queries/README.md b/examples/react/dependent-queries/README.md
new file mode 100644
index 0000000..0dc9ea2
--- /dev/null
+++ b/examples/react/dependent-queries/README.md
@@ -0,0 +1,36 @@
+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).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+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.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/examples/react/dependent-queries/app/api/cities/route.js b/examples/react/dependent-queries/app/api/cities/route.js
new file mode 100644
index 0000000..86b8c62
--- /dev/null
+++ b/examples/react/dependent-queries/app/api/cities/route.js
@@ -0,0 +1,34 @@
+import { NextResponse } from 'next/server';
+
+import { countries, paginate } from '../data';
+
+export async function GET(request) {
+ const searchParams = request.nextUrl.searchParams;
+ const query = searchParams.get('q')?.toLocaleLowerCase();
+ const countryId = searchParams.get('countryId');
+ const provinceId = searchParams.get('provinceId');
+ if (!query || !countryId || !provinceId) {
+ return NextResponse.json(
+ { message: 'Please provide country id, province id, & query parameter' },
+ { status: 400 },
+ );
+ }
+
+ const provinces = countries.find((item) => item.id === countryId)?.provinces;
+ if (!provinces) {
+ return NextResponse.json({ message: `Country id "${countryId}" not found` }, { status: 404 });
+ }
+
+ const cities = provinces.find((item) => item.id === provinceId)?.cities;
+ if (!cities) {
+ return NextResponse.json({ message: `Province id "${provinceId}" not found` }, { status: 404 });
+ }
+
+ const { records, pagination } = paginate(
+ cities.filter((item) => item.name.toLocaleLowerCase().includes(query)),
+ );
+ return NextResponse.json({
+ records: records.map(({ id, name }) => ({ id, name })),
+ pagination,
+ });
+}
diff --git a/examples/react/dependent-queries/app/api/countries/route.js b/examples/react/dependent-queries/app/api/countries/route.js
new file mode 100644
index 0000000..e65ab3f
--- /dev/null
+++ b/examples/react/dependent-queries/app/api/countries/route.js
@@ -0,0 +1,19 @@
+import { NextResponse } from 'next/server';
+
+import { countries, paginate } from '../data';
+
+export async function GET(request) {
+ const searchParams = request.nextUrl.searchParams;
+ const query = searchParams.get('q')?.toLocaleLowerCase();
+ if (!query) {
+ return NextResponse.json({ message: 'Please provide a query parameter' }, { status: 400 });
+ }
+
+ const { records, pagination } = paginate(
+ countries.filter((item) => item.name.toLocaleLowerCase().includes(query)),
+ );
+ return NextResponse.json({
+ records: records.map(({ id, name }) => ({ id, name })),
+ pagination,
+ });
+}
diff --git a/examples/react/dependent-queries/app/api/data.js b/examples/react/dependent-queries/app/api/data.js
new file mode 100644
index 0000000..f22a4d5
--- /dev/null
+++ b/examples/react/dependent-queries/app/api/data.js
@@ -0,0 +1,31 @@
+import { faker } from '@faker-js/faker';
+
+export const paginate = (data, page = 1, limit = 10) => {
+ return {
+ records: data.slice((page - 1) * limit, page * limit),
+ pagination: {
+ currentPage: page,
+ totalPages: Math.ceil(data.length / limit),
+ totalRecords: data.length,
+ },
+ };
+};
+
+export const countries = [...Array(50)].map(() => {
+ return {
+ id: faker.string.alphanumeric(8),
+ name: faker.lorem.words(2),
+ provinces: [...Array(50)].map(() => {
+ return {
+ id: faker.string.alphanumeric(8),
+ name: faker.lorem.words(2),
+ cities: [...Array(50)].map(() => {
+ return {
+ id: faker.string.alphanumeric(8),
+ name: faker.lorem.words(2),
+ };
+ }),
+ };
+ }),
+ };
+});
diff --git a/examples/react/dependent-queries/app/api/provinces/route.js b/examples/react/dependent-queries/app/api/provinces/route.js
new file mode 100644
index 0000000..40c2b99
--- /dev/null
+++ b/examples/react/dependent-queries/app/api/provinces/route.js
@@ -0,0 +1,28 @@
+import { NextResponse } from 'next/server';
+
+import { countries, paginate } from '../data';
+
+export async function GET(request) {
+ const searchParams = request.nextUrl.searchParams;
+ const query = searchParams.get('q')?.toLocaleLowerCase();
+ const countryId = searchParams.get('countryId');
+ if (!query || !countryId) {
+ return NextResponse.json(
+ { message: 'Please provide country id & query parameter' },
+ { status: 400 },
+ );
+ }
+
+ const provinces = countries.find((item) => item.id === countryId)?.provinces;
+ if (!provinces) {
+ return NextResponse.json({ message: `Country id "${countryId}" not found` }, { status: 404 });
+ }
+
+ const { records, pagination } = paginate(
+ provinces.filter((item) => item.name.toLocaleLowerCase().includes(query)),
+ );
+ return NextResponse.json({
+ records: records.map(({ id, name }) => ({ id, name })),
+ pagination,
+ });
+}
diff --git a/examples/react/dependent-queries/app/layout.js b/examples/react/dependent-queries/app/layout.js
new file mode 100644
index 0000000..cd29ec9
--- /dev/null
+++ b/examples/react/dependent-queries/app/layout.js
@@ -0,0 +1,11 @@
+export const metadata = {
+ title: 'Dependent Queries | Floppy Disk',
+};
+
+export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/react/dependent-queries/app/page.js b/examples/react/dependent-queries/app/page.js
new file mode 100644
index 0000000..b598c3c
--- /dev/null
+++ b/examples/react/dependent-queries/app/page.js
@@ -0,0 +1,85 @@
+'use client';
+
+import { useState } from 'react';
+import { useDebounce } from 'react-power-ups';
+
+import { useCitiesQuery, useCountriesQuery, useProvincesQuery } from './queries';
+
+export default function AddressForm() {
+ const [country, setCountry] = useState();
+ const [province, setProvince] = useState();
+ const [city, setCity] = useState();
+
+ return (
+
+ Dependent Queries Example
+
+
+
+
+
+ );
+}
+
+function Dropdown({ title, value, onChange, useQuery, queryParams }) {
+ const [keyword, setKeyword] = useState('');
+ const debouncedKeyword = useDebounce(keyword, 2000);
+ const query = useQuery({ q: debouncedKeyword, ...queryParams });
+ return (
+
+
{title}
+
Selected: {JSON.stringify(value) || '-'}
+
+ setKeyword(e.target.value)}
+ />
+
+ {!!keyword &&
}
+
+ );
+}
+
+function DropdownMenu({ query, onChange }) {
+ const { isLoading, isSuccess, error, data = [] } = query;
+
+ if (isLoading) {
+ return Loading... ⏳
;
+ }
+
+ if (isSuccess) {
+ return data.length ? (
+
+ ) : (
+ No data found
+ );
+ }
+
+ return Error: {JSON.stringify(error, null, 2)}
;
+}
diff --git a/examples/react/dependent-queries/app/queries.js b/examples/react/dependent-queries/app/queries.js
new file mode 100644
index 0000000..98a38e7
--- /dev/null
+++ b/examples/react/dependent-queries/app/queries.js
@@ -0,0 +1,37 @@
+import { createQuery } from 'floppy-disk';
+
+const getCountries = async ({ q }) => {
+ const res = await fetch(`/api/countries?q=${encodeURIComponent(q)}`);
+ const resJson = await res.json();
+ if (res.ok) return resJson;
+ throw resJson;
+};
+
+const getProvinces = async ({ q, countryId }) => {
+ const res = await fetch(`/api/provinces?q=${encodeURIComponent(q)}&countryId=${countryId}`);
+ const resJson = await res.json();
+ if (res.ok) return resJson;
+ throw resJson;
+};
+
+const getCities = async ({ q, countryId, provinceId }) => {
+ const res = await fetch(
+ `/api/countries?q=${encodeURIComponent(q)}&countryId=${countryId}&provinceId=${provinceId}`,
+ );
+ const resJson = await res.json();
+ if (res.ok) return resJson;
+ throw resJson;
+};
+
+export const useCountriesQuery = createQuery(getCountries, {
+ select: (response) => response.records,
+ enabled: ({ q }) => q,
+});
+export const useProvincesQuery = createQuery(getProvinces, {
+ select: (response) => response.records,
+ enabled: ({ q, countryId }) => q && countryId,
+});
+export const useCitiesQuery = createQuery(getCities, {
+ select: (response) => response.records,
+ enabled: ({ q, countryId, provinceId }) => q && countryId && provinceId,
+});
diff --git a/examples/react/dependent-queries/next.config.js b/examples/react/dependent-queries/next.config.js
new file mode 100644
index 0000000..767719f
--- /dev/null
+++ b/examples/react/dependent-queries/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/examples/react/dependent-queries/package.json b/examples/react/dependent-queries/package.json
new file mode 100644
index 0000000..dce9b16
--- /dev/null
+++ b/examples/react/dependent-queries/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "floppy-disk-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@faker-js/faker": "^8.1.0",
+ "floppy-disk": "^2.5.0",
+ "next": "13.5.2",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-power-ups": "^3.1.3"
+ }
+}