├── .gitignore ├── README.md ├── api └── index.js ├── app ├── components │ ├── footer.js │ ├── header.js │ ├── item.js │ ├── item.sc.js │ ├── logo.js │ ├── meta.js │ ├── nav.js │ ├── page.js │ ├── server-info.server.js │ ├── spinner.js │ ├── stories.js │ └── story.js ├── entry.client.jsx ├── entry.server.jsx ├── lib │ ├── fetch-data.js │ ├── get-item.js │ ├── get-story-ids.js │ ├── time-ago.js │ └── use-data.js ├── pages │ ├── _app.js │ ├── _document.js │ ├── csr.js │ ├── index.js │ ├── rsc.server.js │ ├── slow.server.js │ └── ssr.js └── root.jsx ├── jsconfig.json ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── remix.config.js └── vercel.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .cache 4 | .vercel 5 | .output 6 | 7 | public/build 8 | api/build 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Deployment 6 | 7 | After having run the `create-remix` command and selected "Vercel" as a deployment target, you only need to [import your Git repository](https://vercel.com/new) into Vercel, and it will be deployed. 8 | 9 | If you'd like to avoid using a Git repository, you can also deploy the directory by running [Vercel CLI](https://vercel.com/cli): 10 | 11 | ```sh 12 | npm i -g vercel 13 | vercel 14 | ``` 15 | 16 | It is generally recommended to use a Git repository, because future commits will then automatically be deployed by Vercel, through its [Git Integration](https://vercel.com/docs/concepts/git). 17 | 18 | ## Development 19 | 20 | To run your Remix app locally, make sure your project's local dependencies are installed: 21 | 22 | ```sh 23 | npm install 24 | ``` 25 | 26 | Afterwards, start the Remix development server like so: 27 | 28 | ```sh 29 | npm run dev 30 | ``` 31 | 32 | Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go! 33 | 34 | If you're used to using the `vercel dev` command provided by [Vercel CLI](https://vercel.com/cli) instead, you can also use that, but it's not needed. 35 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const { createRequestHandler } = require("@remix-run/vercel"); 2 | 3 | module.exports = createRequestHandler({ 4 | build: require("./build") 5 | }); 6 | -------------------------------------------------------------------------------- /app/components/footer.js: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |
4 | 8 | 9 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/components/item.sc.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { useRouter } from "next/router"; 3 | 4 | import Page from "./page"; 5 | import Item from "./item"; 6 | 7 | import getItem from "../lib/get-item"; 8 | import getComments from "../app/lib/get-comments"; 9 | 10 | let commentsData = {}; 11 | let storyData = {}; 12 | let fetchDataPromise = {}; 13 | 14 | function ItemPageWithData({ id }) { 15 | if (!commentsData[id]) { 16 | if (!fetchDataPromise[id]) { 17 | fetchDataPromise[id] = getItem(id) 18 | .then((story) => { 19 | storyData[id] = story; 20 | return getComments(story.comments); 21 | }) 22 | .then((c) => (commentsData[id] = c)); 23 | } 24 | throw fetchDataPromise[id]; 25 | } 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default function ItemPage() { 35 | const { id } = useRouter().query; 36 | 37 | if (!id) return null; 38 | 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/components/logo.js: -------------------------------------------------------------------------------- 1 | export default function Logo() { 2 | return ( 3 | <> 4 | N 5 | 54 | 55 | ); 56 | -------------------------------------------------------------------------------- /app/components/story.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import timeAgo from "../lib/time-ago"; 4 | 5 | export default function Story({ 6 | id, 7 | title, 8 | date, 9 | url, 10 | user, 11 | score, 12 | commentsCount, 13 | }) { 14 | const { host } = url ? new URL(url) : { host: "#" }; 15 | const [voted, setVoted] = useState(false); 16 | 17 | return ( 18 |
19 |
20 | setVoted(!voted)} 28 | > 29 | ▲ 30 | 31 | {title} 32 | {url && ( 33 | 34 | {host.replace(/^www\./, "")} 35 | 36 | )} 37 |
38 |
39 | {score} {plural(score, "point")} by{" "} 40 | {user}{" "} 41 | 42 | {timeAgo(new Date(date)) /* note: we re-hydrate due to ssr */} ago 43 | {" "} 44 | |{" "} 45 | 46 | {commentsCount} {plural(commentsCount, "comment")} 47 | 48 |
49 |
50 | ); 51 | } 52 | 53 | const plural = (n, s) => s + (n === 0 || n > 1 ? "s" : ""); 54 | -------------------------------------------------------------------------------- /app/entry.client.jsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /app/entry.server.jsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | 4 | export default function handleRequest( 5 | request, 6 | responseStatusCode, 7 | responseHeaders, 8 | remixContext 9 | ) { 10 | let markup = renderToString( 11 | 12 | ); 13 | 14 | responseHeaders.set("Content-Type", "text/html"); 15 | responseHeaders.set("Cache-Control", "s-maxage=5, stale-while-revalidate=5"); 16 | 17 | return new Response("" + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /app/lib/fetch-data.js: -------------------------------------------------------------------------------- 1 | export default async function fetchData(type, delay = 0) { 2 | const [res] = await Promise.all([ 3 | fetch(`https://hacker-news.firebaseio.com/v0/${type}.json`), 4 | new Promise((res) => setTimeout(res, Math.random() * delay)), 5 | ]); 6 | if (res.status !== 200) { 7 | throw new Error(`Status ${res.status}`); 8 | } 9 | return res.json(); 10 | } 11 | -------------------------------------------------------------------------------- /app/lib/get-item.js: -------------------------------------------------------------------------------- 1 | import fetchData from './fetch-data' 2 | 3 | export default async function (id) { 4 | const val = await fetchData(`item/${id}`) 5 | if (val) { 6 | return transform(val) 7 | } else { 8 | return null 9 | } 10 | } 11 | 12 | export function transform(val) { 13 | return { 14 | id: val.id, 15 | url: val.url || '', 16 | user: val.by, 17 | // time is seconds since epoch, not ms 18 | date: new Date(val.time * 1000).getTime() || 0, 19 | // sometimes `kids` is `undefined` 20 | comments: val.kids || [], 21 | commentsCount: val.descendants || 0, 22 | score: val.score, 23 | title: val.title, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/get-story-ids.js: -------------------------------------------------------------------------------- 1 | import fetchData from './fetch-data' 2 | 3 | export default async function ( 4 | type = 'topstories', 5 | { page = 1, max = 30 } = {} 6 | ) { 7 | const start = max * (page - 1) 8 | const end = start + max 9 | const ids = await fetchData(type) 10 | return ids.slice(start, end) 11 | } 12 | -------------------------------------------------------------------------------- /app/lib/time-ago.js: -------------------------------------------------------------------------------- 1 | import ms from 'ms' 2 | 3 | const map = { 4 | s: 'seconds', 5 | ms: 'milliseconds', 6 | m: 'minutes', 7 | h: 'hours', 8 | d: 'days', 9 | } 10 | 11 | export default (date) => 12 | date ? ms(new Date() - date).replace(/[a-z]+/, (str) => ' ' + map[str]) : '' 13 | -------------------------------------------------------------------------------- /app/lib/use-data.js: -------------------------------------------------------------------------------- 1 | const cache = {} 2 | 3 | export default function useData(key, fetcher) { 4 | if (!cache[key]) { 5 | let data 6 | let promise 7 | cache[key] = () => { 8 | if (data !== undefined) return data 9 | if (!promise) promise = fetcher().then((r) => (data = r)) 10 | throw promise 11 | } 12 | } 13 | return cache[key]() 14 | } 15 | -------------------------------------------------------------------------------- /app/pages/_app.js: -------------------------------------------------------------------------------- 1 | function MyApp({ Component, pageProps }) { 2 | return 3 | } 4 | 5 | export default MyApp 6 | -------------------------------------------------------------------------------- /app/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/pages/csr.js: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | // Shared Components 4 | import Spinner from "../components/spinner"; 5 | 6 | // Client Components 7 | import Page from "../components/page"; 8 | import Story from "../components/story"; 9 | 10 | // Utils 11 | import fetchData from "../lib/fetch-data"; 12 | import { transform } from "../lib/get-item"; 13 | import useData from "../lib/use-data"; 14 | 15 | function StoryWithData({ id }) { 16 | const data = useData(`s-${id}`, () => 17 | fetchData(`item/${id}`).then(transform) 18 | ); 19 | return ; 20 | } 21 | 22 | function NewsWithData() { 23 | const storyIds = useData("top", () => fetchData("topstories")); 24 | return ( 25 | <> 26 | {storyIds.slice(0, 30).map((id) => { 27 | return ( 28 | }> 29 | 30 | 31 | ); 32 | })} 33 | 34 | ); 35 | } 36 | 37 | export default function News() { 38 | return ( 39 | 40 | {typeof window === "undefined" ? ( 41 | 42 | ) : ( 43 | }> 44 | 45 | 46 | )} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/pages/index.js: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 |
4 |

React Server Components in Next.js

5 |

Without Streaming

6 |
7 | 8 | Static + Client Side Data Fetching 9 | 10 |
11 |
12 | 13 | 🐌 SSR with API Delays 14 | 15 |
16 |

React Server Components with Streaming

17 |
18 | 19 | RSC + HTTP Streaming 20 | 21 |
22 |
23 | 24 | 🐌 RSC with API Delays + HTTP Streaming 25 | 26 |
27 |
28 |
29 |

30 | 31 | This demo is built with Next.js and React Server Components. Read 32 | about our blog post here:{" "} 33 | 34 | Next.js 12 35 | 36 | . 37 | 38 |

39 |

40 | 41 | Check out the code:{" "} 42 | 43 | https://github.com/vercel/next-rsc-demo 44 | 45 | 46 |

47 |
48 |