├── .gitignore ├── .vscode └── launch.json ├── README.md ├── components └── todo.component.tsx ├── context └── sse.context.tsx ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ └── hello.ts └── index.tsx ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "pwa-chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "npm run dev", 21 | "console": "integratedTerminal", 22 | "serverReadyAction": { 23 | "pattern": "started server on .+, url: (https?://.+)", 24 | "uriFormat": "%s", 25 | "action": "debugWithChrome" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Component-level data fetching with SSR support 2 | 3 | 4 | 5 | > This repo contains the source code for the medium article 6 | 7 | --- 8 | 9 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 10 | 11 | ## Getting Started 12 | 13 | First, run the development server: 14 | 15 | ```bash 16 | npm run dev 17 | # or 18 | yarn dev 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | 23 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 24 | 25 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 26 | 27 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 28 | 29 | ## Learn More 30 | 31 | To learn more about Next.js, take a look at the following resources: 32 | 33 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 34 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 35 | 36 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 37 | 38 | ## Deploy on Vercel 39 | 40 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 41 | 42 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 43 | -------------------------------------------------------------------------------- /components/todo.component.tsx: -------------------------------------------------------------------------------- 1 | import { useSSE } from "use-sse"; 2 | 3 | interface Todo { 4 | userId: number; 5 | id: number; 6 | title: string; 7 | completed: boolean; 8 | } 9 | 10 | export const TodoComponent = () => { 11 | const [todos, error] = useSSE((): Promise => { 12 | return fetch("https://jsonplaceholder.typicode.com/todos").then( 13 | (response: Response) => response.json() 14 | ); 15 | }, []); 16 | 17 | if (error) return {error.message}; 18 | 19 | if (!todos?.length) return Loading...; 20 | 21 | return ( 22 | 23 | Todo List 24 | 25 | {todos.map((todo: Todo) => { 26 | return ( 27 | 28 | {todo.title} 29 | {todo.completed ? "Completed" : "Not Completed"} 30 | 31 | ); 32 | })} 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /context/sse.context.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOMServer from "react-dom/server"; 2 | import { AppContext } from "next/app"; 3 | import { NextComponentType, NextPageContext } from "next"; 4 | import { createServerContext } from "use-sse"; 5 | const compressPayload = require('compress-json') 6 | export const { ServerDataContext, resolveData } = createServerContext(); 7 | 8 | function getOrCreate() { 9 | if (process.browser) { 10 | //decompress the SSR string 11 | // window._initialDataContext = window.__NEXT_DATA__.props.sse 12 | window._initialDataContext = compressPayload.decompress(window.__NEXT_DATA__.props.sse); 13 | return require("use-sse").createBroswerContext(); 14 | } 15 | return ServerDataContext; 16 | } 17 | 18 | export const Context = getOrCreate(); 19 | 20 | type AppPageProps = any; 21 | type WithCTX = NextComponentType; 22 | 23 | export async function initialRender( 24 | appContext: AppContext, 25 | pageProps: AppPageProps 26 | ) { 27 | const WithAppContext: WithCTX = appContext.AppTree; 28 | 29 | ReactDOMServer.renderToString( 30 | 31 | 32 | 33 | ); 34 | 35 | const sse = await resolveData(); 36 | 37 | return sse; 38 | } 39 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "compress-json": "^1.0.5", 12 | "next": "11.1.2", 13 | "react": "17.0.2", 14 | "react-dom": "^17.0.2", 15 | "use-sse": "^2.0.1" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "17.0.30", 19 | "@types/react-dom": "^17.0.9", 20 | "typescript": "4.4.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import App, { AppContext, AppInitialProps, AppProps } from "next/app"; 2 | import { Context, initialRender } from "../context/sse.context"; 3 | 4 | import "../styles/globals.css"; 5 | 6 | export default function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | MyApp.getInitialProps = async (appContext: AppContext) => { 15 | const data: AppInitialProps = await App.getInitialProps(appContext); 16 | const compressPayload = require('compress-json') 17 | const sse = await initialRender(appContext, data); 18 | const compressedSSEData = compressPayload.compress(sse.data) 19 | const b = JSON.stringify(sse.data).length * 2; 20 | const kb = (b / 1024).toFixed(2); 21 | 22 | 23 | const c = JSON.stringify(compressedSSEData).length * 2; 24 | const kb1 = (c / 1024).toFixed(2); 25 | console.log('compressed',kb1,'kb') 26 | console.log('raw',kb,'kb') 27 | 28 | const pageProps = { 29 | ...data, 30 | sse: compressedSSEData, 31 | }; 32 | return pageProps; 33 | }; 34 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default (req: NextApiRequest, res: NextApiResponse) => { 9 | res.status(200).json({ name: 'John Doe' }) 10 | } 11 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import { TodoComponent } from "../components/todo.component"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | export default function Home() { 7 | return ( 8 | 9 | 10 | Create Next App 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexUXUI/component-data-fetching/281da40a68eacbb02c94df7c212a21e1ee26aa6f/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------
{todo.completed ? "Completed" : "Not Completed"}