diff --git a/remix-infinite-scroll/.eslintrc.cjs b/remix-infinite-scroll/.eslintrc.cjs new file mode 100644 index 00000000..4f6f59ee --- /dev/null +++ b/remix-infinite-scroll/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/remix-infinite-scroll/.gitignore b/remix-infinite-scroll/.gitignore new file mode 100644 index 00000000..80ec311f --- /dev/null +++ b/remix-infinite-scroll/.gitignore @@ -0,0 +1,5 @@ +node_modules + +/.cache +/build +.env diff --git a/remix-infinite-scroll/README.md b/remix-infinite-scroll/README.md new file mode 100644 index 00000000..940a8295 --- /dev/null +++ b/remix-infinite-scroll/README.md @@ -0,0 +1,38 @@ +# Remix Infinite Query Example + +A demonstration of infinite scroll implementation in Remix using React Query, Loaders, and Defer for optimal performance and user experience. + +## Preview + +Open this example on [CodeSandbox](https://codesandbox.com): + + + +[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/remix-infinite-scroll) + +## Example + +A demonstration of infinite scroll implementation in Remix using React Query, Loaders, and Defer for optimal performance and user experience. + +๐Ÿ“ Project Structure + +``` +app/ +โ”œโ”€โ”€ components/ # React components +โ”‚ โ”œโ”€โ”€ LoadingState.tsx +โ”‚ โ”œโ”€โ”€ PostCard.tsx +โ”‚ โ”œโ”€โ”€ PostSkeleton.tsx +โ”‚ โ””โ”€โ”€ PostsList.tsx +โ”œโ”€โ”€ hooks/ # Custom hooks +โ”‚ โ”œโ”€โ”€ useIntersectionObserver.ts +โ”‚ โ””โ”€โ”€ usePosts.ts +โ”œโ”€โ”€ routes/ # Remix routes +โ”‚ โ”œโ”€โ”€ _index.tsx +โ”‚ โ””โ”€โ”€ api.posts.ts +โ””โ”€โ”€ types/ # TypeScript types + โ””โ”€โ”€ post.ts +``` + +## Related Links + +[remix-loader-infinite-useQuery-bestpractice](https://github.com/Amateur0x1/remix-loader-infinite-useQuery-bestpractice) \ No newline at end of file diff --git a/remix-infinite-scroll/app/components/LoadingState.tsx b/remix-infinite-scroll/app/components/LoadingState.tsx new file mode 100644 index 00000000..1d39aedf --- /dev/null +++ b/remix-infinite-scroll/app/components/LoadingState.tsx @@ -0,0 +1,24 @@ +import { PostSkeleton } from './PostSkeleton'; + +export function LoadingState() { + return ( +
+
้ฆ–ๆฌกๅŠ ่ฝฝไธญ...
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ); +} + + +export function LoadingState2() { + return ( +
+
ไน‹ๅŽ็š„ๅŠ ่ฝฝ...
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/components/PostCard.tsx b/remix-infinite-scroll/app/components/PostCard.tsx new file mode 100644 index 00000000..f769dd18 --- /dev/null +++ b/remix-infinite-scroll/app/components/PostCard.tsx @@ -0,0 +1,11 @@ +import type { Post } from '~/types/post'; + +export function PostCard({ post }: { post: Post }) { + return ( +
+

{post.title}

+

{post.content}

+

By {post.author}

+
+ ); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/components/PostSkeleton.tsx b/remix-infinite-scroll/app/components/PostSkeleton.tsx new file mode 100644 index 00000000..62dde767 --- /dev/null +++ b/remix-infinite-scroll/app/components/PostSkeleton.tsx @@ -0,0 +1,9 @@ +export function PostSkeleton() { + return ( +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/components/PostsList.tsx b/remix-infinite-scroll/app/components/PostsList.tsx new file mode 100644 index 00000000..f0a49956 --- /dev/null +++ b/remix-infinite-scroll/app/components/PostsList.tsx @@ -0,0 +1,18 @@ +import type { Post } from '~/types/post'; + +interface PostsListProps { + posts: Post[]; +} + +export function PostsList({ posts }: PostsListProps) { + return ( +
+ {posts.map((post) => ( +
+

{post.title}

+

{post.content}

+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/entry.client.tsx b/remix-infinite-scroll/app/entry.client.tsx new file mode 100644 index 00000000..94d5dc0d --- /dev/null +++ b/remix-infinite-scroll/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` โœจ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/remix-infinite-scroll/app/entry.server.tsx b/remix-infinite-scroll/app/entry.server.tsx new file mode 100644 index 00000000..45db3229 --- /dev/null +++ b/remix-infinite-scroll/app/entry.server.tsx @@ -0,0 +1,140 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` โœจ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/remix-infinite-scroll/app/hooks/useIntersectionObserver.ts b/remix-infinite-scroll/app/hooks/useIntersectionObserver.ts new file mode 100644 index 00000000..c60e25c9 --- /dev/null +++ b/remix-infinite-scroll/app/hooks/useIntersectionObserver.ts @@ -0,0 +1,46 @@ +import { useEffect, RefObject, useCallback } from 'react'; + +interface UseIntersectionObserverProps { + target: RefObject; + onIntersect: () => void; + enabled?: boolean; + threshold?: number | number[]; + rootMargin?: string; + root?: Element | null; +} + +export function useIntersectionObserver({ + target, + onIntersect, + enabled = true, + threshold = 0.1, + rootMargin = '0px', + root = null, +}: UseIntersectionObserverProps) { + const handleIntersect = useCallback((entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + onIntersect(); + } + }); + }, [onIntersect]); + + useEffect(() => { + if (!enabled) return; + + const element = target.current; + if (!element) return; + + const observer = new IntersectionObserver(handleIntersect, { + threshold, + rootMargin, + root, + }); + + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [target, enabled, handleIntersect, threshold, rootMargin, root]); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/hooks/usePosts.ts b/remix-infinite-scroll/app/hooks/usePosts.ts new file mode 100644 index 00000000..fd8561f3 --- /dev/null +++ b/remix-infinite-scroll/app/hooks/usePosts.ts @@ -0,0 +1,34 @@ +import { useInfiniteQuery, type UseInfiniteQueryOptions } from '@tanstack/react-query'; +import type { InfiniteData } from '@tanstack/react-query'; +import type { Post } from '~/types/post'; + +export type PostsResponse = { + posts: Post[]; + nextPage: number | null; +}; + +export const fetchPosts = async ({ pageParam = 1 }: { pageParam?: unknown } = {}): Promise => { + await new Promise(resolve => setTimeout(resolve, 1000)); + + const response = await fetch(`/api/posts?page=${Number(pageParam)}&limit=10`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); +}; + +type UsePostsOptions = Partial>>; + +export function usePosts(options?: UsePostsOptions) { + return useInfiniteQuery({ + queryKey: ['posts'], + queryFn: fetchPosts, + initialPageParam: 1, + getNextPageParam: (lastPage) => lastPage.nextPage, + retry: 3, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + ...options, + }); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/root.tsx b/remix-infinite-scroll/app/root.tsx new file mode 100644 index 00000000..8490894e --- /dev/null +++ b/remix-infinite-scroll/app/root.tsx @@ -0,0 +1,51 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import type { LinksFunction } from "@remix-run/node"; +import { QueryClientProvider } from '@tanstack/react-query'; +import { queryClient } from '~/utils/query-client'; + +import "./tailwind.css"; + +export const links: LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ( + + + + ); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/routes/_index.tsx b/remix-infinite-scroll/app/routes/_index.tsx new file mode 100644 index 00000000..bb023a95 --- /dev/null +++ b/remix-infinite-scroll/app/routes/_index.tsx @@ -0,0 +1,76 @@ +import { defer } from '@remix-run/node'; +import type { LoaderFunctionArgs } from '@remix-run/node'; +import { Await, useLoaderData } from '@remix-run/react'; +import { Suspense, useCallback, useRef } from 'react'; +import { LoadingState, LoadingState2 } from '~/components/LoadingState'; +import { PostsList } from '~/components/PostsList'; +import { getPosts } from '~/utils/api'; +import type { Post } from '~/types/post'; +import { usePosts, type PostsResponse } from '~/hooks/usePosts'; +import { useIntersectionObserver } from '~/hooks/useIntersectionObserver'; + +export async function loader({ request }: LoaderFunctionArgs) { + const postsPromise = getPosts(); + + return defer({ + posts: postsPromise + }); +} + +export default function Index() { + const { posts: initialPostsPromise } = useLoaderData(); + const loadMoreRef = useRef(null); + + return ( +
+

Blog Posts

+ }> + + {(initialPosts: Post[]) => { + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = usePosts({ + initialData: { + pages: [{ posts: initialPosts, nextPage: 2 }], + pageParams: [1] + } + }); + + useIntersectionObserver({ + target: loadMoreRef, + onIntersect: () => { + if (!isFetchingNextPage) { + fetchNextPage(); + } + }, + enabled: !!hasNextPage + }); + + const posts = data?.pages?.flatMap((page) => page.posts) ?? []; + + return ( + <> + + {hasNextPage && ( +
+ {isFetchingNextPage ? ( + + ) : ( + Load more posts... + )} +
+ )} + + ); + }} +
+
+
+ ); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/routes/api.posts.ts b/remix-infinite-scroll/app/routes/api.posts.ts new file mode 100644 index 00000000..b1ae30bf --- /dev/null +++ b/remix-infinite-scroll/app/routes/api.posts.ts @@ -0,0 +1,25 @@ +import { json } from '@remix-run/node'; +import type { LoaderFunctionArgs } from '@remix-run/node'; + +export async function loader({ request }: LoaderFunctionArgs) { + const url = new URL(request.url); + const page = Number(url.searchParams.get('page')) || 1; + const limit = Number(url.searchParams.get('limit')) || 10; + + // ๆจกๆ‹ŸไปŽๆ•ฐๆฎๅบ“่Žทๅ–ๆ•ฐๆฎ + const start = (page - 1) * limit; + const posts = Array.from({ length: limit }, (_, i) => ({ + id: start + i + 1, + title: `Post ${start + i + 1}`, + content: `Content ${start + i + 1}`, + author: 'Author' + })); + + // ๆจกๆ‹Ÿๆ€ปๅ…ฑๆœ‰50็ฏ‡ๆ–‡็ซ  + const hasMore = start + limit < 50; + + return json({ + posts, + nextPage: hasMore ? page + 1 : null + }); +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/tailwind.css b/remix-infinite-scroll/app/tailwind.css new file mode 100644 index 00000000..303fe158 --- /dev/null +++ b/remix-infinite-scroll/app/tailwind.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/remix-infinite-scroll/app/types/post.ts b/remix-infinite-scroll/app/types/post.ts new file mode 100644 index 00000000..a541a4a7 --- /dev/null +++ b/remix-infinite-scroll/app/types/post.ts @@ -0,0 +1,6 @@ +export interface Post { + id: number; + title: string; + content: string; + author: string; +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/utils/api.ts b/remix-infinite-scroll/app/utils/api.ts new file mode 100644 index 00000000..125b22cc --- /dev/null +++ b/remix-infinite-scroll/app/utils/api.ts @@ -0,0 +1,17 @@ +import type { Post } from '~/types/post'; + +// Simulate API delay +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +// Mock data +const posts: Post[] = Array.from({ length: 10 }, (_, i) => ({ + id: i + 1, + title: `Post ${i + 1}`, + content: `This is the content for post ${i + 1}`, + author: `Author ${i + 1}` +})); + +export async function getPosts(): Promise { + await delay(2000); // Simulate network delay + return posts; +} \ No newline at end of file diff --git a/remix-infinite-scroll/app/utils/query-client.ts b/remix-infinite-scroll/app/utils/query-client.ts new file mode 100644 index 00000000..f24ea297 --- /dev/null +++ b/remix-infinite-scroll/app/utils/query-client.ts @@ -0,0 +1,10 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10000, + refetchOnWindowFocus: false, + }, + }, +}); \ No newline at end of file diff --git a/remix-infinite-scroll/package.json b/remix-infinite-scroll/package.json new file mode 100644 index 00000000..4ff0adf3 --- /dev/null +++ b/remix-infinite-scroll/package.json @@ -0,0 +1,44 @@ +{ + "name": "my-remix-app", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", + "typecheck": "tsc" + }, + "dependencies": { + "@remix-run/node": "^2.15.2", + "@remix-run/react": "^2.15.2", + "@remix-run/serve": "^2.15.2", + "@tanstack/react-query": "^5.28.4", + "isbot": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@remix-run/dev": "^2.15.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.19", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + } +} \ No newline at end of file diff --git a/remix-infinite-scroll/postcss.config.js b/remix-infinite-scroll/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/remix-infinite-scroll/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/remix-infinite-scroll/public/favicon.ico b/remix-infinite-scroll/public/favicon.ico new file mode 100644 index 00000000..8830cf68 Binary files /dev/null and b/remix-infinite-scroll/public/favicon.ico differ diff --git a/remix-infinite-scroll/public/logo-dark.png b/remix-infinite-scroll/public/logo-dark.png new file mode 100644 index 00000000..b24c7aee Binary files /dev/null and b/remix-infinite-scroll/public/logo-dark.png differ diff --git a/remix-infinite-scroll/public/logo-light.png b/remix-infinite-scroll/public/logo-light.png new file mode 100644 index 00000000..4490ae79 Binary files /dev/null and b/remix-infinite-scroll/public/logo-light.png differ diff --git a/remix-infinite-scroll/sandbox.config.json b/remix-infinite-scroll/sandbox.config.json new file mode 100644 index 00000000..f92e0250 --- /dev/null +++ b/remix-infinite-scroll/sandbox.config.json @@ -0,0 +1,7 @@ +{ + "hardReloadOnChange": true, + "template": "remix", + "container": { + "port": 3000 + } +} diff --git a/remix-infinite-scroll/tailwind.config.ts b/remix-infinite-scroll/tailwind.config.ts new file mode 100644 index 00000000..5f06ad4b --- /dev/null +++ b/remix-infinite-scroll/tailwind.config.ts @@ -0,0 +1,22 @@ +import type { Config } from "tailwindcss"; + +export default { + content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + fontFamily: { + sans: [ + "Inter", + "ui-sans-serif", + "system-ui", + "sans-serif", + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji", + ], + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/remix-infinite-scroll/tsconfig.json b/remix-infinite-scroll/tsconfig.json new file mode 100644 index 00000000..9d87dd37 --- /dev/null +++ b/remix-infinite-scroll/tsconfig.json @@ -0,0 +1,32 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } +} diff --git a/remix-infinite-scroll/vite.config.ts b/remix-infinite-scroll/vite.config.ts new file mode 100644 index 00000000..e4e8cefc --- /dev/null +++ b/remix-infinite-scroll/vite.config.ts @@ -0,0 +1,24 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +declare module "@remix-run/node" { + interface Future { + v3_singleFetch: true; + } +} + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + v3_lazyRouteDiscovery: true, + }, + }), + tsconfigPaths(), + ], +});