├── minimal ├── .gitignore ├── public │ └── favicon.ico ├── src │ ├── global.ts │ ├── types │ │ ├── react.ts │ │ ├── index.ts │ │ └── react-modules.d.ts │ ├── runtime-server.ts │ ├── routes │ │ ├── _client.tsx │ │ └── page.tsx │ ├── runtime-client.ts │ ├── entry-server.tsx │ ├── entry-browser.tsx │ └── entry-ssr.tsx ├── README.md ├── tsconfig.json ├── package.json └── vite.config.ts ├── remix-tutorial ├── .gitignore ├── .prettierignore ├── public │ └── favicon.ico ├── src │ ├── adapters │ │ ├── cloudflare-workers.ts │ │ └── node.ts │ └── routes │ │ ├── contacts │ │ └── [contactId] │ │ │ ├── edit │ │ │ ├── _client.tsx │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── _action.ts │ │ ├── page.tsx │ │ ├── _client.tsx │ │ ├── layout.tsx │ │ └── _data.ts ├── misc │ └── cloudflare-workers │ │ ├── README.md │ │ ├── wrangler.toml │ │ └── build.sh ├── tsconfig.json ├── README.md ├── vite.config.ts └── package.json ├── README.md ├── vercel-app-playground ├── .gitignore ├── .prettierignore ├── src │ ├── entry-server.tsx │ ├── styles │ │ └── globals.css │ ├── entry-react-server.tsx │ ├── adapters │ │ ├── vercel-edge.ts │ │ └── node.ts │ ├── api │ │ ├── reviews │ │ │ ├── review.ts │ │ │ └── getReviews.ts │ │ ├── categories │ │ │ ├── category.ts │ │ │ └── getCategories.ts │ │ └── products │ │ │ └── product.ts │ ├── entry-client.tsx │ ├── routes │ │ ├── patterns │ │ │ ├── layout.tsx │ │ │ ├── search-params │ │ │ │ ├── active-link.tsx │ │ │ │ ├── client.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── streaming │ │ │ ├── _action.ts │ │ │ ├── _components │ │ │ │ ├── cart-count.tsx │ │ │ │ ├── cart-count-context.tsx │ │ │ │ ├── add-to-cart.tsx │ │ │ │ ├── reviews.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── recommended-products.tsx │ │ │ │ ├── single-product.tsx │ │ │ │ └── pricing.tsx │ │ │ ├── layout.tsx │ │ │ ├── node │ │ │ │ ├── layout.tsx │ │ │ │ └── product │ │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── not-found │ │ │ ├── error.tsx │ │ │ ├── [categorySlug] │ │ │ │ ├── error.tsx │ │ │ │ ├── [subCategorySlug] │ │ │ │ │ ├── error.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── loading │ │ │ ├── _loading.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── [categorySlug] │ │ │ │ └── page.tsx │ │ ├── error-handling │ │ │ ├── error.tsx │ │ │ ├── [categorySlug] │ │ │ │ ├── error.tsx │ │ │ │ ├── [subCategorySlug] │ │ │ │ │ ├── error.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── styling │ │ │ ├── global-css │ │ │ │ ├── page.tsx │ │ │ │ └── styles.css │ │ │ ├── page.tsx │ │ │ ├── css-modules │ │ │ │ ├── page.tsx │ │ │ │ └── styles.module.css │ │ │ ├── tailwind │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── context │ │ │ ├── [categorySlug] │ │ │ │ ├── page.tsx │ │ │ │ ├── [subCategorySlug] │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── counter-context.tsx │ │ │ ├── page.tsx │ │ │ ├── context-click-counter.tsx │ │ │ └── layout.tsx │ │ ├── layouts │ │ │ ├── [categorySlug] │ │ │ │ ├── page.tsx │ │ │ │ ├── [subCategorySlug] │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── page.tsx │ │ │ └── layout.tsx │ │ ├── ssg │ │ │ ├── layout.tsx │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── ui │ │ ├── product-best-seller.tsx │ │ ├── hydrated.tsx │ │ ├── ping.tsx │ │ ├── product-low-stock-warning.tsx │ │ ├── click-counter.tsx │ │ ├── count-up.tsx │ │ ├── buggy-button.tsx │ │ ├── product-rating.tsx │ │ ├── section-link.tsx │ │ ├── external-link.tsx │ │ ├── product-currency-symbol.tsx │ │ ├── button.tsx │ │ ├── product-split-payments.tsx │ │ ├── product-used-price.tsx │ │ ├── tab-nav-item.tsx │ │ ├── product-estimated-arrival.tsx │ │ ├── skeleton-card.tsx │ │ ├── tab-group.tsx │ │ ├── product-review-card.tsx │ │ ├── product-lightening-deal.tsx │ │ ├── rendering-info.tsx │ │ ├── rendering-page-skeleton.tsx │ │ ├── tab.tsx │ │ ├── product-deal.tsx │ │ ├── byline.tsx │ │ ├── product-price.tsx │ │ ├── rendered-time-ago.tsx │ │ ├── vercel-logo.tsx │ │ ├── mobile-nav-toggle.tsx │ │ ├── header.tsx │ │ ├── footer.tsx │ │ ├── product-card.tsx │ │ ├── boundary.tsx │ │ ├── global-nav.tsx │ │ ├── address-bar.tsx │ │ ├── next-logo.tsx │ │ └── component-tree.tsx │ └── lib │ │ └── demos.ts ├── misc │ └── vercel-edge │ │ ├── .vc-config.json │ │ ├── README.md │ │ ├── config.json │ │ └── build.sh ├── public │ ├── favicon.ico │ ├── nextjs-icon-light-background.png │ ├── patrick-OIFgeLnjwrM-unsplash.jpg │ ├── eniko-kis-KsLPTsYaqIQ-unsplash.jpg │ ├── prince-akachi-LWkFHEGpleE-unsplash.jpg │ ├── yoann-siloine-_T4w3JDm6ug-unsplash.jpg │ ├── alexander-andrews-brAkTCdnhW8-unsplash.jpg │ ├── guillaume-coupy-6HuoHgK7FN8-unsplash.jpg │ └── grid.svg ├── postcss.config.cjs ├── vercel.json ├── prettier.config.cjs ├── e2e │ ├── helper.ts │ └── basic.test.ts ├── tsconfig.json ├── vite.config.ts ├── playwright.config.ts ├── README.md ├── package.json └── tailwind.config.ts ├── next-ts-tw ├── app │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── next.config.mjs ├── vite.config.ts ├── postcss.config.mjs ├── tailwind.config.ts ├── .gitignore ├── tsconfig.json ├── package.json └── README.md └── .github └── workflows └── ci.yml /minimal/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /remix-tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rsc on vite 2 | 3 | Various demo to run React server component on Vite 4 | -------------------------------------------------------------------------------- /vercel-app-playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vercel 4 | test-results 5 | -------------------------------------------------------------------------------- /vercel-app-playground/.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist 3 | .vercel 4 | test-results 5 | -------------------------------------------------------------------------------- /remix-tutorial/.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | node_modules 3 | dist 4 | .vercel 5 | .wrangler 6 | -------------------------------------------------------------------------------- /minimal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/minimal/public/favicon.ico -------------------------------------------------------------------------------- /next-ts-tw/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/next-ts-tw/app/favicon.ico -------------------------------------------------------------------------------- /vercel-app-playground/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | export { handler } from '@hiogawa/react-server/entry-server'; 2 | -------------------------------------------------------------------------------- /vercel-app-playground/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /next-ts-tw/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/next-ts-tw/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /remix-tutorial/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/remix-tutorial/public/favicon.ico -------------------------------------------------------------------------------- /vercel-app-playground/misc/vercel-edge/.vc-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime": "edge", 3 | "entrypoint": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /vercel-app-playground/src/entry-react-server.tsx: -------------------------------------------------------------------------------- 1 | export { handler } from '@hiogawa/react-server/entry-react-server'; 2 | -------------------------------------------------------------------------------- /next-ts-tw/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/next-ts-tw/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /vercel-app-playground/src/adapters/vercel-edge.ts: -------------------------------------------------------------------------------- 1 | import { handler } from '../entry-server'; 2 | 3 | export default handler; 4 | -------------------------------------------------------------------------------- /next-ts-tw/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /vercel-app-playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/favicon.ico -------------------------------------------------------------------------------- /vercel-app-playground/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /vercel-app-playground/src/api/reviews/review.ts: -------------------------------------------------------------------------------- 1 | export type Review = { 2 | id: string; 3 | name: string; 4 | rating: number; 5 | text: string; 6 | }; 7 | -------------------------------------------------------------------------------- /vercel-app-playground/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import './styles/globals.css'; 2 | import { start } from '@hiogawa/react-server/entry-browser'; 3 | 4 | start(); 5 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/patterns/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: { children: React.ReactNode }) { 2 | return children; 3 | } 4 | -------------------------------------------------------------------------------- /vercel-app-playground/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "version": 2, 4 | "buildCommand": "pnpm vc-build" 5 | } 6 | -------------------------------------------------------------------------------- /remix-tutorial/src/adapters/cloudflare-workers.ts: -------------------------------------------------------------------------------- 1 | import { handler } from "@hiogawa/react-server/entry/ssr"; 2 | 3 | export default { 4 | fetch: handler, 5 | }; 6 | -------------------------------------------------------------------------------- /next-ts-tw/vite.config.ts: -------------------------------------------------------------------------------- 1 | import next from "next/vite"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | plugins: [next()], 6 | }); 7 | -------------------------------------------------------------------------------- /remix-tutorial/misc/cloudflare-workers/README.md: -------------------------------------------------------------------------------- 1 | copied from https://github.com/hi-ogawa/vite-plugins/tree/main/packages/react-server/examples/basic/misc/cloudflare-workers 2 | -------------------------------------------------------------------------------- /vercel-app-playground/public/nextjs-icon-light-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/nextjs-icon-light-background.png -------------------------------------------------------------------------------- /vercel-app-playground/public/patrick-OIFgeLnjwrM-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/patrick-OIFgeLnjwrM-unsplash.jpg -------------------------------------------------------------------------------- /vercel-app-playground/src/api/categories/category.ts: -------------------------------------------------------------------------------- 1 | export type Category = { 2 | name: string; 3 | slug: string; 4 | count: number; 5 | parent: string | null; 6 | }; 7 | -------------------------------------------------------------------------------- /vercel-app-playground/public/eniko-kis-KsLPTsYaqIQ-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/eniko-kis-KsLPTsYaqIQ-unsplash.jpg -------------------------------------------------------------------------------- /vercel-app-playground/public/prince-akachi-LWkFHEGpleE-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/prince-akachi-LWkFHEGpleE-unsplash.jpg -------------------------------------------------------------------------------- /vercel-app-playground/public/yoann-siloine-_T4w3JDm6ug-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/yoann-siloine-_T4w3JDm6ug-unsplash.jpg -------------------------------------------------------------------------------- /next-ts-tw/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 | -------------------------------------------------------------------------------- /vercel-app-playground/public/alexander-andrews-brAkTCdnhW8-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/alexander-andrews-brAkTCdnhW8-unsplash.jpg -------------------------------------------------------------------------------- /vercel-app-playground/public/guillaume-coupy-6HuoHgK7FN8-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hi-ogawa/rsc-on-vite/HEAD/vercel-app-playground/public/guillaume-coupy-6HuoHgK7FN8-unsplash.jpg -------------------------------------------------------------------------------- /vercel-app-playground/src/adapters/node.ts: -------------------------------------------------------------------------------- 1 | import { handler } from '../entry-server'; 2 | import { webToNodeHandler } from '@hiogawa/utils-node'; 3 | 4 | export default webToNodeHandler(handler); 5 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/_action.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | export let cartCount = 0; 4 | 5 | export async function addCartCount() { 6 | cartCount++; 7 | return cartCount; 8 | } 9 | -------------------------------------------------------------------------------- /remix-tutorial/src/adapters/node.ts: -------------------------------------------------------------------------------- 1 | import { handler } from "@hiogawa/react-server/entry/ssr"; 2 | import { webToNodeHandler } from "@hiogawa/utils-node"; 3 | 4 | export default webToNodeHandler(handler); 5 | -------------------------------------------------------------------------------- /remix-tutorial/src/routes/contacts/[contactId]/edit/_client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export function BackButton(props: JSX.IntrinsicElements["button"]) { 4 | return 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /next-ts-tw/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/count-up.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCountUp } from 'use-count-up'; 4 | 5 | const CountUp = ({ 6 | start, 7 | end, 8 | duration = 1, 9 | }: { 10 | start: number; 11 | end: number; 12 | duration?: number; 13 | }) => { 14 | const { value } = useCountUp({ 15 | isCounting: true, 16 | end, 17 | start, 18 | duration, 19 | decimalPlaces: 1, 20 | }); 21 | 22 | return {value}; 23 | }; 24 | 25 | export default CountUp; 26 | -------------------------------------------------------------------------------- /minimal/src/runtime-server.ts: -------------------------------------------------------------------------------- 1 | import ReactServer from "react-server-dom-webpack/server.edge"; 2 | 3 | export function registerClientReference(id: string, name: string) { 4 | // use async module + meme import 5 | // so we don't have to deal with preloadModule cache for now 6 | return Object.defineProperties( 7 | {}, 8 | { 9 | ...Object.getOwnPropertyDescriptors( 10 | ReactServer.registerClientReference({}, id, name), 11 | ), 12 | $$async: { value: true }, 13 | }, 14 | ) as any; 15 | } 16 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/[categorySlug]/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Boundary } from '#/ui/boundary'; 4 | 5 | export default function NotFound() { 6 | return ( 7 | 8 |
9 |

Category Not Found

10 | 11 |

Could not find requested resource

12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /remix-tutorial/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "skipLibCheck": true, 8 | "verbatimModuleSyntax": true, 9 | "noEmit": true, 10 | "moduleResolution": "Bundler", 11 | "module": "ESNext", 12 | "target": "ESNext", 13 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 14 | "types": ["vite/client", "react/experimental"], 15 | "jsx": "react-jsx" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/buggy-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Button from '#/ui/button'; 4 | import React from 'react'; 5 | 6 | export default function BuggyButton() { 7 | const [clicked, setClicked] = React.useState(false); 8 | 9 | if (clicked) { 10 | throw new Error('Oh no! Something went wrong.'); 11 | } 12 | 13 | return ( 14 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /minimal/src/routes/_client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | export function Counter() { 6 | const [count, setCount] = React.useState(0); 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | export function Hydrated() { 15 | const hydrated = React.useSyncExternalStore( 16 | React.useCallback(() => () => {}, []), 17 | () => true, 18 | () => false, 19 | ); 20 | return
hydrated: {Number(hydrated)}
; 21 | } 22 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-rating.tsx: -------------------------------------------------------------------------------- 1 | import { StarIcon } from '@heroicons/react/24/solid'; 2 | import clsx from 'clsx'; 3 | 4 | export const ProductRating = ({ rating }: { rating: number }) => { 5 | return ( 6 |
7 | {Array.from({ length: 5 }).map((_, i) => { 8 | return ( 9 | 13 | ); 14 | })} 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /remix-tutorial/src/routes/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 |

4 | This is a demo for{" "} 5 | 9 | @hiogawa/react-server 10 | 11 |
12 | ported from{" "} 13 | 14 | Remix Tutorial 15 | 16 | . 17 |

18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/section-link.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@hiogawa/react-server/client'; 2 | 3 | export const SectionLink = ({ 4 | children, 5 | href, 6 | text, 7 | }: { 8 | children: React.ReactNode; 9 | href: string; 10 | text: string; 11 | }) => ( 12 | 13 |
14 | {children} 15 |
16 |
{text}
17 | 18 | ); 19 | -------------------------------------------------------------------------------- /minimal/src/routes/page.tsx: -------------------------------------------------------------------------------- 1 | import { Counter, Hydrated } from "./_client"; 2 | 3 | export default function Page() { 4 | return ( 5 | 6 | 7 | 8 | react-server 9 | 13 | 14 | 15 |
16 |
server random: {Math.random().toString(36).slice(2)}
17 | 18 | 19 |
20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/external-link.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRightIcon } from '@heroicons/react/24/outline'; 2 | 3 | export const ExternalLink = ({ 4 | children, 5 | href, 6 | }: { 7 | children: React.ReactNode; 8 | href: string; 9 | }) => { 10 | return ( 11 | 15 |
{children}
16 | 17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/[categorySlug]/[subCategorySlug]/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Boundary } from '#/ui/boundary'; 4 | 5 | export default function NotFound() { 6 | return ( 7 | 11 |
12 |

Sub Category Not Found

13 | 14 |

Could not find requested resource

15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-currency-symbol.tsx: -------------------------------------------------------------------------------- 1 | import { toFormat, type Dinero } from 'dinero.js'; 2 | 3 | export const ProductCurrencySymbol = ({ 4 | dinero, 5 | }: { 6 | dinero: Dinero; 7 | }) => { 8 | let symbol = ''; 9 | switch (toFormat(dinero, ({ currency }) => currency.code)) { 10 | case 'GBP': { 11 | symbol = '£'; 12 | break; 13 | } 14 | 15 | case 'EUR': { 16 | symbol = '€'; 17 | break; 18 | } 19 | 20 | default: { 21 | symbol = '$'; 22 | break; 23 | } 24 | } 25 | 26 | return <>{symbol}; 27 | }; 28 | -------------------------------------------------------------------------------- /minimal/src/runtime-client.ts: -------------------------------------------------------------------------------- 1 | export function createMemoImport() { 2 | return memoize(importClientReference); 3 | } 4 | 5 | async function importClientReference(id: string) { 6 | if (import.meta.env.DEV) { 7 | return import(/* @vite-ignore */ id); 8 | } else { 9 | const mod = await import("virtual:client-references" as string); 10 | return mod.default[id](); 11 | } 12 | } 13 | 14 | function memoize(f: (k: K) => V) { 15 | const cache = new Map(); 16 | return (k: K): V => { 17 | if (!cache.has(k)) { 18 | cache.set(k, f(k)); 19 | } 20 | return cache.get(k)!; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export default function Button({ 4 | kind = 'default', 5 | ...props 6 | }: React.ButtonHTMLAttributes & { 7 | kind?: 'default' | 'error'; 8 | }) { 9 | return ( 10 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /vercel-app-playground/src/api/reviews/getReviews.ts: -------------------------------------------------------------------------------- 1 | import type { Review } from './review'; 2 | 3 | // `server-only` guarantees any modules that import code in file 4 | // will never run on the client. Even though this particular api 5 | // doesn't currently use sensitive environment variables, it's 6 | // good practise to add `server-only` preemptively. 7 | import 'server-only'; 8 | 9 | export async function getReviews() { 10 | const res = await fetch(`https://app-playground-api.vercel.app/api/reviews`); 11 | 12 | if (!res.ok) { 13 | // Render the closest `error.js` Error Boundary 14 | throw new Error('Something went wrong!'); 15 | } 16 | 17 | const reviews = (await res.json()) as Review[]; 18 | return reviews; 19 | } 20 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/global-css/page.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | 3 | const SkeletonCard = () => ( 4 |
5 |
6 |
7 |
8 |
9 |
10 | ); 11 | 12 | export default function Page() { 13 | return ( 14 |
15 |

16 | Styled with a Global CSS Stylesheet 17 |

18 |
19 | 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/skeleton-card.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export const SkeletonCard = ({ isLoading }: { isLoading?: boolean }) => ( 4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ); 17 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/tab-group.tsx: -------------------------------------------------------------------------------- 1 | import { Tab } from '#/ui/tab'; 2 | 3 | export type Item = { 4 | text: string; 5 | slug?: string; 6 | segment?: string; 7 | parallelRoutesKey?: string; 8 | disabled?: boolean; 9 | }; 10 | 11 | export const TabGroup = ({ 12 | path, 13 | parallelRoutesKey, 14 | items, 15 | }: { 16 | path: string; 17 | parallelRoutesKey?: string; 18 | items: Item[]; 19 | }) => { 20 | return ( 21 |
22 | {items.map((item) => ( 23 | 29 | ))} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/[categorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { Counter } from '../context-click-counter'; 4 | 5 | export default async function Page({ 6 | params, 7 | }: { 8 | params: { categorySlug: string }; 9 | }) { 10 | const category = await getCategory({ slug: params.categorySlug }); 11 | 12 | return ( 13 | 14 |
15 |

16 | All {category.name} 17 |

18 | 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /vercel-app-playground/src/api/products/product.ts: -------------------------------------------------------------------------------- 1 | export type Product = { 2 | id: string; 3 | stock: number; 4 | rating: number; 5 | name: string; 6 | description: string; 7 | price: Price; 8 | isBestSeller: boolean; 9 | leadTime: number; 10 | image?: string; 11 | imageBlur?: string; 12 | discount?: Discount; 13 | usedPrice?: UsedPrice; 14 | }; 15 | 16 | type Price = { 17 | amount: number; 18 | currency: Currency; 19 | scale: number; 20 | }; 21 | 22 | type Currency = { 23 | code: string; 24 | base: number; 25 | exponent: number; 26 | }; 27 | 28 | type Discount = { 29 | percent: number; 30 | expires?: number; 31 | }; 32 | 33 | type UsedPrice = { 34 | amount: number; 35 | currency: Currency; 36 | scale: number; 37 | }; 38 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/[categorySlug]/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Boundary } from '#/ui/boundary'; 4 | import Button from '#/ui/button'; 5 | import React from 'react'; 6 | 7 | export default function Error({ error, reset }: any) { 8 | React.useEffect(() => { 9 | console.log('logging error:', error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 |
15 |

Error

16 |

{error?.message}

17 |
18 | 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-review-card.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import type { Review } from '#/app/api/reviews/review'; 4 | import { ProductRating } from '#/ui/product-rating'; 5 | 6 | export const ProductReviewCard = ({ review }: { review: Review }) => { 7 | return ( 8 |
9 |
10 |
11 |
12 |
{review.name}
13 |
14 | 15 | {review.rating ? : null} 16 |
17 | 18 |
{review.text}
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/layouts/[categorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import { SkeletonCard } from '#/ui/skeleton-card'; 3 | 4 | export default async function Page({ 5 | params, 6 | }: { 7 | params: { categorySlug: string }; 8 | }) { 9 | const category = await getCategory({ slug: params.categorySlug }); 10 | 11 | return ( 12 |
13 |

14 | All {category.name} 15 |

16 | 17 |
18 | {Array.from({ length: 9 }).map((_, i) => ( 19 | 20 | ))} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /vercel-app-playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "vite.config.ts", 5 | "tailwind.config.ts", 6 | "playwright.config.ts", 7 | "e2e" 8 | ], 9 | "compilerOptions": { 10 | "types": ["vite/client", "react/experimental"], 11 | "lib": ["dom", "dom.iterable", "esnext"], 12 | "allowJs": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noEmit": true, 17 | "esModuleInterop": true, 18 | "target": "ESNext", 19 | "module": "ESNext", 20 | "moduleResolution": "Bundler", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "jsx": "react-jsx", 24 | "paths": { 25 | "#/*": ["./src/*"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/layouts/[categorySlug]/[subCategorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import { SkeletonCard } from '#/ui/skeleton-card'; 3 | 4 | export default async function Page({ 5 | params, 6 | }: { 7 | params: { subCategorySlug: string }; 8 | }) { 9 | const category = await getCategory({ slug: params.subCategorySlug }); 10 | 11 | return ( 12 |
13 |

{category.name}

14 | 15 |
16 | {Array.from({ length: category.count }).map((_, i) => ( 17 | 18 | ))} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |

Styling

7 | 8 |
    9 |
  • This example shows different styling solutions.
  • 10 |
11 | 12 |
13 | 14 | Docs 15 | 16 | 17 | 18 | Code 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/counter-context.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | const CounterContext = React.createContext< 6 | [number, React.Dispatch>] | undefined 7 | >(undefined); 8 | 9 | export function CounterProvider({ children }: { children: React.ReactNode }) { 10 | const [count, setCount] = React.useState(0); 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | 18 | export function useCounter() { 19 | const context = React.useContext(CounterContext); 20 | if (context === undefined) { 21 | throw new Error('useCounter must be used within a CounterProvider'); 22 | } 23 | return context; 24 | } 25 | -------------------------------------------------------------------------------- /minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "preview": "vite preview", 7 | "lint": "biome check --write --linter-enabled=false ." 8 | }, 9 | "dependencies": { 10 | "react": "19.0.0-rc-38e3b23483-20240529", 11 | "react-dom": "19.0.0-rc-38e3b23483-20240529", 12 | "react-server-dom-webpack": "19.0.0-rc-38e3b23483-20240529" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^20.12.13", 16 | "@types/react": "^18.3.3", 17 | "@types/react-dom": "^18.3.0", 18 | "@vitejs/plugin-react": "^4.3.0", 19 | "vite": "^5.2.12" 20 | }, 21 | "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" 22 | } 23 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/[categorySlug]/[subCategorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { Counter } from '../../context-click-counter'; 4 | 5 | export default async function Page({ 6 | params, 7 | }: { 8 | params: { categorySlug: string; subCategorySlug: string }; 9 | }) { 10 | const category = await getCategory({ slug: params.subCategorySlug }); 11 | 12 | return ( 13 | 14 |
15 |

16 | {category.name} 17 |

18 | 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/css-modules/page.tsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.css'; 2 | 3 | const SkeletonCard = () => ( 4 |
5 |
6 |
7 |
8 |
9 |
10 | ); 11 | 12 | export default function Page() { 13 | return ( 14 |
15 |

16 | Styled with CSS Modules 17 |

18 |
19 | 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/[categorySlug]/[subCategorySlug]/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Boundary } from '#/ui/boundary'; 4 | import Button from '#/ui/button'; 5 | import React from 'react'; 6 | 7 | export default function Error({ error, reset }: any) { 8 | React.useEffect(() => { 9 | console.log('logging error:', error); 10 | }, [error]); 11 | 12 | return ( 13 | 17 |
18 |

Error

19 |

{error?.message}

20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/tailwind/page.tsx: -------------------------------------------------------------------------------- 1 | const SkeletonCard = () => ( 2 |
3 |
4 |
5 |
6 |
7 |
8 | ); 9 | 10 | export default function Page() { 11 | return ( 12 |
13 |

14 | Styled with Tailwind CSS 15 |

16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-lightening-deal.tsx: -------------------------------------------------------------------------------- 1 | import { ProductDeal } from '#/ui/product-deal'; 2 | import { add, formatDistanceToNow } from 'date-fns'; 3 | import { type Dinero } from 'dinero.js'; 4 | 5 | export const ProductLighteningDeal = ({ 6 | price, 7 | discount, 8 | }: { 9 | price: Dinero; 10 | discount: { 11 | amount: Dinero; 12 | expires?: number; 13 | }; 14 | }) => { 15 | const date = add(new Date(), { days: discount.expires }); 16 | 17 | return ( 18 | <> 19 |
20 |
21 | Expires in {formatDistanceToNow(date)} 22 |
23 |
24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/ssg/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tab } from '#/ui/tab'; 2 | import React from 'react'; 3 | import { Boundary } from '#/ui/boundary'; 4 | 5 | const title = 'Static Data'; 6 | 7 | export default function Layout({ children }: { children: React.ReactNode }) { 8 | return ( 9 |
10 | {title} 11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 | {children} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /vercel-app-playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { vitePluginReactServer } from '@hiogawa/react-server/plugin'; 3 | import { 4 | vitePluginLogger, 5 | vitePluginSsrMiddleware, 6 | } from '@hiogawa/vite-plugin-ssr-middleware'; 7 | import react from '@vitejs/plugin-react'; 8 | import { defineConfig } from 'vite'; 9 | 10 | export default defineConfig({ 11 | clearScreen: false, 12 | plugins: [ 13 | react(), 14 | vitePluginReactServer({ 15 | prerender: async () => { 16 | process.env['REACT_SERVER_PRERENDER'] = '1'; 17 | return ['/ssg/1', '/ssg/2']; 18 | }, 19 | }), 20 | vitePluginLogger(), 21 | vitePluginSsrMiddleware({ 22 | entry: process.env['SSR_ENTRY'] || '/src/adapters/node', 23 | preview: path.resolve('./dist/server/index.js'), 24 | }), 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/[categorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import BuggyButton from '#/ui/buggy-button'; 3 | import { SkeletonCard } from '#/ui/skeleton-card'; 4 | 5 | export default async function Page({ 6 | params, 7 | }: { 8 | params: { categorySlug: string }; 9 | }) { 10 | const category = await getCategory({ slug: params.categorySlug }); 11 | 12 | return ( 13 |
14 |

15 | All {category.name} 16 |

17 | 18 | 19 | 20 |
21 | {Array.from({ length: 9 }).map((_, i) => ( 22 | 23 | ))} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/patterns/search-params/active-link.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Link, useRouter } from '@hiogawa/react-server/client'; 4 | import clsx from 'clsx'; 5 | 6 | export default function ActiveLink({ 7 | isActive, 8 | searchParams, 9 | children, 10 | }: { 11 | isActive: boolean; 12 | searchParams: string; 13 | children: React.ReactNode; 14 | }) { 15 | const pathname = useRouter((s) => s.location.pathname); 16 | 17 | return ( 18 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /next-ts-tw/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Create Next App", 18 | description: "Generated by create next app", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/[categorySlug]/[subCategorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import BuggyButton from '#/ui/buggy-button'; 3 | import { SkeletonCard } from '#/ui/skeleton-card'; 4 | 5 | export default async function Page({ 6 | params, 7 | }: { 8 | params: { categorySlug: string; subCategorySlug: string }; 9 | }) { 10 | const category = await getCategory({ slug: params.subCategorySlug }); 11 | 12 | return ( 13 |
14 |

{category.name}

15 | 16 | 17 | 18 |
19 | {Array.from({ length: category.count }).map((_, i) => ( 20 | 21 | ))} 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /vercel-app-playground/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | const port = Number(process.env.E2E_PORT || 6174); 4 | const isPreview = Boolean(process.env.E2E_PREVIEW); 5 | const command = isPreview 6 | ? `pnpm preview --port ${port} --strict-port` 7 | : `pnpm dev --port ${port} --strict-port`; 8 | 9 | export default defineConfig({ 10 | testDir: 'e2e', 11 | use: { 12 | trace: 'on-first-retry', 13 | }, 14 | projects: [ 15 | { 16 | name: 'chromium', 17 | use: { 18 | ...devices['Desktop Chrome'], 19 | viewport: null, 20 | deviceScaleFactor: undefined, 21 | }, 22 | }, 23 | ], 24 | webServer: { 25 | command, 26 | port, 27 | }, 28 | grepInvert: isPreview ? /@dev/ : /@build/, 29 | forbidOnly: !!process.env['CI'], 30 | retries: process.env['CI'] ? 2 : 0, 31 | reporter: 'list', 32 | }); 33 | -------------------------------------------------------------------------------- /next-ts-tw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-ts-tw", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@hiogawa/react-server": "latest", 14 | "next": "npm:@hiogawa/react-server-next@latest", 15 | "react": "rc", 16 | "react-dom": "rc", 17 | "react-server-dom-webpack": "rc" 18 | }, 19 | "devDependencies": { 20 | "typescript": "^5", 21 | "@types/node": "^20", 22 | "@types/react": "^18", 23 | "@types/react-dom": "^18", 24 | "vite": "latest", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.4.1" 27 | }, 28 | "packageManager": "pnpm@8.15.8+sha512.d1a029e1a447ad90bc96cd58b0fad486d2993d531856396f7babf2d83eb1823bb83c5a3d0fc18f675b2d10321d49eb161fece36fe8134aa5823ecd215feed392" 29 | } 30 | -------------------------------------------------------------------------------- /vercel-app-playground/README.md: -------------------------------------------------------------------------------- 1 | > [!note] 2 | > See https://github.com/hi-ogawa/next-app-router-playground/pull/1 3 | > for the similar attempt based on Next.js drop-in-replacement package `@hiogawa/react-server-next` 4 | 5 | # vercel-app-playground 6 | 7 | Porting https://github.com/vercel/app-playground to Vite ([`@hiogawa/react-server`](https://github.com/hi-ogawa/vite-plugins/tree/main/packages/react-server)) (See https://github.com/hi-ogawa/rsc-on-vite/issues/5 for the progress) 8 | 9 | https://rsc-on-vite-vercel-app-playground.vercel.app 10 | 11 | ```sh 12 | pnpm dev 13 | 14 | # local preview 15 | pnpm build 16 | pnpm preview 17 | 18 | # deploy to vercel edge 19 | pnpm vc-build 20 | pnpm vc-release 21 | ``` 22 | 23 | ### One-Click Deploy with Vercel 24 | 25 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fhi-ogawa%2Frsc-on-vite%2Ftree%2Fmain%2Fvercel-app-playground) 26 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/rendering-info.tsx: -------------------------------------------------------------------------------- 1 | import { RenderedTimeAgo } from '#/ui/rendered-time-ago'; 2 | 3 | export function RenderingInfo({ 4 | type, 5 | }: { 6 | type: 'ssg' | 'ssgod' | 'ssr' | 'isr'; 7 | }) { 8 | let msg = ''; 9 | switch (type) { 10 | case 'ssg': 11 | msg = 'Statically pre-rendered at build time'; 12 | break; 13 | case 'ssgod': 14 | msg = 'Statically rendered on demand'; 15 | break; 16 | case 'isr': 17 | msg = 18 | 'Statically pre-rendered at build time and periodically revalidated'; 19 | break; 20 | case 'ssr': 21 | msg = 'Dynamically rendered at request time'; 22 | break; 23 | } 24 | 25 | return ( 26 |
27 |
{msg}
28 | 29 |
30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/layout.tsx: -------------------------------------------------------------------------------- 1 | import { TabGroup } from '#/ui/tab-group'; 2 | import React from 'react'; 3 | 4 | export default async function Layout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 |
11 | Streaming 12 |
13 | 32 |
33 | 34 |
{children}
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/rendering-page-skeleton.tsx: -------------------------------------------------------------------------------- 1 | const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`; 2 | 3 | export function RenderingPageSkeleton() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/ssg/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { RenderingInfo } from '#/ui/rendering-info'; 2 | 3 | export default async function Page({ params }: { params: { id: string } }) { 4 | const res = await fetch( 5 | `https://jsonplaceholder.typicode.com/posts/${params.id}`, 6 | ); 7 | const data = (await res.json()) as { title: string; body: string }; 8 | 9 | return ( 10 |
11 |
12 |

13 | {data.title} 14 |

15 |

{data.body}

16 |
17 |
18 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/layouts/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |

Layouts

7 | 8 |
    9 |
  • 10 | A layout is UI that is shared between multiple pages. On navigation, 11 | layouts preserve state, remain interactive, and do not re-render. Two 12 | or more layouts can also be nested. 13 |
  • 14 |
  • Try navigating between categories and sub categories.
  • 15 |
16 | 17 |
18 | 19 | Docs 20 | 21 | 22 | Code 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /vercel-app-playground/e2e/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { Page, expect, test } from '@playwright/test'; 2 | import { testNoJs, waitForHydration } from './helper'; 3 | 4 | test('layouts @js', async ({ page }) => { 5 | await page.goto('/'); 6 | await waitForHydration(page); 7 | await testLayouts(page, { js: true }); 8 | }); 9 | 10 | testNoJs('layouts @nojs', async ({ page }) => { 11 | await page.goto('/'); 12 | await testLayouts(page, { js: false }); 13 | }); 14 | 15 | async function testLayouts(page: Page, options: { js: boolean }) { 16 | await page 17 | .getByRole('link', { name: 'Nested Layouts Create UI that' }) 18 | .click(); 19 | await page.waitForURL('/layouts'); 20 | 21 | await page.getByRole('link', { name: 'Electronics' }).click(); 22 | await page.getByRole('heading', { name: 'All Electronics' }).click(); 23 | await page.waitForURL('/layouts/electronics'); 24 | 25 | await page.getByRole('link', { name: 'Phones' }).click(); 26 | await page.getByRole('heading', { name: 'Phones' }).click(); 27 | await page.waitForURL('/layouts/electronics/phones'); 28 | } 29 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/_components/cart-count-context.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | 5 | const CartCountContext = React.createContext< 6 | [number, React.Dispatch>] | undefined 7 | >(undefined); 8 | 9 | export function CartCountProvider({ 10 | children, 11 | initialCartCount, 12 | }: { 13 | children: React.ReactNode; 14 | initialCartCount: number; 15 | }) { 16 | const [optimisticCartCount, setOptimisticCartCount] = useState( 17 | null, 18 | ); 19 | 20 | const count = 21 | optimisticCartCount !== null ? optimisticCartCount : initialCartCount; 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | 30 | export function useCartCount() { 31 | const context = React.useContext(CartCountContext); 32 | if (context === undefined) { 33 | throw new Error('useCartCount must be used within a CartCountProvider'); 34 | } 35 | return context; 36 | } 37 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Boundary } from '#/ui/boundary'; 2 | import { TabGroup } from '#/ui/tab-group'; 3 | import React from 'react'; 4 | 5 | const items = [ 6 | { 7 | text: 'Global CSS', 8 | slug: 'global-css', 9 | }, 10 | { 11 | text: 'CSS Modules', 12 | slug: 'css-modules', 13 | }, 14 | { 15 | text: 'Styled Components', 16 | slug: 'styled-components', 17 | disabled: true, 18 | }, 19 | { 20 | text: 'Styled JSX', 21 | slug: 'styled-jsx', 22 | disabled: true, 23 | }, 24 | { 25 | text: 'Tailwind CSS', 26 | slug: 'tailwind', 27 | }, 28 | ]; 29 | 30 | export default function Layout({ children }: { children: React.ReactNode }) { 31 | return ( 32 |
33 | Styling 34 | 43 |
44 | {children} 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/tab.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { Item } from '#/ui/tab-group'; 4 | import { Link, useRouter } from '@hiogawa/react-server/client'; 5 | import clsx from 'clsx'; 6 | 7 | export const Tab = ({ 8 | path, 9 | item, 10 | }: { 11 | path: string; 12 | parallelRoutesKey?: string; 13 | item: Item; 14 | }) => { 15 | const pathname = useRouter((s) => s.location.pathname); 16 | const segment = pathname.slice(path.length).split('/')[1]; 17 | 18 | const href = item.slug ? path + '/' + item.slug : path; 19 | const isActive = item.slug ? item.slug === segment : !segment; 20 | item.disabled; 21 | 22 | return ( 23 | 36 | {item.text} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /minimal/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // cf. 2 | // https://github.com/dai-shi/waku/blob/4d16c28a58204991de2985df0d202f21a48ae1f9/packages/waku/src/types.d.ts#L60-L65 3 | // https://github.com/facebook/react/blob/706d95f486fbdec35b771ea4aaf3e78feb907249/packages/react-server-dom-webpack/src/shared/ReactFlightImportMetadata.js 4 | // https://github.com/facebook/react/blob/706d95f486fbdec35b771ea4aaf3e78feb907249/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js 5 | 6 | export interface ImportManifestEntry { 7 | id: string; 8 | name: string; 9 | chunks: string[]; 10 | } 11 | 12 | export interface BundlerConfig { 13 | [bundlerId: string]: ImportManifestEntry; 14 | } 15 | 16 | export type ModuleMap = { 17 | [id: string]: { 18 | [exportName: string]: ImportManifestEntry; 19 | }; 20 | }; 21 | 22 | export interface SsrManifest { 23 | moduleMap?: ModuleMap; 24 | moduleLoading?: null; 25 | } 26 | 27 | export type WebpackRequire = (id: string) => Promise; 28 | 29 | // TODO 30 | export type WebpackChunkLoad = (id: string) => Promise; 31 | 32 | export type CallServerCallback = (id: string, args: unknown[]) => unknown; 33 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |

Client Context

7 | 8 |
    9 |
  • 10 | This example uses context to share state between Client Components 11 | that cross the Server/Client Component boundary. 12 |
  • 13 |
  • 14 | Try incrementing the counter and navigating between pages. Note how 15 | the counter state is shared across the app even though they are inside 16 | different layouts and pages that are Server Components. 17 |
  • 18 |
19 | 20 |
21 | 22 | Docs 23 | 24 | 25 | Code 26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { ClickCounter } from '#/ui/click-counter'; 4 | import { TabGroup } from '#/ui/tab-group'; 5 | import React from 'react'; 6 | 7 | export default async function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const categories = await getCategories(); 13 | 14 | return ( 15 |
16 | Error Handling 17 | 18 |
19 | ({ 26 | text: x.name, 27 | slug: x.slug, 28 | })), 29 | ]} 30 | /> 31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | {children} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/layouts/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { ClickCounter } from '#/ui/click-counter'; 4 | import { TabGroup } from '#/ui/tab-group'; 5 | import type { LayoutProps } from '@hiogawa/react-server/server'; 6 | 7 | const title = 'Nested Layouts'; 8 | 9 | export default async function Layout({ children }: LayoutProps) { 10 | const categories = await getCategories(); 11 | 12 | return ( 13 |
14 | {title} 15 |
16 | ({ 23 | text: x.name, 24 | slug: x.slug, 25 | })), 26 | ]} 27 | /> 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 | {children} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/loading/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { ClickCounter } from '#/ui/click-counter'; 4 | import { TabGroup } from '#/ui/tab-group'; 5 | import React from 'react'; 6 | 7 | const title = 'Loading'; 8 | 9 | export default async function Layout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | const categories = await getCategories(); 15 | 16 | return ( 17 |
18 | {title} 19 |
20 | ({ 27 | text: x.name, 28 | slug: x.slug, 29 | })), 30 | ]} 31 | /> 32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 | {children} 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /minimal/src/entry-browser.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMClient from "react-dom/client"; 3 | import type { FlightData } from "./entry-server"; 4 | import { createMemoImport } from "./runtime-client"; 5 | 6 | async function main() { 7 | // react client (flight -> react node) 8 | (globalThis as any).__webpack_require__ = createMemoImport(); 9 | const { default: ReactClient } = await import( 10 | "react-server-dom-webpack/client.browser" 11 | ); 12 | const initPromise = ReactClient.createFromReadableStream( 13 | (globalThis as any).__flightStreamScript, 14 | ); 15 | 16 | let $__setState: (data: Promise) => void; 17 | 18 | function Root() { 19 | const [promise, setState] = React.useState(initPromise); 20 | $__setState = setState; 21 | 22 | return React.use(promise); 23 | } 24 | 25 | // react dom browser (react node -> html) 26 | React.startTransition(() => { 27 | ReactDOMClient.hydrateRoot(document, ); 28 | }); 29 | 30 | if (import.meta.hot) { 31 | import.meta.hot.on("rsc-update", () => { 32 | $__setState(ReactClient.createFromFetch(fetch("/?__flight"))); 33 | }); 34 | } 35 | } 36 | 37 | main(); 38 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/[categorySlug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories, getCategory } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { TabGroup } from '#/ui/tab-group'; 4 | import { Counter } from '../context-click-counter'; 5 | 6 | export default async function Layout({ 7 | children, 8 | params, 9 | }: { 10 | children: React.ReactNode; 11 | params: { categorySlug: string }; 12 | }) { 13 | const category = await getCategory({ slug: params.categorySlug }); 14 | const categories = await getCategories({ parent: params.categorySlug }); 15 | 16 | return ( 17 | 18 |
19 | ({ 26 | text: x.name, 27 | slug: x.slug, 28 | })), 29 | ]} 30 | /> 31 | 32 |
{children}
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/global-css/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-template-columns: repeat(1, minmax(0, 1fr)); 4 | gap: 1.5rem /* 24px */; 5 | } 6 | 7 | @media (min-width: 1024px) { 8 | .container { 9 | grid-template-columns: repeat(3, minmax(0, 1fr)); 10 | } 11 | } 12 | 13 | .skeleton { 14 | padding: 1rem /* 16px */; 15 | border-radius: 1rem /* 16px */; 16 | background-color: rgb(24 24 27 / 0.8); 17 | } 18 | 19 | .skeleton-img, 20 | .skeleton-btn, 21 | .skeleton-line-one, 22 | .skeleton-line-two { 23 | border-radius: 0.5rem /* 8px */; 24 | } 25 | 26 | .skeleton-img { 27 | height: 3.5rem /* 56px */; 28 | background-color: rgb(63 63 70 / 1); 29 | } 30 | 31 | .skeleton-btn, 32 | .skeleton-line-one, 33 | .skeleton-line-two { 34 | margin-top: 0.75rem /* 12px */; 35 | height: 0.75rem /* 12px */; 36 | } 37 | 38 | .skeleton-btn { 39 | background-color: rgb(245 166 35 / 1); 40 | width: 25%; 41 | } 42 | 43 | .skeleton-line-one, 44 | .skeleton-line-two { 45 | background-color: rgb(63 63 70 / 1); 46 | } 47 | 48 | .skeleton-line-one { 49 | width: 91.666667%; 50 | } 51 | 52 | .skeleton-line-two { 53 | width: 66.666667%; 54 | } 55 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/styling/css-modules/styles.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | grid-template-columns: repeat(1, minmax(0, 1fr)); 4 | gap: 1.5rem /* 24px */; 5 | } 6 | 7 | @media (min-width: 1024px) { 8 | .container { 9 | grid-template-columns: repeat(3, minmax(0, 1fr)); 10 | } 11 | } 12 | 13 | .skeleton { 14 | padding: 1rem /* 16px */; 15 | border-radius: 1rem /* 16px */; 16 | background-color: rgb(24 24 27 / 0.8); 17 | } 18 | 19 | .skeleton-img, 20 | .skeleton-btn, 21 | .skeleton-line-one, 22 | .skeleton-line-two { 23 | border-radius: 0.5rem /* 8px */; 24 | } 25 | 26 | .skeleton-img { 27 | height: 3.5rem /* 56px */; 28 | background-color: rgb(63 63 70 / 1); 29 | } 30 | 31 | .skeleton-btn, 32 | .skeleton-line-one, 33 | .skeleton-line-two { 34 | margin-top: 0.75rem /* 12px */; 35 | height: 0.75rem /* 12px */; 36 | } 37 | 38 | .skeleton-btn { 39 | background-color: rgb(121 40 202 / 1); 40 | width: 25%; 41 | } 42 | 43 | .skeleton-line-one, 44 | .skeleton-line-two { 45 | background-color: rgb(63 63 70 / 1); 46 | } 47 | 48 | .skeleton-line-one { 49 | width: 91.666667%; 50 | } 51 | 52 | .skeleton-line-two { 53 | width: 66.666667%; 54 | } 55 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-deal.tsx: -------------------------------------------------------------------------------- 1 | import { ProductCurrencySymbol } from '#/ui/product-currency-symbol'; 2 | import { toUnit, type Dinero } from 'dinero.js'; 3 | 4 | export const ProductDeal = ({ 5 | price: priceRaw, 6 | discount: discountRaw, 7 | }: { 8 | price: Dinero; 9 | discount: { 10 | amount: Dinero; 11 | }; 12 | }) => { 13 | const discount = toUnit(discountRaw.amount); 14 | const price = toUnit(priceRaw); 15 | const percent = Math.round(100 - (discount / price) * 100); 16 | 17 | return ( 18 |
19 |
20 | -{percent}% 21 |
22 |
23 |
24 | 25 |
26 |
27 | {discount} 28 |
29 |
30 |
31 | 32 | {price} 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/[categorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import { SkeletonCard } from '#/ui/skeleton-card'; 3 | 4 | export default async function Page({ 5 | params, 6 | }: { 7 | params: { categorySlug: string }; 8 | }) { 9 | // - `getCategory()` returns `notFound()` if the fetched data is `null` or `undefined`. 10 | // - `notFound()` renders the closest `not-found.tsx` in the route segment hierarchy. 11 | // - For `layout.js`, the closest `not-found.tsx` starts from the parent segment. 12 | // - For `page.js`, the closest `not-found.tsx` starts from the same segment. 13 | // - Learn more: https://nextjs.org/docs/app/building-your-application/routing#component-hierarchy. 14 | const category = await getCategory({ slug: params.categorySlug }); 15 | 16 | return ( 17 |
18 |

19 | All {category.name} 20 |

21 | 22 |
23 | {Array.from({ length: 9 }).map((_, i) => ( 24 | 25 | ))} 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/ssg/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |

Static Data

7 | 8 |
    9 |
  • 10 | By default, data fetching in Next.js is cached static. 11 |
  • 12 |
  • This example statically caches data fetches for Post 1 and 2.
  • 13 |
  • 14 | A random third post is fetched on-demand the first time it is 15 | requested. 16 |
  • 17 |
  • 18 | Try navigating to each post and noting the timestamp of when the page 19 | was rendered. 20 |
  • 21 |
22 | 23 |
24 | 25 | Docs 26 | 27 | 28 | Code 29 | 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | concurrency: 9 | group: ci-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | vercel-app-playground: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./vercel-app-playground 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | - run: corepack enable 24 | - run: pnpm i 25 | - run: pnpm lint-check 26 | - run: pnpm tsc 27 | - run: pnpm exec playwright install chromium 28 | - run: pnpm test-e2e 29 | - run: pnpm build 30 | - run: pnpm test-e2e-preview 31 | 32 | remix-tutorial: 33 | runs-on: ubuntu-latest 34 | defaults: 35 | run: 36 | working-directory: ./remix-tutorial 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: 20 42 | - run: corepack enable 43 | - run: pnpm i 44 | - run: pnpm lint-check 45 | - run: pnpm tsc 46 | - run: pnpm cf-build 47 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/[categorySlug]/[subCategorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCategory } from '#/api/categories/getCategories'; 2 | import { SkeletonCard } from '#/ui/skeleton-card'; 3 | 4 | export default async function Page({ 5 | params, 6 | }: { 7 | params: { categorySlug: string; subCategorySlug: string }; 8 | }) { 9 | // - `getCategory()` returns `notFound()` if the fetched data is `null` or `undefined`. 10 | // - `notFound()` renders the closest `not-found.tsx` in the route segment hierarchy. 11 | // - For `layout.js`, the closest `not-found.tsx` starts from the parent segment. 12 | // - For `page.js`, the closest `not-found.tsx` starts from the same segment. 13 | // - Learn more: https://nextjs.org/docs/app/building-your-application/routing#component-hierarchy. 14 | const category = await getCategory({ slug: params.subCategorySlug }); 15 | 16 | return ( 17 |
18 |

{category.name}

19 | 20 |
21 | {Array.from({ length: category.count }).map((_, i) => ( 22 | 23 | ))} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/context-click-counter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCounter } from './counter-context'; 4 | import { Boundary } from '#/ui/boundary'; 5 | 6 | const ContextClickCounter = () => { 7 | const [count, setCount] = useCounter(); 8 | 9 | return ( 10 | 16 | 22 | 23 | ); 24 | }; 25 | 26 | export const Counter = () => { 27 | const [count] = useCounter(); 28 | 29 | return ( 30 | 36 |
37 | {count} Clicks 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default ContextClickCounter; 44 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { demoSlugs } from '#/lib/demos'; 4 | import { Boundary } from '#/ui/boundary'; 5 | import { useRouter } from '@hiogawa/react-server/client'; 6 | import type { ErrorPageProps } from '@hiogawa/react-server/server'; 7 | 8 | export default function ErrorPage(props: ErrorPageProps) { 9 | const pathname = useRouter((s) => s.location.pathname); 10 | const demoNotFound = demoSlugs.includes(pathname.slice(1)); 11 | 12 | return ( 13 | 14 |
15 | {props.serverError?.status === 404 ? ( 16 | <> 17 |

Not Found

18 |

19 | {demoNotFound ? ( 20 | <>TODO: Not implemented 21 | ) : ( 22 | <>Could not find requested resource 23 | )} 24 |

25 | 26 | ) : ( 27 | <> 28 |

Unknown Error

29 |
{JSON.stringify(props.serverError)}
30 | 31 | )} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/layouts/[categorySlug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories, getCategory } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { ClickCounter } from '#/ui/click-counter'; 4 | import { TabGroup } from '#/ui/tab-group'; 5 | 6 | export default async function Layout({ 7 | children, 8 | params, 9 | }: { 10 | children: React.ReactNode; 11 | params: { categorySlug: string }; 12 | }) { 13 | const category = await getCategory({ slug: params.categorySlug }); 14 | const categories = await getCategories({ parent: params.categorySlug }); 15 | 16 | return ( 17 |
18 |
19 | ({ 26 | text: x.name, 27 | slug: x.slug, 28 | })), 29 | ]} 30 | /> 31 | 32 |
33 | 34 |
35 |
36 | 37 |
38 | {children} 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /vercel-app-playground/src/api/categories/getCategories.ts: -------------------------------------------------------------------------------- 1 | import type { Category } from './category'; 2 | import { createError } from '@hiogawa/react-server/server'; 3 | 4 | export async function getCategories({ parent }: { parent?: string } = {}) { 5 | // TODO: not working on stackblitz? 6 | const res = await fetch( 7 | `https://app-playground-api.vercel.app/api/categories${ 8 | parent ? `?parent=${parent}` : '' 9 | }`, 10 | ); 11 | 12 | if (!res.ok) { 13 | throw new Error('Something went wrong!'); 14 | } 15 | 16 | const categories = (await res.json()) as Category[]; 17 | 18 | if (categories.length === 0) { 19 | throw createError({ status: 404 }); 20 | } 21 | 22 | return categories; 23 | } 24 | 25 | export async function getCategory({ slug }: { slug: string }) { 26 | const res = await fetch( 27 | `https://app-playground-api.vercel.app/api/categories${ 28 | slug ? `?slug=${slug}` : '' 29 | }`, 30 | ); 31 | 32 | if (!res.ok) { 33 | // Render the closest `error.js` Error Boundary 34 | throw new Error('Something went wrong!'); 35 | } 36 | 37 | const category = (await res.json()) as Category; 38 | 39 | if (!category) { 40 | throw createError({ status: 404 }); 41 | } 42 | 43 | return category; 44 | } 45 | -------------------------------------------------------------------------------- /vercel-app-playground/misc/vercel-edge/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu -o pipefail 3 | 4 | # https://vercel.com/docs/build-output-api/v3/primitives#edge-functions 5 | 6 | # .vercel/ 7 | # project.json 8 | # output/ 9 | # config.json 10 | # static/ 11 | # assets/ = dist/client/assets 12 | # functions/ 13 | # index.func/ 14 | # .vc-config.json 15 | # index.js = dist/server/index.js 16 | 17 | this_dir="$(dirname "${BASH_SOURCE[0]}")" 18 | 19 | # clean 20 | rm -rf .vercel/output 21 | mkdir -p .vercel/output 22 | 23 | # config.json 24 | cp "$this_dir/config.json" .vercel/output/config.json 25 | 26 | # static 27 | mkdir -p .vercel/output/static 28 | cp -r dist/client/. .vercel/output/static 29 | rm -rf .vercel/output/static/.vite 30 | 31 | # functions 32 | mkdir -p .vercel/output/functions/index.func 33 | cp "$this_dir/.vc-config.json" .vercel/output/functions/index.func/.vc-config.json 34 | npx esbuild dist/server/index.js \ 35 | --outfile=.vercel/output/functions/index.func/index.js \ 36 | --metafile=dist/server/esbuild-metafile.json \ 37 | --define:process.env.NODE_ENV='"production"' \ 38 | --log-override:ignored-bare-import=silent \ 39 | --bundle \ 40 | --minify \ 41 | --format=esm \ 42 | --platform=browser 43 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { ClickCounter } from '#/ui/click-counter'; 4 | import { TabGroup } from '#/ui/tab-group'; 5 | import React from 'react'; 6 | 7 | const title = 'Not Found'; 8 | 9 | export default async function Layout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | const categories = await getCategories(); 15 | 16 | return ( 17 |
18 | {title} 19 |
20 | ({ 27 | text: x.name, 28 | slug: x.slug, 29 | })), 30 | { 31 | text: 'Category That Does Not Exist', 32 | slug: 'does-not-exist', 33 | }, 34 | ]} 35 | /> 36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 | {children} 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/page.tsx: -------------------------------------------------------------------------------- 1 | import BuggyButton from '#/ui/buggy-button'; 2 | import { ExternalLink } from '#/ui/external-link'; 3 | 4 | export default function Page() { 5 | return ( 6 |
7 |

Error Handling

8 | 9 |
    10 |
  • 11 | error.js defines the error boundary for a route segment 12 | and the children below it. It can be used to show specific error 13 | information, and functionality to attempt to recover from the error. 14 |
  • 15 |
  • 16 | Trying navigation pages and triggering an error inside nested layouts. 17 | Notice how the error is isolated to that segment, while the rest of 18 | the app remains interactive. 19 |
  • 20 |
21 | 22 |
23 | 24 | 25 | 26 | Docs 27 | 28 | 29 | Code 30 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/error-handling/[categorySlug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories, getCategory } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { ClickCounter } from '#/ui/click-counter'; 4 | import { TabGroup } from '#/ui/tab-group'; 5 | 6 | export default async function Layout({ 7 | children, 8 | params, 9 | }: { 10 | children: React.ReactNode; 11 | params: { categorySlug: string }; 12 | }) { 13 | const category = await getCategory({ slug: params.categorySlug }); 14 | const categories = await getCategories({ parent: params.categorySlug }); 15 | 16 | return ( 17 |
18 |
19 |
20 | ({ 27 | text: x.name, 28 | slug: x.slug, 29 | })), 30 | ]} 31 | /> 32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | {children} 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/node/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Boundary } from '#/ui/boundary'; 2 | import React from 'react'; 3 | import { CartCountProvider } from '../_components/cart-count-context'; 4 | import { Header } from '../_components/header'; 5 | import { cartCount } from '../_action'; 6 | 7 | export default function Layout({ children }: { children: React.ReactNode }) { 8 | return ( 9 | <> 10 |
11 |
    12 |
  • 13 | Primary product information is loaded first as part of the initial 14 | response. 15 |
  • 16 |
  • 17 | Secondary, more personalized details (that might be slower) like 18 | ship date, other recommended products, and customer reviews are 19 | progressively streamed in. 20 |
  • 21 |
  • Try refreshing or navigating to other recommended products.
  • 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | {children} 30 |
31 |
32 |
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/loading/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |

Instant Loading States

7 | 8 |
    9 |
  • 10 | This example has an artificial delay when "fetching" data 11 | for each category page. loading.js is used to show a 12 | loading skeleton immediately while data for category page loads before 13 | being streamed in. 14 |
  • 15 |
  • 16 | Shared layouts remain interactive while nested layouts or pages load. 17 | Try clicking the counter while the children load. 18 |
  • 19 |
  • 20 | Navigation is interruptible. Try navigating to one category, then 21 | clicking a second category before the first one has loaded. 22 |
  • 23 |
24 | 25 |
26 | 27 | Docs 28 | 29 | 30 | Code 31 | 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | 3 | export default async function Page() { 4 | return ( 5 |
6 |

Streaming with Suspense

7 | 8 |
    9 |
  • 10 | Streaming allows you to progressively render and send units of the UI 11 | from the server to the client. 12 |
  • 13 | 14 |
  • 15 | This allows the user to see and interact with the most essential parts 16 | of the page while the rest of the content loads - instead of waiting 17 | for the whole page to load before they can interact with anything. 18 |
  • 19 | 20 |
  • Streaming works with both Edge and Node runtimes.
  • 21 | 22 |
  • 23 | Try streaming by selecting a runtime in the 24 | navigation above. 25 |
  • 26 |
27 | 28 |
29 | 30 | Docs 31 | 32 | 33 | Code 34 | 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/_components/add-to-cart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTransition } from 'react'; 4 | import { useCartCount } from './cart-count-context'; 5 | import { addCartCount } from '../_action'; 6 | 7 | export function AddToCart({ initialCartCount }: { initialCartCount: number }) { 8 | const [isPending, startTransition] = useTransition(); 9 | 10 | const [, setOptimisticCartCount] = useCartCount(); 11 | 12 | const addToCart = () => { 13 | setOptimisticCartCount(initialCartCount + 1); 14 | 15 | // Use a transition and isPending to create inline loading UI 16 | startTransition(async () => { 17 | const newCount = await addCartCount(); 18 | setOptimisticCartCount(newCount); 19 | }); 20 | }; 21 | 22 | return ( 23 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AddressBar } from '#/ui/address-bar'; 2 | import Byline from '#/ui/byline'; 3 | import { GlobalNav } from '#/ui/global-nav'; 4 | import { Hydrated } from '#/ui/hydrated'; 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
{children}
32 |
33 | 34 |
35 |
36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /remix-tutorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hiogawa/react-server-example-starter", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "tsc": "tsc -b", 11 | "tsc-dev": "pnpm tsc --watch --preserveWatchOutput", 12 | "lint": "prettier -w --cache .", 13 | "lint-check": "prettier -c --cache .", 14 | "cf-build": "SSR_ENTRY=/src/adapters/cloudflare-workers.ts pnpm build && bash misc/cloudflare-workers/build.sh", 15 | "cf-preview": "cd misc/cloudflare-workers && wrangler dev", 16 | "cf-release": "cd misc/cloudflare-workers && wrangler deploy" 17 | }, 18 | "dependencies": { 19 | "@hiogawa/react-server": "0.3.3", 20 | "react": "rc", 21 | "react-dom": "rc", 22 | "react-server-dom-webpack": "rc" 23 | }, 24 | "devDependencies": { 25 | "@hiogawa/utils": "1.6.4-pre.2", 26 | "@hiogawa/utils-node": "^0.0.1", 27 | "@hiogawa/vite-plugin-ssr-middleware": "0.0.3", 28 | "@types/react": "18.2.66", 29 | "@types/react-dom": "18.2.22", 30 | "@vitejs/plugin-react": "^4.2.1", 31 | "esbuild": "^0.20.2", 32 | "prettier": "^3.2.5", 33 | "typescript": "^5.4.4", 34 | "vite": "5.2.10", 35 | "wrangler": "^3.53.0" 36 | }, 37 | "packageManager": "pnpm@8.15.8+sha512.d1a029e1a447ad90bc96cd58b0fad486d2993d531856396f7babf2d83eb1823bb83c5a3d0fc18f675b2d10321d49eb161fece36fe8134aa5823ecd215feed392" 38 | } 39 | -------------------------------------------------------------------------------- /vercel-app-playground/public/grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/byline.tsx: -------------------------------------------------------------------------------- 1 | import { VercelLogo } from '#/ui/vercel-logo'; 2 | 3 | export default function Byline({ className }: { className: string }) { 4 | return ( 5 |
8 |
9 |
10 |
By
11 | 12 |
13 | 14 |
15 |
16 |
17 | 18 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/_components/reviews.tsx: -------------------------------------------------------------------------------- 1 | import type { Review } from '#/api/reviews/review'; 2 | import { ProductReviewCard } from '#/ui/product-review-card'; 3 | 4 | export async function Reviews({ data }: { data: Promise }) { 5 | const reviews = (await data.then((res) => res.json())) as Review[]; 6 | 7 | return ( 8 |
9 |
Customer Reviews
10 |
11 | {reviews.map((review) => { 12 | return ; 13 | })} 14 |
15 |
16 | ); 17 | } 18 | 19 | const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`; 20 | 21 | function Skeleton() { 22 | return ( 23 |
24 |
25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | export function ReviewsSkeleton() { 33 | return ( 34 |
35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-price.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { Product } from '#/app/api/products/product'; 3 | import { ProductCurrencySymbol } from '#/ui/product-currency-symbol'; 4 | import { ProductDeal } from '#/ui/product-deal'; 5 | import { ProductLighteningDeal } from '#/ui/product-lightening-deal'; 6 | import { multiply, toUnit, type Dinero } from 'dinero.js'; 7 | 8 | function isDiscount(obj: any): obj is { percent: number; expires?: number } { 9 | return typeof obj?.percent === 'number'; 10 | } 11 | 12 | function formatDiscount( 13 | price: Dinero, 14 | discountRaw: Product['discount'], 15 | ) { 16 | return isDiscount(discountRaw) 17 | ? { 18 | amount: multiply(price, { 19 | amount: discountRaw.percent, 20 | scale: 2, 21 | }), 22 | expires: discountRaw.expires, 23 | } 24 | : undefined; 25 | } 26 | 27 | export const ProductPrice = ({ 28 | price, 29 | discount: discountRaw, 30 | }: { 31 | price: Dinero; 32 | discount: Product['discount']; 33 | }) => { 34 | const discount = formatDiscount(price, discountRaw); 35 | 36 | if (discount) { 37 | if (discount?.expires && typeof discount.expires === 'number') { 38 | return ; 39 | } 40 | return ; 41 | } 42 | 43 | return ( 44 |
45 |
46 | 47 |
48 |
49 | {toUnit(price)} 50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/loading/[categorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Category } from '#/api/categories/category'; 2 | import { SkeletonCard } from '#/ui/skeleton-card'; 3 | import { PageProps, createError } from '@hiogawa/react-server/server'; 4 | import React from 'react'; 5 | import { Loading } from '../_loading'; 6 | 7 | // TODO: userland `loading` implementation 8 | export default function PageWithLoading(props: PageProps) { 9 | return ( 10 | }> 11 | 12 | 13 | ); 14 | } 15 | 16 | async function Page({ params }: { params: { categorySlug: string } }) { 17 | const res = await fetch( 18 | // We intentionally delay the response to simulate a slow data 19 | // request that would benefit from `loading.js` 20 | `https://app-playground-api.vercel.app/api/categories?delay=1000&slug=${params.categorySlug}`, 21 | { 22 | // We intentionally disable Next.js Cache to better demo 23 | // `loading.js` 24 | cache: 'no-cache', 25 | }, 26 | ); 27 | 28 | if (!res.ok) { 29 | // Render the closest `error.js` Error Boundary 30 | throw new Error('Something went wrong!'); 31 | } 32 | 33 | const category = (await res.json()) as Category; 34 | 35 | if (!category) { 36 | throw createError({ status: 404 }); 37 | } 38 | 39 | return ( 40 |
41 |

{category.name}

42 | 43 |
44 | {Array.from({ length: category.count }).map((_, i) => ( 45 | 46 | ))} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/rendered-time-ago.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import ms from 'ms'; 4 | import { useEffect, useRef, useState } from 'react'; 5 | 6 | // https://github.com/streamich/react-use/blob/master/src/useInterval.ts 7 | const useInterval = (callback: Function, delay?: number | null) => { 8 | const savedCallback = useRef(() => {}); 9 | 10 | useEffect(() => { 11 | savedCallback.current = callback; 12 | }); 13 | 14 | useEffect(() => { 15 | if (delay !== null) { 16 | const interval = setInterval(() => savedCallback.current(), delay || 0); 17 | return () => clearInterval(interval); 18 | } 19 | 20 | return undefined; 21 | }, [delay]); 22 | }; 23 | 24 | export function RenderedTimeAgo({ timestamp }: { timestamp: number }) { 25 | const [msAgo, setMsAgo] = useState(0); 26 | 27 | // update on page change 28 | useEffect(() => { 29 | setMsAgo(Date.now() - timestamp); 30 | }, [timestamp]); 31 | 32 | // update every second 33 | useInterval(() => { 34 | setMsAgo(Date.now() - timestamp); 35 | }, 1000); 36 | 37 | return ( 38 |
42 | {msAgo ? ( 43 | <> 44 | 49 | {msAgo >= 1000 ? ms(msAgo) : '0s'} 50 | {' '} 51 | ago 52 | 53 | ) : null} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/[categorySlug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories, getCategory } from '#/api/categories/getCategories'; 2 | import { ClickCounter } from '#/ui/click-counter'; 3 | import { TabGroup } from '#/ui/tab-group'; 4 | 5 | export default async function Layout({ 6 | children, 7 | params, 8 | }: { 9 | children: React.ReactNode; 10 | params: { categorySlug: string }; 11 | }) { 12 | // - `getCategory()` returns `notFound()` if the fetched data is `null` or `undefined`. 13 | // - `notFound()` renders the closest `not-found.tsx` in the route segment hierarchy. 14 | // - For `layout.js`, the closest `not-found.tsx` starts from the parent segment. 15 | // - For `page.js`, the closest `not-found.tsx` starts from the same segment. 16 | // - Learn more: https://nextjs.org/docs/app/building-your-application/routing#component-hierarchy. 17 | const category = await getCategory({ slug: params.categorySlug }); 18 | const categories = await getCategories({ parent: params.categorySlug }); 19 | 20 | return ( 21 |
22 |
23 |
24 | ({ 31 | text: x.name, 32 | slug: x.slug, 33 | })), 34 | { 35 | text: 'Subcategory That Does Not Exist', 36 | slug: 'does-not-exist', 37 | }, 38 | ]} 39 | /> 40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
{children}
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/vercel-logo.tsx: -------------------------------------------------------------------------------- 1 | export function VercelLogo() { 2 | return ( 3 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/context/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories } from '#/api/categories/getCategories'; 2 | import { Boundary } from '#/ui/boundary'; 3 | import { TabGroup } from '#/ui/tab-group'; 4 | import React from 'react'; 5 | import ContextClickCounter from './context-click-counter'; 6 | import { CounterProvider } from './counter-context'; 7 | 8 | const title = 'Client Context'; 9 | 10 | export default async function Layout({ 11 | children, 12 | }: { 13 | children: React.ReactNode; 14 | }) { 15 | const categories = await getCategories(); 16 | return ( 17 | 22 | 28 | 29 | 34 |
35 | {title} 36 |
37 | ({ 44 | text: x.name, 45 | slug: x.slug, 46 | })), 47 | ]} 48 | /> 49 |
50 | 51 | 52 |
{children}
53 |
54 |
55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/mobile-nav-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/solid'; 4 | import clsx from 'clsx'; 5 | import React from 'react'; 6 | 7 | const MobileNavContext = React.createContext< 8 | [boolean, React.Dispatch>] | undefined 9 | >(undefined); 10 | 11 | export function MobileNavContextProvider({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | const [isOpen, setIsOpen] = React.useState(false); 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | 24 | export function useMobileNavToggle() { 25 | const context = React.useContext(MobileNavContext); 26 | if (context === undefined) { 27 | throw new Error( 28 | 'useMobileNavToggle must be used within a MobileNavContextProvider', 29 | ); 30 | } 31 | return context; 32 | } 33 | 34 | export function MobileNavToggle({ children }: { children: React.ReactNode }) { 35 | const [isOpen, setIsOpen] = useMobileNavToggle(); 36 | 37 | return ( 38 | <> 39 | 53 | 54 |
60 | {children} 61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/header.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 'use client'; 3 | 4 | import styled from 'styled-components'; 5 | 6 | const HeadContainer = styled.header` 7 | position: relative; 8 | height: 64px; 9 | align-items: center; 10 | padding: 0px 8px; 11 | margin-bottom: 48px; 12 | display: flex; 13 | border: 0 solid #e5e7eb; 14 | color: rgb(244 244 245); 15 | grid-column-start: 2; 16 | grid-column-end: 4; 17 | `; 18 | 19 | const Title = styled.span` 20 | margin: 0 8px; 21 | `; 22 | 23 | const NextJsLogo = (props: any) => ( 24 | 30 | 34 | 35 | ); 36 | 37 | export default function Header() { 38 | return ( 39 | 40 | 41 | The React Framework 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/not-found/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | import { Link } from '@hiogawa/react-server/client'; 3 | 4 | export default function Page() { 5 | return ( 6 |
7 |

Not Found

8 | 9 |
    10 |
  • 11 | 12 | 13 | not-found.js 14 | 15 | {' '} 16 | file is used to render UI when the{' '} 17 | 18 | 19 | notFound() 20 | 21 | {' '} 22 | function is thrown within a route segment. 23 |
  • 24 |
  • 25 | In this example, when fetching the data we return{' '} 26 | notFound() for{' '} 27 | Categories and{' '} 28 | 29 | Sub Categories 30 | {' '} 31 | that do not exist. This renders the closest appropriate{' '} 32 | not-found.js. 33 |
  • 34 |
  • 35 | 36 | Note: not-found.js currently only renders when 37 | triggered by the notFound() function. We're 38 | working on support for catching unmatched routes (404). 39 | 40 |
  • 41 |
42 | 43 |
44 | 45 | Docs 46 | 47 | 48 | 49 | Code 50 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/streaming/_components/header.tsx: -------------------------------------------------------------------------------- 1 | import { NextLogoLight } from '#/ui/next-logo'; 2 | import { 3 | MagnifyingGlassIcon, 4 | ShoppingCartIcon, 5 | } from '@heroicons/react/24/solid'; 6 | import { CartCount } from './cart-count'; 7 | import { Link } from '@hiogawa/react-server/client'; 8 | 9 | export function Header() { 10 | return ( 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 | User 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /vercel-app-playground/src/routes/patterns/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '#/ui/external-link'; 2 | import { Link } from '@hiogawa/react-server/client'; 3 | import clsx from 'clsx'; 4 | 5 | const items = [ 6 | { 7 | name: 'Active links', 8 | slug: 'active-links', 9 | description: 'Update the style of the current active link', 10 | }, 11 | { 12 | name: 'Breadcrumbs', 13 | slug: 'breadcrumbs', 14 | description: 'Shared server-side Breadcrumb UI using Parallel Routes', 15 | }, 16 | { 17 | name: 'Updating URL search params', 18 | slug: 'search-params', 19 | description: 'Update searchParams using `useRouter` and ``', 20 | ok: true, 21 | }, 22 | ]; 23 | 24 | export default function Page() { 25 | return ( 26 |
27 | Patterns 28 |

Patterns

29 | 30 |
31 | {items.map((item) => { 32 | return ( 33 | 42 |
43 | {item.name} 44 |
45 | 46 | {item.description ? ( 47 |
48 | {item.description} 49 |
50 | ) : null} 51 | 52 | ); 53 | })} 54 |
55 | 56 |
57 | 58 | Code 59 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/footer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default function Footer({ 4 | reactVersion, 5 | nextVersion, 6 | }: { 7 | reactVersion: string; 8 | nextVersion: string; 9 | }) { 10 | return ( 11 |
12 | {/* @ts-expect-error */} 13 | 25 | 26 | 27 | Powered by 28 | 29 | 33 | 34 | 35 | 36 |
37 |
React: {reactVersion}
38 |
Next: {nextVersion}
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /vercel-app-playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "imports": { 5 | "#/*": "./src/*" 6 | }, 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "vc-build": "SSR_ENTRY=/src/adapters/vercel-edge vite build && bash misc/vercel-edge/build.sh", 12 | "vc-release": "vercel deploy --prebuilt --prod .", 13 | "vc-release-staging": "vercel deploy --prebuilt .", 14 | "test-e2e": "playwright test", 15 | "test-e2e-preview": "E2E_PREVIEW=1 playwright test", 16 | "tsc": "tsc -b", 17 | "tsc-dev": "tsc -b --watch --preserveWatchOutput", 18 | "lint": "prettier --cache --write --ignore-unknown .", 19 | "lint-check": "prettier --cache --check --ignore-unknown ." 20 | }, 21 | "dependencies": { 22 | "@heroicons/react": "2.1.3", 23 | "@hiogawa/react-server": "0.2.3", 24 | "@hiogawa/utils-node": "^0.0.1", 25 | "clsx": "2.1.1", 26 | "date-fns": "3.6.0", 27 | "dinero.js": "2.0.0-alpha.10", 28 | "ms": "3.0.0-canary.1", 29 | "react": "19.0.0-rc.0", 30 | "react-dom": "19.0.0-rc.0", 31 | "react-server-dom-webpack": "19.0.0-rc.0", 32 | "server-only": "0.0.1", 33 | "use-count-up": "3.0.1" 34 | }, 35 | "devDependencies": { 36 | "@hattip/adapter-node": "^0.0.44", 37 | "@hiogawa/vite-plugin-ssr-middleware": "latest", 38 | "@playwright/test": "^1.44.1", 39 | "@tailwindcss/forms": "0.5.7", 40 | "@tailwindcss/typography": "0.5.12", 41 | "@types/ms": "0.7.34", 42 | "@types/node": "20.12.7", 43 | "@types/react": "18.3.1", 44 | "@types/react-dom": "18.3.0", 45 | "@vercel/git-hooks": "1.0.0", 46 | "@vitejs/plugin-react": "^4.2.1", 47 | "autoprefixer": "10.4.19", 48 | "esbuild": "^0.21.4", 49 | "postcss": "8.4.38", 50 | "prettier": "3.2.5", 51 | "prettier-plugin-tailwindcss": "0.5.14", 52 | "tailwindcss": "3.4.3", 53 | "typescript": "5.4.5", 54 | "vite": "latest" 55 | }, 56 | "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" 57 | } 58 | -------------------------------------------------------------------------------- /vercel-app-playground/src/ui/product-card.tsx: -------------------------------------------------------------------------------- 1 | import { Product } from '#/api/products/product'; 2 | import { ProductBestSeller } from '#/ui/product-best-seller'; 3 | import { ProductEstimatedArrival } from '#/ui/product-estimated-arrival'; 4 | import { ProductLowStockWarning } from '#/ui/product-low-stock-warning'; 5 | import { ProductPrice } from '#/ui/product-price'; 6 | import { ProductRating } from '#/ui/product-rating'; 7 | import { ProductUsedPrice } from '#/ui/product-used-price'; 8 | import { Link } from '@hiogawa/react-server/client'; 9 | import { dinero, type DineroSnapshot } from 'dinero.js'; 10 | 11 | export const ProductCard = ({ 12 | product, 13 | href, 14 | }: { 15 | product: Product; 16 | href: string; 17 | }) => { 18 | const price = dinero(product.price as DineroSnapshot); 19 | 20 | return ( 21 | 22 |
23 |
24 | {product.isBestSeller ? ( 25 |
26 | 27 |
28 | ) : null} 29 | {product.name} 36 |
37 | 38 |
39 | {product.name} 40 |
41 | 42 | {product.rating ? : null} 43 | 44 | 45 | 46 | {/* */} 47 | 48 | {product.usedPrice ? ( 49 | 50 | ) : null} 51 | 52 | 53 | 54 | {product.stock <= 1 ? ( 55 | 56 | ) : null} 57 |
58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /remix-tutorial/src/routes/contacts/[contactId]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createError, 3 | redirect, 4 | revalidatePath, 5 | type PageProps, 6 | } from "@hiogawa/react-server/server"; 7 | import { fakeContacts, getContact } from "../../../_data"; 8 | import { BackButton } from "./_client"; 9 | 10 | export default async function EditContact(props: PageProps) { 11 | const contact = await getContact(props.params["contactId"]); 12 | if (!contact) { 13 | throw createError({ status: 404 }); 14 | } 15 | 16 | return ( 17 |
{ 19 | "use server"; 20 | revalidatePath("/"); 21 | await fakeContacts.set(contact.id, Object.fromEntries(formData)); 22 | throw redirect(`/contacts/${contact.id}`); 23 | }} 24 | key={contact.id} 25 | id="contact-form" 26 | > 27 |

28 | Name 29 | 36 | 43 |

44 | 53 | 63 |