├── .eslintrc.json ├── .github └── workflows │ └── nextjs.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── jsconfig.json ├── mdx ├── recma.mjs ├── rehype.mjs └── remark.mjs ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── eric-diviney.jpg ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── framework-tweet.png ├── mileta-dulovic.jpg └── site.webmanifest ├── src ├── components │ ├── Accordion.jsx │ ├── Button.jsx │ ├── Card.jsx │ ├── Code.jsx │ ├── Details.tsx │ ├── Expand.jsx │ ├── FAQ.tsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── Heading.jsx │ ├── Layout.jsx │ ├── LeadHeading.jsx │ ├── Libraries.jsx │ ├── Logo.jsx │ ├── MobileNavigation.jsx │ ├── ModeToggle.jsx │ ├── Navigation.jsx │ ├── PillTab.tsx │ ├── Prose.jsx │ ├── Resource.jsx │ ├── Search.jsx │ ├── SectionProvider.jsx │ ├── SimpleTable.jsx │ ├── StepList.jsx │ ├── Summary.tsx │ ├── Tab.jsx │ ├── Tag.jsx │ ├── Topics.jsx │ ├── Tweet.jsx │ ├── mdx.jsx │ └── pages │ │ ├── About.jsx │ │ ├── Frameworks.jsx │ │ ├── Standards-Config.tsx │ │ ├── Standards.tsx │ │ └── State.jsx ├── images │ └── logos │ │ ├── go.svg │ │ ├── handbook.svg │ │ ├── node.svg │ │ ├── php.svg │ │ ├── python.svg │ │ └── ruby.svg ├── lib │ └── remToPx.js ├── pages │ ├── _app.jsx │ ├── _document.jsx │ ├── about.mdx │ ├── automated-testing.mdx │ ├── ecosystem.mdx │ ├── frameworks │ │ ├── alternate-tech-stacks.mdx │ │ ├── index.mdx │ │ ├── nextjs.mdx │ │ └── react-native.mdx │ ├── fundamentals.mdx │ ├── hooks.mdx │ ├── index.mdx │ ├── project-standards.mdx │ ├── react-performance-optimization.mdx │ ├── semantics.mdx │ ├── state-management.mdx │ ├── styling.mdx │ ├── team │ │ └── eric-diviney.mdx │ └── topics.mdx └── styles │ └── tailwind.css ├── tailwind.config.js ├── tsconfig.json └── typography.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["main"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow one concurrent deployment 22 | concurrency: 23 | group: "pages" 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | # Build job 28 | build: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Detect package manager 34 | id: detect-package-manager 35 | run: | 36 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 37 | echo "manager=yarn" >> $GITHUB_OUTPUT 38 | echo "command=install" >> $GITHUB_OUTPUT 39 | echo "runner=yarn" >> $GITHUB_OUTPUT 40 | exit 0 41 | elif [ -f "${{ github.workspace }}/package.json" ]; then 42 | echo "manager=npm" >> $GITHUB_OUTPUT 43 | echo "command=ci" >> $GITHUB_OUTPUT 44 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 45 | exit 0 46 | else 47 | echo "Unable to determine packager manager" 48 | exit 1 49 | fi 50 | - name: Setup Node 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: "16" 54 | cache: ${{ steps.detect-package-manager.outputs.manager }} 55 | - name: Setup Pages 56 | uses: actions/configure-pages@v3 57 | with: 58 | # Automatically inject basePath in your Next.js configuration file and disable 59 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 60 | # 61 | # You may remove this line if you want to manage the configuration yourself. 62 | static_site_generator: next 63 | - name: Restore cache 64 | uses: actions/cache@v3 65 | with: 66 | path: | 67 | .next/cache 68 | # Generate a new cache whenever packages or source files change. 69 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 70 | # If source files changed but packages didn't, rebuild from a prior cache. 71 | restore-keys: | 72 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 73 | - name: Install dependencies 74 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 75 | - name: Build with Next.js 76 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 77 | - name: Static HTML export with Next.js 78 | run: ${{ steps.detect-package-manager.outputs.runner }} next export 79 | - name: Upload artifact 80 | uses: actions/upload-pages-artifact@v1 81 | with: 82 | path: ./out 83 | 84 | # Deployment job 85 | deploy: 86 | environment: 87 | name: github-pages 88 | url: ${{ steps.deployment.outputs.page_url }} 89 | runs-on: ubuntu-latest 90 | needs: build 91 | steps: 92 | - name: Deploy to GitHub Pages 93 | id: deployment 94 | uses: actions/deploy-pages@v1 95 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # idea 36 | .idea 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ericdiviney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚛🤌 React Handbook 2 | 3 | Modern approaches to architecture and feature development in React apps. 4 | 5 | ### Come chat with us on [discord](https://discord.gg/SbDwke7wpy). 6 | 7 | ## Suggestions 8 | 9 | To start a discussion about a particular idea you'd like to champion (or think we should be covering), please open an issue in our GitHub repository with the label `Question` or join us on Discord and start the conversation there. 10 | 11 | ## Opening Pull Requests 12 | 13 | Fork the repo, open your PR's into `develop`. We plan releases on an ad-hoc basis, but generally they happen everytime we make content changes. 14 | 15 | In general, we'll look at just about any pull request opened, but we'd really appreciate PR's that tackle [bugs/issues](https://github.com/ericdiviney/react-handbook/labels/good%20first%20issue). 16 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mdx/recma.mjs: -------------------------------------------------------------------------------- 1 | import { mdxAnnotations } from 'mdx-annotations' 2 | import recmaNextjsStaticProps from 'recma-nextjs-static-props' 3 | 4 | function recmaRemoveNamedExports() { 5 | return (tree) => { 6 | tree.body = tree.body.map((node) => { 7 | if (node.type === 'ExportNamedDeclaration') { 8 | return node.declaration 9 | } 10 | return node 11 | }) 12 | } 13 | } 14 | 15 | export const recmaPlugins = [ 16 | mdxAnnotations.recma, 17 | recmaRemoveNamedExports, 18 | recmaNextjsStaticProps, 19 | ] 20 | -------------------------------------------------------------------------------- /mdx/rehype.mjs: -------------------------------------------------------------------------------- 1 | import { mdxAnnotations } from 'mdx-annotations' 2 | import { visit } from 'unist-util-visit' 3 | import rehypeMdxTitle from 'rehype-mdx-title' 4 | import shiki from 'shiki' 5 | import { toString } from 'mdast-util-to-string' 6 | import * as acorn from 'acorn' 7 | import { slugifyWithCounter } from '@sindresorhus/slugify' 8 | 9 | function rehypeParseCodeBlocks() { 10 | return (tree) => { 11 | visit(tree, 'element', (node, _nodeIndex, parentNode) => { 12 | if (node.tagName === 'code' && node.properties.className) { 13 | parentNode.properties.language = node.properties.className[0]?.replace( 14 | /^language-/, 15 | '' 16 | ) 17 | } 18 | }) 19 | } 20 | } 21 | 22 | let highlighter 23 | 24 | function rehypeShiki() { 25 | return async (tree) => { 26 | highlighter = 27 | highlighter ?? (await shiki.getHighlighter({ theme: 'css-variables' })) 28 | 29 | visit(tree, 'element', (node) => { 30 | if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') { 31 | let codeNode = node.children[0] 32 | let textNode = codeNode.children[0] 33 | 34 | node.properties.code = textNode.value 35 | 36 | if (node.properties.language) { 37 | let tokens = highlighter.codeToThemedTokens( 38 | textNode.value, 39 | node.properties.language 40 | ) 41 | 42 | textNode.value = shiki.renderToHtml(tokens, { 43 | elements: { 44 | pre: ({ children }) => children, 45 | code: ({ children }) => children, 46 | line: ({ children }) => `${children}`, 47 | }, 48 | }) 49 | } 50 | } 51 | }) 52 | } 53 | } 54 | 55 | function rehypeSlugify() { 56 | return (tree) => { 57 | let slugify = slugifyWithCounter() 58 | visit(tree, 'element', (node) => { 59 | if (node.tagName === 'h2' && !node.properties.id) { 60 | node.properties.id = slugify(toString(node)) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | function rehypeAddMDXExports(getExports) { 67 | return (tree) => { 68 | let exports = Object.entries(getExports(tree)) 69 | 70 | for (let [name, value] of exports) { 71 | for (let node of tree.children) { 72 | if ( 73 | node.type === 'mdxjsEsm' && 74 | new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value) 75 | ) { 76 | return 77 | } 78 | } 79 | 80 | let exportStr = `export const ${name} = ${value}` 81 | 82 | tree.children.push({ 83 | type: 'mdxjsEsm', 84 | value: exportStr, 85 | data: { 86 | estree: acorn.parse(exportStr, { 87 | sourceType: 'module', 88 | ecmaVersion: 'latest', 89 | }), 90 | }, 91 | }) 92 | } 93 | } 94 | } 95 | 96 | function getSections(node) { 97 | let sections = [] 98 | 99 | for (let child of node.children ?? []) { 100 | if (child.type === 'element' && child.tagName === 'h2') { 101 | sections.push(`{ 102 | title: ${JSON.stringify(toString(child))}, 103 | id: ${JSON.stringify(child.properties.id)}, 104 | ...${child.properties.annotation} 105 | }`) 106 | } else if (child.children) { 107 | sections.push(...getSections(child)) 108 | } 109 | } 110 | 111 | return sections 112 | } 113 | 114 | export const rehypePlugins = [ 115 | mdxAnnotations.rehype, 116 | rehypeParseCodeBlocks, 117 | rehypeShiki, 118 | rehypeSlugify, 119 | rehypeMdxTitle, 120 | [ 121 | rehypeAddMDXExports, 122 | (tree) => ({ 123 | sections: `[${getSections(tree).join()}]`, 124 | }), 125 | ], 126 | ] 127 | -------------------------------------------------------------------------------- /mdx/remark.mjs: -------------------------------------------------------------------------------- 1 | import { mdxAnnotations } from 'mdx-annotations' 2 | import remarkGfm from 'remark-gfm' 3 | 4 | export const remarkPlugins = [mdxAnnotations.remark, remarkGfm] 5 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextMDX from '@next/mdx' 2 | import { remarkPlugins } from './mdx/remark.mjs' 3 | import { rehypePlugins } from './mdx/rehype.mjs' 4 | import { recmaPlugins } from './mdx/recma.mjs' 5 | 6 | const withMDX = nextMDX({ 7 | options: { 8 | remarkPlugins, 9 | rehypePlugins, 10 | recmaPlugins, 11 | providerImportSource: '@mdx-js/react', 12 | }, 13 | }) 14 | 15 | /** @type {import('next').NextConfig} */ 16 | const nextConfig = { 17 | reactStrictMode: true, 18 | pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'], 19 | experimental: { 20 | scrollRestoration: true, 21 | }, 22 | compiler: { 23 | removeConsole: process.env.NODE_ENV === "production", 24 | }, 25 | async redirects() { 26 | return [ 27 | { 28 | source: '/project-structure', 29 | destination: '/project-standards', 30 | permanent: true, 31 | }, 32 | { 33 | source: '/react-native-project-standards', 34 | destination: '/frameworks/react-native', 35 | permanent: true, 36 | }, 37 | ] 38 | }, 39 | } 40 | 41 | export default withMDX(nextConfig) 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindui-protocol", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "browserslist": "defaults, not ie <= 11", 12 | "dependencies": { 13 | "@algolia/autocomplete-core": "^1.7.3", 14 | "@algolia/autocomplete-preset-algolia": "^1.7.3", 15 | "@headlessui/react": "^1.7.7", 16 | "@mdx-js/loader": "^2.1.5", 17 | "@mdx-js/react": "^2.1.5", 18 | "@next/mdx": "^13.0.3", 19 | "@radix-ui/react-accordion": "^1.1.1", 20 | "@radix-ui/react-hover-card": "^1.0.5", 21 | "@radix-ui/react-scroll-area": "^1.0.3", 22 | "@radix-ui/react-tabs": "^1.0.3", 23 | "@sindresorhus/slugify": "^2.1.1", 24 | "@tailwindcss/typography": "^0.5.8", 25 | "@vercel/analytics": "^0.1.11", 26 | "acorn": "^8.8.1", 27 | "algoliasearch": "^4.14.2", 28 | "autoprefixer": "^10.4.7", 29 | "clsx": "^1.2.0", 30 | "focus-visible": "^5.2.0", 31 | "framer-motion": "7.8.1", 32 | "mdast-util-to-string": "^3.1.0", 33 | "mdx-annotations": "^0.1.1", 34 | "next": "13.0.2", 35 | "postcss-focus-visible": "^6.0.4", 36 | "react": "18.2.0", 37 | "react-dom": "18.2.0", 38 | "react-twitter-embed": "^4.0.4", 39 | "recma-nextjs-static-props": "^1.0.0", 40 | "rehype-mdx-title": "^2.0.0", 41 | "remark-gfm": "^3.0.1", 42 | "shiki": "^0.11.1", 43 | "tailwindcss": "^3.2.4", 44 | "unist-util-visit": "^4.1.1", 45 | "zustand": "^4.3.2" 46 | }, 47 | "devDependencies": { 48 | "eslint": "8.26.0", 49 | "eslint-config-next": "13.0.2", 50 | "prettier": "^2.7.1", 51 | "prettier-plugin-tailwindcss": "^0.1.13" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | 'postcss-focus-visible': { 5 | replaceWith: '[data-focus-visible-added]', 6 | }, 7 | autoprefixer: {}, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | semi: false, 4 | plugins: [require('prettier-plugin-tailwindcss')], 5 | } 6 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/eric-diviney.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/eric-diviney.jpg -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/favicon.ico -------------------------------------------------------------------------------- /public/framework-tweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/framework-tweet.png -------------------------------------------------------------------------------- /public/mileta-dulovic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericdiviney/react-handbook/a8b0624d3981054fa6cd751899d60e86f5aeaf71/public/mileta-dulovic.jpg -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/components/Accordion.jsx: -------------------------------------------------------------------------------- 1 | import * as RadixAccordion from '@radix-ui/react-accordion' 2 | import { clsx } from 'clsx' 3 | 4 | export function Accordion({ children, defaultValue }) { 5 | return {children} 6 | } 7 | 8 | export function AccordionItem({ children, className, ...props }) { 9 | return ( 10 | 17 | {children} 18 | 19 | ) 20 | } 21 | 22 | export function AccordionTrigger({ children, title, ...props }) { 23 | return ( 24 | 25 | setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 240)} 27 | className={clsx( 28 | 'group relative my-1 flex w-full cursor-pointer flex-col rounded-lg border pl-9 text-left', 29 | 'border-slate-300 transition duration-150', 30 | 'data-[state=closed]:hover:border-zinc-600 data-[state=closed]:dark:border-zinc-800 data-[state=closed]:dark:hover:border-zinc-600', 31 | 'data-[state=open]:border-sky-400' 32 | )} 33 | {...props} 34 | > 35 |
36 | 37 | {title} 38 | 39 | {children} 40 |
41 | 50 | 55 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export function AccordionContent({ children, className, ...props }) { 62 | return ( 63 | 70 |
{children}
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | 4 | function ArrowIcon(props) { 5 | return ( 6 | 14 | ) 15 | } 16 | 17 | const variantStyles = { 18 | primary: 19 | 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-sky-400/10 dark:text-sky-400 dark:ring-1 dark:ring-inset dark:ring-sky-400/20 dark:hover:bg-sky-400/10 dark:hover:text-sky-300 dark:hover:ring-sky-300', 20 | secondary: 21 | 'rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300', 22 | filled: 23 | 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-sky-500 dark:text-white dark:hover:bg-sky-400', 24 | outline: 25 | 'rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white', 26 | text: 'text-sky-500 hover:text-sky-600 dark:text-sky-400 dark:hover:text-sky-500', 27 | } 28 | 29 | export function Button({ 30 | variant = 'primary', 31 | className = '', 32 | children, 33 | arrow, 34 | ...props 35 | }) { 36 | let Component = props.href ? Link : 'button' 37 | 38 | className = clsx( 39 | 'inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition', 40 | variantStyles[variant], 41 | className 42 | ) 43 | 44 | let arrowIcon = ( 45 | 53 | ) 54 | 55 | let arrowDownIcon = ( 56 | 61 | ) 62 | 63 | let arrowUpIcon = ( 64 | 69 | ) 70 | 71 | return ( 72 | 73 | {arrow === 'left' && arrowIcon} 74 | {children} 75 | {arrow === 'right' && arrowIcon} 76 | {arrow === 'down' && arrowDownIcon} 77 | {arrow === 'up' && arrowUpIcon} 78 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | function CardIcon({ icon: Icon }) { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export function Card({ resource }) { 12 | return ( 13 |
17 |
18 |
19 | {resource.icon && } 20 |

21 | 22 | 23 | {resource.name} 24 | 25 |

26 |

27 | {resource.description} 28 |

29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Code.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Children, 3 | createContext, 4 | useContext, 5 | useEffect, 6 | useRef, 7 | useState, 8 | } from 'react' 9 | import { Tab } from '@headlessui/react' 10 | import clsx from 'clsx' 11 | import { create } from 'zustand' 12 | 13 | import { Tag } from '@/components/Tag' 14 | 15 | const languageNames = { 16 | js: 'JavaScript', 17 | ts: 'TypeScript', 18 | javascript: 'JavaScript', 19 | typescript: 'TypeScript', 20 | php: 'PHP', 21 | python: 'Python', 22 | ruby: 'Ruby', 23 | go: 'Go', 24 | } 25 | 26 | function getPanelTitle({ title, language }) { 27 | return title ?? languageNames[language] ?? 'Code' 28 | } 29 | 30 | function ClipboardIcon(props) { 31 | return ( 32 | 43 | ) 44 | } 45 | 46 | function CopyButton({ code }) { 47 | let [copyCount, setCopyCount] = useState(0) 48 | let copied = copyCount > 0 49 | 50 | useEffect(() => { 51 | if (copyCount > 0) { 52 | let timeout = setTimeout(() => setCopyCount(0), 1000) 53 | return () => { 54 | clearTimeout(timeout) 55 | } 56 | } 57 | }, [copyCount]) 58 | 59 | function copyToClipboard() { 60 | window.navigator.clipboard.writeText(code).then(() => { 61 | setCopyCount((count) => count + 1) 62 | }) 63 | } 64 | 65 | return ( 66 | 96 | ) 97 | } 98 | 99 | function CodePanelHeader({ tag, label }) { 100 | if (!tag && !label) { 101 | return null 102 | } 103 | 104 | return ( 105 |
106 | {tag && ( 107 |
108 | {tag} 109 |
110 | )} 111 | {tag && label && ( 112 | 113 | )} 114 | {label && ( 115 | {label} 116 | )} 117 |
118 | ) 119 | } 120 | 121 | function CodePanel({ tag, label, code, children }) { 122 | let child = Children.only(children) 123 | 124 | return ( 125 |
126 | 130 |
131 |
{children}
132 | 133 |
134 |
135 | ) 136 | } 137 | 138 | function CodeGroupHeader({ title, children, selectedIndex }) { 139 | let hasTabs = Children.count(children) > 1 140 | 141 | if (!title && !hasTabs) { 142 | return null 143 | } 144 | 145 | return ( 146 |
147 | {title && ( 148 |

149 | {title} 150 |

151 | )} 152 | {hasTabs && ( 153 | 154 | {Children.map(children, (child, childIndex) => ( 155 | 163 | {getPanelTitle(child.props)} 164 | 165 | ))} 166 | 167 | )} 168 |
169 | ) 170 | } 171 | 172 | function CodeGroupPanels({ children, ...props }) { 173 | let hasTabs = Children.count(children) > 1 174 | 175 | if (hasTabs) { 176 | return ( 177 | 178 | {Children.map(children, (child) => ( 179 | 180 | {child} 181 | 182 | ))} 183 | 184 | ) 185 | } 186 | 187 | return {children} 188 | } 189 | 190 | function usePreventLayoutShift() { 191 | let positionRef = useRef() 192 | let rafRef = useRef() 193 | 194 | useEffect(() => { 195 | return () => { 196 | window.cancelAnimationFrame(rafRef.current) 197 | } 198 | }, []) 199 | 200 | return { 201 | positionRef, 202 | preventLayoutShift(callback) { 203 | let initialTop = positionRef.current.getBoundingClientRect().top 204 | 205 | callback() 206 | 207 | rafRef.current = window.requestAnimationFrame(() => { 208 | let newTop = positionRef.current.getBoundingClientRect().top 209 | window.scrollBy(0, newTop - initialTop) 210 | }) 211 | }, 212 | } 213 | } 214 | 215 | const usePreferredLanguageStore = create((set) => ({ 216 | preferredLanguages: [], 217 | addPreferredLanguage: (language) => 218 | set((state) => ({ 219 | preferredLanguages: [ 220 | ...state.preferredLanguages.filter( 221 | (preferredLanguage) => preferredLanguage !== language 222 | ), 223 | language, 224 | ], 225 | })), 226 | })) 227 | 228 | function useTabGroupProps(availableLanguages) { 229 | let { preferredLanguages, addPreferredLanguage } = usePreferredLanguageStore() 230 | let [selectedIndex, setSelectedIndex] = useState(0) 231 | let activeLanguage = [...availableLanguages].sort( 232 | (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a) 233 | )[0] 234 | let languageIndex = availableLanguages.indexOf(activeLanguage) 235 | let newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex 236 | if (newSelectedIndex !== selectedIndex) { 237 | setSelectedIndex(newSelectedIndex) 238 | } 239 | 240 | let { positionRef, preventLayoutShift } = usePreventLayoutShift() 241 | 242 | return { 243 | as: 'div', 244 | ref: positionRef, 245 | selectedIndex, 246 | onChange: (newSelectedIndex) => { 247 | preventLayoutShift(() => 248 | addPreferredLanguage(availableLanguages[newSelectedIndex]) 249 | ) 250 | }, 251 | } 252 | } 253 | 254 | const CodeGroupContext = createContext(false) 255 | 256 | export function CodeGroup({ children, title, ...props }) { 257 | let languages = Children.map(children, (child) => getPanelTitle(child.props)) 258 | let tabGroupProps = useTabGroupProps(languages) 259 | let hasTabs = Children.count(children) > 1 260 | let Container = hasTabs ? Tab.Group : 'div' 261 | let containerProps = hasTabs ? tabGroupProps : {} 262 | let headerProps = hasTabs 263 | ? { selectedIndex: tabGroupProps.selectedIndex } 264 | : {} 265 | 266 | return ( 267 | 268 | 272 | 273 | {children} 274 | 275 | {children} 276 | 277 | 278 | ) 279 | } 280 | 281 | export function Code({ children, ...props }) { 282 | let isGrouped = useContext(CodeGroupContext) 283 | 284 | if (isGrouped) { 285 | return 286 | } 287 | 288 | return {children} 289 | } 290 | 291 | export function Pre({ children, ...props }) { 292 | let isGrouped = useContext(CodeGroupContext) 293 | 294 | if (isGrouped) { 295 | return children 296 | } 297 | 298 | return {children} 299 | } 300 | -------------------------------------------------------------------------------- /src/components/Details.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { Button } from '@/components/Button' 3 | import { PropsWithChildren, useState } from 'react' 4 | 5 | interface DetailsProps { 6 | icon: 'learn' | 'contribute' | 'resources' 7 | label: string 8 | } 9 | 10 | const Icons = { 11 | 'contribute': ( 12 | 20 | 25 | 26 | ), 27 | 'learn': ( 28 | 36 | 41 | 42 | ), 43 | 'resources': ( 44 | 45 | 46 | 47 | ), 48 | } 49 | 50 | export function Details({ 51 | children, 52 | icon = 'learn', 53 | label = 'Deep Dive', 54 | }: PropsWithChildren) { 55 | const [isOpen, setIsOpen] = useState(false) 56 | 57 | const Icon = Icons[icon]; 58 | 59 | const buttonLabel = icon === 'contribute' ? 'What we need' : 'Read More'; 60 | 61 | function toggleOpen() { 62 | setIsOpen(!isOpen) 63 | } 64 | 65 | return ( 66 |
67 |
68 |
69 | {Icon} 70 | 71 | {label} 72 | 73 |
74 |
75 | 82 |
83 |
84 |
85 |
:first-child]:mt-0 [&>:last-child]:mb-0', 88 | { 89 | 'animate-enterFromTop': isOpen, 90 | 'hidden': !isOpen 91 | } 92 | )} 93 | > 94 | {children} 95 |
96 |
97 |
98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /src/components/Expand.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import clsx from 'clsx' 3 | 4 | export function Expand({ children, parent }) { 5 | const [hidden, setHidden] = useState(true) 6 | 7 | function toggleHidden() { 8 | setHidden(!hidden) 9 | } 10 | 11 | return ( 12 |
13 |
14 | 37 |
38 |
39 |
:first-child]:mt-0 [&>:last-child]:mb-0 px-0 py-2', 42 | { 43 | 'hidden': hidden, 44 | 'animate-enterFromTop': !hidden, 45 | } 46 | )} 47 | > 48 | {children} 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/FAQ.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as AccordionPrimitive from '@radix-ui/react-accordion' 3 | import { clsx } from 'clsx' 4 | 5 | export function FAQ({ children }) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | 13 | export function Question({ children, value }) { 14 | return ( 15 | 19 | {children} 20 | 21 | ) 22 | } 23 | 24 | export function QuestionText({ children }) { 25 | return 26 | svg]:rotate-180 hover:text-zinc-600 dark:hover:text-sky-200', 29 | )} 30 | > 31 | {children} 32 | 40 | 45 | 46 | 47 | 48 | } 49 | 50 | export function Answer({ children }) { 51 | return ( 52 | 57 |
{children}
58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { useRouter } from 'next/router' 3 | 4 | import { Button } from '@/components/Button' 5 | import { navigation } from '@/components/Navigation' 6 | import { Resource } from '@/components/Resource' 7 | 8 | function Feedback() { 9 | return ( 10 |
11 |

12 | Have recommendations?{' '} 13 | 16 | Create an issue 17 | {' '} 18 | on our GitHub repository to start a discussion. 19 |

20 |
21 | ) 22 | } 23 | 24 | function PageLink({ label, page, previous = false }) { 25 | return ( 26 | <> 27 | 35 | 41 | {page.title} 42 | 43 | 44 | ) 45 | } 46 | 47 | function PageNavigation() { 48 | let router = useRouter() 49 | let allPages = navigation.flatMap((group) => group.links) 50 | let currentPageIndex = allPages.findIndex( 51 | (page) => page.href === router.pathname 52 | ) 53 | 54 | if (currentPageIndex === -1) { 55 | return null 56 | } 57 | 58 | let previousPage = allPages[currentPageIndex - 1] 59 | let nextPage = allPages[currentPageIndex + 1] 60 | 61 | if (!previousPage && !nextPage) { 62 | return null 63 | } 64 | 65 | return ( 66 |
67 | {previousPage && ( 68 |
69 | 70 |
71 | )} 72 | {nextPage && ( 73 |
74 | 75 |
76 | )} 77 |
78 | ) 79 | } 80 | 81 | function GitHubIcon(props) { 82 | return ( 83 | 90 | ) 91 | } 92 | 93 | function SocialLink({ href, icon: Icon, children }) { 94 | return ( 95 | 101 | {children} 102 | 103 | 104 | ) 105 | } 106 | 107 | function SmallPrint() { 108 | return ( 109 | <> 110 | 111 |
112 |

113 | © Copyright {new Date().getFullYear()}. All rights reserved. 114 |

115 | 116 |
117 | 118 | ) 119 | } 120 | 121 | export function SocialLinksList() { 122 | return ( 123 |
124 | 128 | Follow on GitHub 129 | 130 |
131 | ); 132 | } 133 | 134 | export function Footer() { 135 | return ( 136 |
137 | 138 | 139 |
140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import Link from 'next/link' 3 | import clsx from 'clsx' 4 | import { motion, useScroll, useTransform } from 'framer-motion' 5 | 6 | import { Logo } from '@/components/Logo' 7 | import { SocialLinksList } from '@/components/Footer' 8 | import { 9 | MobileNavigation, 10 | useIsInsideMobileNavigation, 11 | } from '@/components/MobileNavigation' 12 | import { useMobileNavigationStore } from '@/components/MobileNavigation' 13 | import { ModeToggle } from '@/components/ModeToggle' 14 | import { MobileSearch, Search } from '@/components/Search' 15 | 16 | function TopLevelNavItem({ href, children }) { 17 | return ( 18 |
  • 19 | 23 | {children} 24 | 25 |
  • 26 | ) 27 | } 28 | 29 | export const Header = forwardRef(function Header({ className }, ref) { 30 | let { isOpen: mobileNavIsOpen } = useMobileNavigationStore() 31 | let isInsideMobileNavigation = useIsInsideMobileNavigation() 32 | 33 | let { scrollY } = useScroll() 34 | let bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9]) 35 | let bgOpacityDark = useTransform(scrollY, [0, 72], [0.2, 0.8]) 36 | 37 | return ( 38 | 54 |
    61 |
    62 | 63 | 64 | 65 |
    66 | 67 |
    68 | 69 | 70 | 71 | 72 |
    73 |
    74 | 82 |
    83 |
    84 | 85 | 86 |
    87 |
    88 | 89 | ) 90 | }) 91 | -------------------------------------------------------------------------------- /src/components/Heading.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import Link from 'next/link' 3 | import { useInView } from 'framer-motion' 4 | 5 | import { useSectionStore } from '@/components/SectionProvider' 6 | import { Tag } from '@/components/Tag' 7 | import { remToPx } from '@/lib/remToPx' 8 | 9 | function AnchorIcon(props) { 10 | return ( 11 | 20 | ) 21 | } 22 | 23 | function Eyebrow({ tag, label }) { 24 | if (!tag && !label) { 25 | return null 26 | } 27 | 28 | return ( 29 |
    30 | {tag && {tag}} 31 | {tag && label && ( 32 | 33 | )} 34 | {label && ( 35 | {label} 36 | )} 37 |
    38 | ) 39 | } 40 | 41 | function Anchor({ id, inView, children }) { 42 | return ( 43 | 47 | {inView && ( 48 |
    49 |
    50 | 51 |
    52 |
    53 | )} 54 | {children} 55 | 56 | ) 57 | } 58 | 59 | export function Heading({ 60 | level = 2, 61 | children, 62 | id, 63 | tag, 64 | label, 65 | anchor = true, 66 | ...props 67 | }) { 68 | let Component = `h${level}` 69 | let ref = useRef() 70 | let registerHeading = useSectionStore((s) => s.registerHeading) 71 | 72 | let inView = useInView(ref, { 73 | margin: `${remToPx(-3.5)}px 0px 0px 0px`, 74 | amount: 'all', 75 | }) 76 | 77 | useEffect(() => { 78 | if (level === 2) { 79 | registerHeading({ id, ref, offsetRem: tag || label ? 8 : 6 }) 80 | } 81 | }) 82 | 83 | return ( 84 | <> 85 | 86 | 92 | {anchor ? ( 93 | 94 | {children} 95 | 96 | ) : ( 97 | children 98 | )} 99 | 100 | 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { motion } from 'framer-motion' 3 | import * as ScrollArea from '@radix-ui/react-scroll-area' 4 | 5 | import { Footer } from '@/components/Footer' 6 | import { Header } from '@/components/Header' 7 | import { Logo } from '@/components/Logo' 8 | import { Navigation } from '@/components/Navigation' 9 | import { Prose } from '@/components/Prose' 10 | import { SectionProvider } from '@/components/SectionProvider' 11 | import clsx from 'clsx' 12 | import { useState } from 'react' 13 | 14 | const storageKey = 'sidebar'; 15 | 16 | export function Layout({ children, sections = [] }) { 17 | const [isHoveringNav, setHoveringNav] = useState(false); 18 | const [isNavigationOpen, setNavigationOpen] = useState(false) 19 | 20 | function toggleNavOpen() { 21 | localStorage.setItem(storageKey, !isNavigationOpen); 22 | setNavigationOpen(!isNavigationOpen); 23 | } 24 | 25 | // if user had sidebar pinned open previously - on new page loads we should continue that behavior 26 | useEffect(() => { 27 | const shouldSidebarBeOpen = !(localStorage.getItem(storageKey) === 'false'); 28 | if (shouldSidebarBeOpen) setNavigationOpen(true); 29 | }, []) 30 | 31 | return ( 32 | 33 |
    34 | 38 |
    setHoveringNav(true)} 40 | onMouseLeave={() => setHoveringNav(false)} 41 | data-state={`${isHoveringNav || isNavigationOpen ? 'open' : ''}`} 42 | className={clsx( 43 | 'group contents min-w-[55px] bg-white transition-all dark:bg-zinc-900 lg:pointer-events-auto lg:mt-12 lg:block lg:border-r lg:border-zinc-900/10 lg:px-4 lg:pb-8 lg:dark:border-white/10', 44 | '', 45 | { 46 | 'shadow hover:shadow-xl': !isHoveringNav, 47 | 'shadow-xl': isHoveringNav, 48 | } 49 | )} 50 | > 51 |
    52 | 56 |
    59 | 106 |
    107 | 108 |
    114 |
    124 |
    131 | 132 |
    133 |
    134 |
    135 |
    136 | 140 | 141 | 142 |
    143 |
    144 |
    145 |
    153 |
    154 | {children} 155 |
    156 |
    157 |
    158 |
    159 |
    160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /src/components/LeadHeading.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | export function LeadHeading({ children, className: cn = '' }) { 4 | return ( 5 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Libraries.jsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | import { Button } from '@/components/Button' 4 | import { Heading } from '@/components/Heading' 5 | import logoGo from '@/images/logos/go.svg' 6 | import logoNode from '@/images/logos/node.svg' 7 | import logoPhp from '@/images/logos/php.svg' 8 | import logoPython from '@/images/logos/python.svg' 9 | import logoRuby from '@/images/logos/ruby.svg' 10 | 11 | const libraries = [ 12 | { 13 | href: '#', 14 | name: 'PHP', 15 | description: 16 | 'A popular general-purpose scripting language that is especially suited to web development.', 17 | logo: logoPhp, 18 | }, 19 | { 20 | href: '#', 21 | name: 'Ruby', 22 | description: 23 | 'A dynamic, open source programming language with a focus on simplicity and productivity.', 24 | logo: logoRuby, 25 | }, 26 | { 27 | href: '#', 28 | name: 'Node.js', 29 | description: 30 | 'Node.js® is an open-source, cross-platform JavaScript runtime environment.', 31 | logo: logoNode, 32 | }, 33 | { 34 | href: '#', 35 | name: 'Python', 36 | description: 37 | 'Python is a programming language that lets you work quickly and integrate systems more effectively.', 38 | logo: logoPython, 39 | }, 40 | { 41 | href: '#', 42 | name: 'Go', 43 | description: 44 | 'An open-source programming language supported by Google with built-in concurrency.', 45 | logo: logoGo, 46 | }, 47 | ] 48 | 49 | export function Libraries() { 50 | return ( 51 |
    52 | 53 | Official libraries 54 | 55 |
    56 | {libraries.map((library) => ( 57 |
    58 |
    59 |

    60 | {library.name} 61 |

    62 |

    63 | {library.description} 64 |

    65 |

    66 | 69 |

    70 |
    71 | 77 |
    78 | ))} 79 |
    80 |
    81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | export function Logo(props) { 2 | return ( 3 |
    4 | ⚛ 🤌 5 | React Handbook 6 |
    7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /src/components/MobileNavigation.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, Fragment, useContext } from 'react' 2 | import { Dialog, Transition } from '@headlessui/react' 3 | import { motion } from 'framer-motion' 4 | import { create } from 'zustand' 5 | 6 | import { Header } from '@/components/Header' 7 | import { Navigation } from '@/components/Navigation' 8 | 9 | function MenuIcon(props) { 10 | return ( 11 | 20 | ) 21 | } 22 | 23 | function XIcon(props) { 24 | return ( 25 | 34 | ) 35 | } 36 | 37 | const IsInsideMobileNavigationContext = createContext(false) 38 | 39 | export function useIsInsideMobileNavigation() { 40 | return useContext(IsInsideMobileNavigationContext) 41 | } 42 | 43 | export const useMobileNavigationStore = create((set) => ({ 44 | isOpen: false, 45 | open: () => set({ isOpen: true }), 46 | close: () => set({ isOpen: false }), 47 | toggle: () => set((state) => ({ isOpen: !state.isOpen })), 48 | })) 49 | 50 | export function MobileNavigation() { 51 | let isInsideMobileNavigation = useIsInsideMobileNavigation() 52 | let { isOpen, toggle, close } = useMobileNavigationStore() 53 | let ToggleIcon = isOpen ? XIcon : MenuIcon 54 | 55 | return ( 56 | 57 | 65 | {!isInsideMobileNavigation && ( 66 | 67 | 68 | 77 |
    78 | 79 | 80 | 81 | 90 |
    91 | 92 | 93 | 102 | 106 | 107 | 108 | 109 | 110 |
    111 |
    112 | )} 113 |
    114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /src/components/ModeToggle.jsx: -------------------------------------------------------------------------------- 1 | function SunIcon(props) { 2 | return ( 3 | 10 | ) 11 | } 12 | 13 | function MoonIcon(props) { 14 | return ( 15 | 18 | ) 19 | } 20 | 21 | export function ModeToggle() { 22 | function disableTransitionsTemporarily() { 23 | document.documentElement.classList.add('[&_*]:!transition-none') 24 | window.setTimeout(() => { 25 | document.documentElement.classList.remove('[&_*]:!transition-none') 26 | }, 0) 27 | } 28 | 29 | function toggleMode() { 30 | disableTransitionsTemporarily() 31 | 32 | let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 33 | let isSystemDarkMode = darkModeMediaQuery.matches 34 | let isDarkMode = document.documentElement.classList.toggle('dark') 35 | 36 | if (isDarkMode === isSystemDarkMode) { 37 | delete window.localStorage.isDarkMode 38 | } else { 39 | window.localStorage.isDarkMode = isDarkMode 40 | } 41 | } 42 | 43 | return ( 44 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Navigation.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import Link from 'next/link' 3 | import { useRouter } from 'next/router' 4 | import clsx from 'clsx' 5 | import { AnimatePresence, motion, useIsPresent } from 'framer-motion' 6 | 7 | import { useIsInsideMobileNavigation } from '@/components/MobileNavigation' 8 | import { useSectionStore } from '@/components/SectionProvider' 9 | import { Tag } from '@/components/Tag' 10 | import { remToPx } from '@/lib/remToPx' 11 | 12 | function useInitialValue(value, condition = true) { 13 | let initialValue = useRef(value).current 14 | return condition ? initialValue : value 15 | } 16 | 17 | function TopLevelNavItem({ href, children }) { 18 | return ( 19 |
  • 20 | 24 | {children} 25 | 26 |
  • 27 | ) 28 | } 29 | 30 | function NavLink({ href, tag, active, isAnchorLink = false, children }) { 31 | return ( 32 | 43 | {children} 44 | {tag && ( 45 | 46 | {tag} 47 | 48 | )} 49 | 50 | ) 51 | } 52 | 53 | function VisibleSectionHighlight({ group, pathname }) { 54 | let [sections, visibleSections] = useInitialValue( 55 | [ 56 | useSectionStore((s) => s.sections), 57 | useSectionStore((s) => s.visibleSections), 58 | ], 59 | useIsInsideMobileNavigation() 60 | ) 61 | 62 | let isPresent = useIsPresent() 63 | let firstVisibleSectionIndex = Math.max( 64 | 0, 65 | [{ id: '_top' }, ...sections].findIndex( 66 | (section) => section.id === visibleSections[0] 67 | ) 68 | ) 69 | let itemHeight = remToPx(2) 70 | let height = isPresent 71 | ? Math.max(1, visibleSections.length) * itemHeight 72 | : itemHeight 73 | let top = 74 | group.links.findIndex((link) => link.href === pathname) * itemHeight + 75 | firstVisibleSectionIndex * itemHeight 76 | 77 | return ( 78 | 86 | ) 87 | } 88 | 89 | function ActivePageMarker({ group, pathname }) { 90 | let itemHeight = remToPx(2) 91 | let offset = remToPx(0.25) 92 | let activePageIndex = group.links.findIndex((link) => link.href === pathname) 93 | let top = offset + activePageIndex * itemHeight 94 | 95 | return ( 96 | 104 | ) 105 | } 106 | 107 | function NavigationGroup({ group, className }) { 108 | // If this is the mobile navigation then we always render the initial 109 | // state, so that the state does not change during the close animation. 110 | // The state will still update when we re-open (re-render) the navigation. 111 | let isInsideMobileNavigation = useIsInsideMobileNavigation() 112 | let [router, sections] = useInitialValue( 113 | [useRouter(), useSectionStore((s) => s.sections)], 114 | isInsideMobileNavigation 115 | ) 116 | 117 | let isActiveGroup = 118 | group.links.findIndex((link) => link.href === router.pathname) !== -1 119 | 120 | return ( 121 |
  • 122 | 126 | {group.href ? ( 127 | {group.title} 128 | ) : ( 129 | group.title 130 | )} 131 | 132 |
    133 | 134 | {isActiveGroup && ( 135 | 136 | )} 137 | 138 | 142 | 143 | {isActiveGroup && ( 144 | 145 | )} 146 | 147 |
      148 | {group.links.map((link) => ( 149 | 150 | 151 | {link.title} 152 | 153 | 154 | {link.href === router.pathname && sections.length > 0 && ( 155 | 167 | {sections.map((section) => ( 168 |
    • 169 | 174 | {section.title} 175 | 176 |
    • 177 | ))} 178 |
      179 | )} 180 |
      181 |
      182 | ))} 183 |
    184 |
    185 |
  • 186 | ) 187 | } 188 | 189 | export const navigation = [ 190 | { 191 | title: 'Getting Started', 192 | links: [ 193 | { title: 'Home', href: '/' }, 194 | { title: 'React Fundamentals', href: '/fundamentals' }, 195 | ], 196 | }, 197 | { 198 | title: 'Topics', 199 | href: '/topics', 200 | links: [ 201 | { title: 'Project Standards', href: '/project-standards' }, 202 | { title: 'Accessibility & Semantics', href: '/semantics' }, 203 | { title: 'Styling & UI Libraries', href: '/styling' }, 204 | { title: 'Ecosystem & npm libraries', href: '/ecosystem' }, 205 | { title: 'Proficiency with Hooks', href: '/hooks' }, 206 | { title: 'State Management Fundamentals', href: '/state-management' }, 207 | { 208 | title: 'Performance & Optimization', 209 | href: '/react-performance-optimization', 210 | }, 211 | { title: 'Automated Testing', href: '/automated-testing' }, 212 | ], 213 | }, 214 | { 215 | title: 'React Frameworks', 216 | links: [ 217 | { title: 'Frameworks & Build Tools', href: '/frameworks' }, 218 | { title: 'React Native', href: '/frameworks/react-native' }, 219 | { title: 'Next.js', href: '/frameworks/nextjs' }, 220 | { title: 'Alternate React Stacks', href: '/frameworks/alternate-tech-stacks' }, 221 | { title: 'Remix', href: '#remix', tag: 'coming soon' }, 222 | { title: 'Gatsby', href: '#gatsby', tag: 'coming soon' }, 223 | { title: 'Nx', href: '#nx', tag: 'coming soon' }, 224 | ], 225 | }, 226 | { 227 | title: 'Upcoming & In Progress', 228 | links: [ 229 | { 230 | title: 'RSC (React Server Components) / SSR', 231 | href: '#server-components', 232 | }, 233 | { title: 'Error Handling & Boundaries', href: '#debugging' }, 234 | { title: 'Analytics & Monitoring', href: '#analytics' }, 235 | { title: 'CI/CD Pipelines', href: '#cicd' }, 236 | { title: 'Component Design Patterns', href: '#components-patterns' }, 237 | ], 238 | }, 239 | ] 240 | 241 | export function Navigation(props) { 242 | return ( 243 | 254 | ) 255 | } 256 | -------------------------------------------------------------------------------- /src/components/PillTab.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | import { clsx } from 'clsx' 6 | 7 | const Tabs = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )) 20 | Tabs.displayName = TabsPrimitive.Root.displayName 21 | 22 | const TabsList = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => ( 26 | 34 | )) 35 | TabsList.displayName = TabsPrimitive.List.displayName 36 | 37 | const TabsTrigger = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, ...props }, ref) => ( 41 | 50 | )) 51 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 52 | 53 | const TabsContent = React.forwardRef< 54 | React.ElementRef, 55 | React.ComponentPropsWithoutRef 56 | >(({ className, ...props }, ref) => ( 57 | 65 | )) 66 | TabsContent.displayName = TabsPrimitive.Content.displayName 67 | 68 | export { Tabs as PillTabs, TabsList as PillTabsList, TabsTrigger as PillTabsTrigger, TabsContent as PillTabsContent } 69 | -------------------------------------------------------------------------------- /src/components/Prose.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | export function Prose({ as: Component = 'div', className, ...props }) { 4 | return ( 5 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Resource.jsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx" 2 | import Link from "next/link"; 3 | 4 | // use like 5 | 6 | export function Resource({ children, url, className: cn = '' }) { 7 | // lol, i know this is a shortcut, but it's all I need for now 8 | const linkingOnSite = url.includes('reacthandbook.dev') || url.includes('localhost'); 9 | const beginsWithSlash = url[0] === '/'; // relative urls start with slashes, so this is an easy tell if I'm linking on or off-site 10 | const beginsWithHash = url[0] === '#'; // anchors on the same page start with # 11 | const openNewTab = !linkingOnSite && !beginsWithSlash && !beginsWithHash; 12 | 13 | return ( 14 | <> 15 | 21 | {children} 22 | 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /src/components/SectionProvider.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useContext, 4 | useEffect, 5 | useLayoutEffect, 6 | useState, 7 | } from 'react' 8 | import { createStore, useStore } from 'zustand' 9 | 10 | import { remToPx } from '@/lib/remToPx' 11 | 12 | function createSectionStore(sections) { 13 | return createStore((set) => ({ 14 | sections, 15 | visibleSections: [], 16 | setVisibleSections: (visibleSections) => 17 | set((state) => 18 | state.visibleSections.join() === visibleSections.join() 19 | ? {} 20 | : { visibleSections } 21 | ), 22 | registerHeading: ({ id, ref, offsetRem }) => 23 | set((state) => { 24 | return { 25 | sections: state.sections.map((section) => { 26 | if (section.id === id) { 27 | return { 28 | ...section, 29 | headingRef: ref, 30 | offsetRem, 31 | } 32 | } 33 | return section 34 | }), 35 | } 36 | }), 37 | })) 38 | } 39 | 40 | function useVisibleSections(sectionStore) { 41 | let setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections) 42 | let sections = useStore(sectionStore, (s) => s.sections) 43 | 44 | useEffect(() => { 45 | function checkVisibleSections() { 46 | let { innerHeight, scrollY } = window 47 | let newVisibleSections = [] 48 | 49 | for ( 50 | let sectionIndex = 0; 51 | sectionIndex < sections.length; 52 | sectionIndex++ 53 | ) { 54 | let { id, headingRef, offsetRem } = sections[sectionIndex] 55 | let offset = remToPx(offsetRem) 56 | let top = headingRef.current.getBoundingClientRect().top + scrollY 57 | 58 | if (sectionIndex === 0 && top - offset > scrollY) { 59 | newVisibleSections.push('_top') 60 | } 61 | 62 | let nextSection = sections[sectionIndex + 1] 63 | let bottom = 64 | (nextSection?.headingRef.current.getBoundingClientRect().top ?? 65 | Infinity) + 66 | scrollY - 67 | remToPx(nextSection?.offsetRem ?? 0) 68 | 69 | if ( 70 | (top > scrollY && top < scrollY + innerHeight) || 71 | (bottom > scrollY && bottom < scrollY + innerHeight) || 72 | (top <= scrollY && bottom >= scrollY + innerHeight) 73 | ) { 74 | newVisibleSections.push(id) 75 | } 76 | } 77 | 78 | setVisibleSections(newVisibleSections) 79 | } 80 | 81 | let raf = window.requestAnimationFrame(() => checkVisibleSections()) 82 | window.addEventListener('scroll', checkVisibleSections, { passive: true }) 83 | window.addEventListener('resize', checkVisibleSections) 84 | 85 | return () => { 86 | window.cancelAnimationFrame(raf) 87 | window.removeEventListener('scroll', checkVisibleSections) 88 | window.removeEventListener('resize', checkVisibleSections) 89 | } 90 | }, [setVisibleSections, sections]) 91 | } 92 | 93 | const SectionStoreContext = createContext() 94 | 95 | const useIsomorphicLayoutEffect = 96 | typeof window === 'undefined' ? useEffect : useLayoutEffect 97 | 98 | export function SectionProvider({ sections, children }) { 99 | let [sectionStore] = useState(() => createSectionStore(sections)) 100 | 101 | useVisibleSections(sectionStore) 102 | 103 | useIsomorphicLayoutEffect(() => { 104 | sectionStore.setState({ sections }) 105 | }, [sectionStore, sections]) 106 | 107 | return ( 108 | 109 | {children} 110 | 111 | ) 112 | } 113 | 114 | export function useSectionStore(selector) { 115 | let store = useContext(SectionStoreContext) 116 | return useStore(store, selector) 117 | } 118 | -------------------------------------------------------------------------------- /src/components/SimpleTable.jsx: -------------------------------------------------------------------------------- 1 | export function SimpleTable({ children }) { 2 | return ( 3 |
    4 |
    {children}
    5 |
    6 | ) 7 | } 8 | 9 | export function SimpleRow({ children, header = false }) { 10 | if (header) { 11 | return ( 12 |
    13 | {children} 14 |
    15 | ) 16 | } 17 | return ( 18 |
    19 | {children} 20 |
    21 | ) 22 | } 23 | 24 | export function SimpleCell({ children, header = false }) { 25 | if (header) { 26 | return ( 27 |
    28 |
    29 | {children} 30 |
    31 |
    32 | ) 33 | } 34 | return ( 35 |
    36 |
    37 | {children} 38 |
    39 |
    40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/StepList.jsx: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | 3 | export function Step({ children, last }) { 4 | return ( 5 |
  • 6 | {/* vertical gray bar */} 7 |
    15 |
    16 |
    17 | {/* bullet marker */} 18 |
    19 |
    20 |
    21 | {/* list text */} 22 |
    23 | {children} 24 |
    25 |
  • 26 | ) 27 | } 28 | 29 | export function StepList({ children, nested, className = '' }) { 30 | return ( 31 |
      41 | {children} 42 |
    43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Summary.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | 3 | export function Summary({ children, title = 'What You\'ll Learn' }) { 4 | return ( 5 |
    12 |
    13 | 14 | 15 | 16 |

    17 | {title} 18 |

    19 |
    20 |
    21 | {children} 22 |
    23 |
    24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Tab.jsx: -------------------------------------------------------------------------------- 1 | import * as RadixTab from '@radix-ui/react-tabs' 2 | 3 | import clsx from 'clsx' 4 | import * as React from 'react' 5 | 6 | export function Tabs({ children, defaultValue }) { 7 | return {children} 8 | } 9 | 10 | export function TabList({ children, label }) { 11 | return ( 12 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | export function TabTrigger({ tabValue, title, children }) { 22 | return ( 23 | 31 |
    32 | 33 | {title} 34 | 35 | {children} 36 | 44 | 49 | 50 |
    51 |
    52 | ) 53 | } 54 | 55 | export function Tab({ children, value }) { 56 | return ( 57 | 61 | {children} 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Tag.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | const variantStyles = { 4 | medium: 'rounded-lg px-1.5 ring-1 ring-inset', 5 | } 6 | 7 | const colorStyles = { 8 | sky: { 9 | small: 'text-sky-500 dark:text-sky-400', 10 | medium: 11 | 'ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-400', 12 | }, 13 | amber: { 14 | small: 'text-amber-500', 15 | medium: 16 | 'ring-amber-300 bg-amber-400/10 text-amber-500 dark:ring-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400', 17 | }, 18 | rose: { 19 | small: 'text-red-500 dark:text-rose-500', 20 | medium: 21 | 'ring-rose-200 bg-rose-50 text-red-500 dark:ring-rose-500/20 dark:bg-rose-400/10 dark:text-rose-400', 22 | }, 23 | zinc: { 24 | small: 'text-zinc-400 dark:text-zinc-500', 25 | medium: 26 | 'ring-zinc-200 bg-zinc-50 text-zinc-500 dark:ring-zinc-500/20 dark:bg-zinc-400/10 dark:text-zinc-400', 27 | }, 28 | } 29 | 30 | const valueColorMap = { 31 | get: 'sky', 32 | post: 'sky', 33 | put: 'amber', 34 | delete: 'rose', 35 | } 36 | 37 | export function Tag({ 38 | children, 39 | variant = 'medium', 40 | color = valueColorMap[children.toLowerCase()] ?? 'sky', 41 | }) { 42 | return ( 43 | 50 | {children} 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Topics.jsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@/components/Card' 2 | 3 | const topics = [ 4 | { 5 | name: 'React Frameworks & Build Tools', 6 | description: 7 | 'What to consider when starting a new React project (or potential migration). Dive into the world of React meta-frameworks and the tools that power them.', 8 | href: '/frameworks', 9 | }, 10 | { 11 | name: 'Project Standards', 12 | description: 13 | 'In React you can do just about anything you want. But, what *should* you do with your folders and components?', 14 | href: '/project-standards', 15 | }, 16 | { 17 | name: 'React Ecosystem', 18 | description: 19 | 'Popular libraries for common problems (handling dates/date-times, animations, data visualization, etc.)', 20 | href: '/ecosystem', 21 | }, 22 | { 23 | name: 'Accessibility & Semantics', 24 | description: 25 | 'Use the proper markup in every situation. Ensure that everyone can access your application.', 26 | href: '/semantics', 27 | }, 28 | { 29 | name: 'Styling & UI Libraries', 30 | description: 31 | 'Promising approaches for writing/maintaining CSS in your application.', 32 | href: '/styling', 33 | }, 34 | { 35 | name: 'Hooks', 36 | description: 37 | 'Get ahead of the learning curve on using the React API (like useState and useEffect).', 38 | href: '/hooks', 39 | }, 40 | { 41 | name: 'State Management', 42 | description: 'Manage your SPA application state with confidence.', 43 | href: '/state-management', 44 | }, 45 | { 46 | name: 'React Performance & Optimization', 47 | description: 48 | 'Ship fast and performant UIs, a great start to positive UX.', 49 | href: '/react-performance-optimization', 50 | }, 51 | ] 52 | 53 | export function Topics() { 54 | return ( 55 |
    56 |
    57 | {topics.map((resource) => ( 58 | 59 | ))} 60 |
    61 |
    62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Tweet.jsx: -------------------------------------------------------------------------------- 1 | import { TwitterTweetEmbed } from 'react-twitter-embed'; 2 | 3 | export function Tweet({ tweetId }) { 4 | return ( 5 |
    6 | 7 |
    8 | ); 9 | } -------------------------------------------------------------------------------- /src/components/mdx.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | import { Heading } from '@/components/Heading' 4 | 5 | export const a = Link; 6 | export { CodeGroup, Code as code, Pre as pre } from '@/components/Code'; 7 | export * from '@/components/Accordion' 8 | export { Button } from '@/components/Button'; 9 | export * from '@/components/Details'; 10 | export * from '@/components/Expand'; 11 | export * from '@/components/FAQ'; 12 | export * from '@/components/LeadHeading' 13 | export * from '@/components/PillTab'; 14 | export * from '@/components/Resource'; 15 | export * from '@/components/Summary'; 16 | export * from '@/components/StepList'; 17 | export * from '@/components/Tab'; 18 | export * from '@/components/Tweet'; 19 | 20 | export const h2 = function H2(props) { 21 | return 22 | } 23 | 24 | function InfoIcon(props) { 25 | return ( 26 | 37 | ) 38 | } 39 | 40 | export function Note({ children }) { 41 | return ( 42 |
    43 | 44 |
    45 | {children} 46 |
    47 |
    48 | ) 49 | } 50 | 51 | export function Row({ children }) { 52 | return ( 53 |
    54 | {children} 55 |
    56 | ) 57 | } 58 | 59 | export function Col({ children, sticky = false }) { 60 | return ( 61 |
    :first-child]:mt-0 [&>:last-child]:mb-0', 64 | sticky && 'xl:sticky xl:top-24' 65 | )} 66 | > 67 | {children} 68 |
    69 | ) 70 | } 71 | 72 | export function Properties({ children }) { 73 | return ( 74 |
    75 |
      79 | {children} 80 |
    81 |
    82 | ) 83 | } 84 | 85 | export function Property({ name, type, children }) { 86 | return ( 87 |
  • 88 |
    89 |
    Name
    90 |
    91 | {name} 92 |
    93 |
    Type
    94 |
    95 | {type} 96 |
    97 |
    Description
    98 |
    99 | {children} 100 |
    101 |
    102 |
  • 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /src/components/pages/About.jsx: -------------------------------------------------------------------------------- 1 | import * as HoverCard from '@radix-ui/react-hover-card' 2 | import { Resource } from '@/components/Resource' 3 | 4 | const core = [ 5 | { 6 | name: 'Eric Diviney', 7 | description: 'Software Engineer | IBM Consulting', 8 | socials: { 9 | twitter: 'https://twitter.com/EricDiviney', 10 | linkedin: 'https://www.linkedin.com/in/eric-diviney/', 11 | github: 'https://github.com/ericdiviney', 12 | site: 'https://ericdiviney.com/', 13 | }, 14 | image: '/eric-diviney.jpg', 15 | }, 16 | { 17 | name: 'Mileta Dulovic', 18 | description: 'CTO | Optinian', 19 | socials: { 20 | github: 'https://github.com/M1ck0', 21 | linkedin: 'https://www.linkedin.com/in/mileta-dulovic', 22 | site: 'https://miletadulovic.me/', 23 | }, 24 | image: '/mileta-dulovic.jpg', 25 | }, 26 | ] 27 | 28 | export function Contributors() { 29 | return ( 30 |
    31 |
    32 |
    33 | 34 | Core Team 35 | 36 |
    37 |
    38 | {core.map((person) => ( 39 | 40 | ))} 41 |
    42 |
    43 | 44 | Special thanks to: 45 | 46 | 47 |
      48 |
    • 49 | Josh Claunch - someone we repeatedly go to for feedback/advice on things I'm writing, and for having very advanced knowledge of state management in React applications 50 |
    • 51 |
    • 52 | Anthony Conklin - someone else we can always rely on for fresh feedback 53 |
    • 54 |
    • 55 | Theo-Flux for being the first person to contribute to the project besides myself, giving us hope that we're working on something worthwhile 56 |
    • 57 |
    • 58 | 59 | Tono Nogueras{' '} 60 | {' '} 61 | for significant contributions to the React Native guide 62 |
    • 63 |
    64 |
    65 |
    66 | 67 |

    68 | Join 10+ other{' '} 69 | 70 | contributors 71 | 72 | , and help us maintain the website as an example of what a solid Next.js 73 | application codebase can look like. 74 |

    75 |
    76 | ) 77 | } 78 | 79 | function MemberCard({ person, large = false }) { 80 | const { description, image, name, socials } = person 81 | 82 | const size = large ? `w-16 h-16` : `w-8 h-8` 83 | 84 | return ( 85 | 86 | 87 |
    88 | 92 | {/* eslint-disable-next-line @next/next/no-img-element */} 93 | 99 | 100 |
    101 |
    102 | 103 | 107 |
    108 | {/* eslint-disable-next-line @next/next/no-img-element */} 109 | 114 |
    115 |
    116 |
    {name}
    117 |
    118 |
    {description}
    119 |
    120 | {socials.twitter && ( 121 | <> 122 | 126 | Twitter 127 | 128 | 129 | )} 130 | {socials.linkedin && ( 131 | <> 132 | 136 | Linkedin 137 | 138 | 139 | )} 140 | {socials.github && ( 141 | <> 142 | 146 | GitHub 147 | 148 | 149 | )} 150 |
    151 |
    152 |
    153 | 154 |
    155 |
    156 |
    157 | ) 158 | } 159 | -------------------------------------------------------------------------------- /src/components/pages/Frameworks.jsx: -------------------------------------------------------------------------------- 1 | import { Resource } from '@/components/Resource' 2 | 3 | export function FrameworkComparisonTable() { 4 | return ( 5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 | 18 | 19 | 23 | 24 | 25 | Gatsby 26 | 27 |
    28 |
    29 |
    30 |
    31 | 37 | 46 | 47 | 48 | 49 | 56 | 60 | 67 | 68 | 69 | 77 | 78 | 79 | 80 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Next.js 95 | 96 |
    97 |
    98 |
    99 |
    100 | 106 | 107 | 113 | 117 | 122 | 123 | 124 | Remix 125 | 126 |
    127 |
    128 |
    129 |
    130 |
    131 |
    Good for:
    132 |
    133 |
    134 | Static websites, Content-driven websites, SEO-friendly websites, 135 | High-traffic sites w/ non-dynamic content (Blogs, Marketing, 136 | Portfolio, Documentation) 137 |
    138 |
    139 | Static websites, SEO-friendly rendering, Fast dynamic apps/sites, 140 | SPAish Support (via export) 141 |
    142 |
    143 | SEO-friendly rendering, Fast dynamic apps/sites, Deploying 144 | "to the edge" 145 |
    146 |
    147 |
    148 |
    149 |
    Learning Resources
    150 |
    151 |
    152 |
    153 | 154 | Gatsby Tutorial 155 | 156 |
    157 |
    158 | 159 | Getting Started With Gatsby 160 | 161 |
    162 |
    163 | 164 | Build a Blog with Markdown & Gatsby 165 | 166 |
    167 |
    168 |
    169 |
    170 | 171 | Next.js Tutorial 172 | 173 |
    174 |
    175 | 176 | Introduction to Next.js by Xiaoru Li 177 | 178 |
    179 |
    180 |
    181 |
    182 | 183 | Remix Tutorial 184 | 185 |
    186 |
    187 | 188 | Up & Running With Remix by Kent C. Dodds 189 | 190 |
    191 |
    192 | 193 | Remix Tutorial with Kent C. Dodds 194 | 195 |
    196 |
    197 |
    198 |
    199 |
    200 |
    Things To Be Aware Of
    201 |
    202 |
    Mostly for SSG only.
    203 |
    204 | Next.js strongly encourages a Node.js runtime to 205 | serve/run the app (not required if you do a static export). Gatsby 206 | and Remix do not. 207 |
    208 |
    209 |
    210 | Remix generally encourages you to move a lot of typical 211 | client-side "state" to the edge/server, sometimes 212 | removing the need to manage a "state" in the frontend 213 | altogether. 214 |
    215 |
    216 | Remix generally encourages you to fetch data for your 217 | apps/components from the edge/server via "loader 218 | functions". 219 |
    220 |
    221 | While the router / nested routes bring a big performance boost 222 | to your app, there's also a bit of a learning curve. This 223 | is a moot point if you're also considering the App Router 224 | in Next.js, as that also comes with a learning curve. 225 |
    226 |
    227 | Remix doesn't support SSG (which in a lot of cases is 228 | probably fine if your goals are simply to deliver a fast user 229 | experience). 230 |
    231 |
    232 |
    233 |
    234 | 235 |
    236 |
    Commercially Backed By:
    237 |
    238 |
    239 | Netlify 240 |
    241 |
    242 | Vercel 243 |
    244 |
    245 | Shopify 246 |
    247 |
    248 |
    249 |
    250 |
    251 | ) 252 | } 253 | -------------------------------------------------------------------------------- /src/components/pages/Standards.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { useState } from 'react' 3 | import { Resource } from '@/components/Resource' 4 | import { 5 | PillTabs, 6 | PillTabsList, 7 | PillTabsTrigger, 8 | PillTabsContent, 9 | } from '@/components/PillTab' 10 | import { bulletproofStructure, scaleStructure, simpleStructure } from './Standards-Config'; 11 | import type { DirectoryExplorerProps, DirectoryItemType, DirectoryItemProps } from './Standards-Config'; 12 | 13 | export function DirectoryExplorerTabs() { 14 | return ( 15 |
    16 | 17 | 18 | 19 | bulletproof-react 20 | 21 | 22 | simple 23 | 24 | 25 | scale-up 26 | 27 | 28 | 29 | This directory structure is based on the{' '} 30 | 31 | bulletproof-react 32 | {' '} 33 | repo - it is an absolute goldmine for good practices and architecture 34 | of large/enterprise React application. 35 | 36 | 37 | 38 | This structure comes directly from{' '} 39 | 40 | Josh Comeau 41 | {' '} 42 | - a very talented author of educational material in the webdev 43 | community. Read more of his React articles{' '} 44 | 45 | here 46 | 47 | . 48 | 49 | 50 | 51 | This structure can be used for medium to large React applications, adapted from{' '} 52 | 53 | Robin Wieruch 54 | {' '} 55 | - a high quality educator and content creator for in-depth tutorials and articles. Visit his blog{' '} 56 | 57 | here 58 | 59 | . 60 | 61 | 62 | 63 |
    64 | ) 65 | } 66 | 67 | export function DirectoryExplorer({ structure }: DirectoryExplorerProps) { 68 | const [selectedItem, setSelectedItem] = useState() 69 | 70 | return ( 71 |
    72 |
    73 |
    74 |
      75 | {structure.map((item, index) => ( 76 | setSelectedItem(item)} 80 | /> 81 | ))} 82 |
    83 |
    84 |
    85 | {selectedItem && ( 86 |
    87 | {selectedItem.description} 88 |
    89 | )} 90 |
    91 |
    92 |
    93 | ) 94 | } 95 | 96 | export function DirectoryItem({ item, onSelect }: DirectoryItemProps) { 97 | const [isHighlighted, setIsHighlighted] = useState(() => { 98 | return item.name === 'src' 99 | }) 100 | 101 | function handleClickItem() { 102 | setIsHighlighted(!isHighlighted) 103 | onSelect(item) 104 | } 105 | 106 | return ( 107 |
  • 108 | 164 | {item.items !== undefined && 165 | Array.isArray(item.items) && 166 | item.items.length > 0 && ( 167 |
      173 | {item.items.map((item, index) => ( 174 | onSelect(item)} 178 | /> 179 | ))} 180 |
    181 | )} 182 |
  • 183 | ) 184 | } 185 | -------------------------------------------------------------------------------- /src/components/pages/State.jsx: -------------------------------------------------------------------------------- 1 | import { LeadHeading } from '@/components/LeadHeading' 2 | import { Resource } from '@/components/Resource' 3 | import { Step, StepList } from '@/components/StepList' 4 | import { SimpleCell, SimpleRow, SimpleTable } from '@/components/SimpleTable' 5 | 6 | // Helpers for the state management page 7 | 8 | export function ColumnStack({ children }) { 9 | return ( 10 |
    {children}
    11 | ) 12 | } 13 | 14 | export function Column({ children }) { 15 | return ( 16 |
    17 | {children} 18 |
    19 | ) 20 | } 21 | 22 | export function GreenfieldContent() { 23 | return ( 24 | 25 | 26 | 1. Start by lifting state where possible 27 | 28 | 29 | 30 | Choose a State Structure 31 | {' '} 32 | for the data you'll manage 33 | 34 | 35 | Lift state{' '} 36 | 37 | to a parent 38 | 39 | 40 | 41 | Lift state to{' '} 42 | 43 | avoid prop-drilling 44 | 45 | 46 | 47 | Lift state to{' '} 48 | 49 | communicate with sibling components 50 | 51 | 52 | 53 | 54 | Co-locate 55 | {' '} 56 | state near where it is used 57 | 58 | 59 | 60 | 61 | 2. Need More Functionality? 62 | 63 | 64 | For Data-Fetching{' '} 65 | choose one of the following 66 | 67 | 68 | 69 | tanstack-query 70 | {' '} 71 | (REST APIs) 72 | 73 | 74 | 75 | apollo-client 76 | {' '} 77 | (GraphQL) 78 | 79 | 80 | 81 | 82 | For{' '} 83 | 84 | Global Stores 85 | {' '} 86 | choose one of the following 87 | 88 | 89 | 90 | Zustand 91 | {' '} 92 | (FLUX) 93 | 94 | 95 | 96 | Jotai 97 | {' '} 98 | (Atomic) 99 | 100 | 101 | 102 | 103 | For extremely complex state, consider state machines like{' '} 104 | 105 | xState (or the simpler @xState/xStore) 106 | 107 | 108 | 109 | 110 | 111 | ) 112 | } 113 | 114 | export function ReduxContent() { 115 | return ( 116 | 117 | 118 | Redux to the Rescue 119 | 120 | 121 | 122 | Redux Toolkit (RTK) 123 | {' '} 124 | is the modern way to write with Redux 125 | 126 | 127 | Includes a mechanism for{' '} 128 | 129 | Data-Fetching 130 | 131 | 132 | 133 | Includes common{' '} 134 | 135 | middleware 136 | {' '} 137 | out of the box 138 | 139 | Can provide consistency to large projects/teams 140 | 141 | 142 | 143 | ) 144 | } 145 | 146 | export function RecommendationContent() { 147 | return ( 148 | <> 149 | 150 | Starting Points for State Management 151 | 152 | 153 | 154 | Data-Fetching 155 | Store 156 | 157 | 158 | 159 | 160 | tanstack-query 161 | {' '} 162 | (REST APIs) 163 | 164 | 165 | 166 | Zustand 167 | {' '} 168 | or{' '} 169 | Jotai{' '} 170 | or{' '} 171 | MobX 172 | 173 | 174 | 175 | 176 | swr{' '} 177 | (REST APIs) 178 | 179 | 180 | 181 | Zustand 182 | {' '} 183 | or{' '} 184 | Jotai{' '} 185 | or{' '} 186 | MobX 187 | 188 | 189 | 190 | 191 | 192 | apollo-client 193 | {' '} 194 | (GraphQL) 195 | 196 | 197 | 198 | Zustand 199 | {' '} 200 | or{' '} 201 | Jotai{' '} 202 | or{' '} 203 | MobX 204 | 205 | 206 | 207 | 208 | 209 | RTK-Query 210 | {' '} 211 | 212 | 213 | 214 | Redux (RTK) 215 | 216 | 217 | 218 | 219 | 220 | ) 221 | } 222 | -------------------------------------------------------------------------------- /src/images/logos/go.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/images/logos/handbook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/logos/node.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/logos/php.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/images/logos/python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/images/logos/ruby.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /src/lib/remToPx.js: -------------------------------------------------------------------------------- 1 | export function remToPx(remValue) { 2 | let rootFontSize = 3 | typeof window === 'undefined' 4 | ? 16 5 | : parseFloat(window.getComputedStyle(document.documentElement).fontSize) 6 | 7 | return parseFloat(remValue) * rootFontSize 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { Router, useRouter } from 'next/router' 3 | import { MDXProvider } from '@mdx-js/react' 4 | import { Analytics } from '@vercel/analytics/react' 5 | 6 | import { Layout } from '@/components/Layout' 7 | import * as mdxComponents from '@/components/mdx' 8 | import { useMobileNavigationStore } from '@/components/MobileNavigation' 9 | 10 | import '@/styles/tailwind.css' 11 | import 'focus-visible' 12 | 13 | function onRouteChange() { 14 | useMobileNavigationStore.getState().close() 15 | } 16 | 17 | Router.events.on('hashChangeStart', onRouteChange) 18 | Router.events.on('routeChangeComplete', onRouteChange) 19 | Router.events.on('routeChangeError', onRouteChange) 20 | 21 | export default function App({ Component, pageProps }) { 22 | let router = useRouter() 23 | 24 | return ( 25 | <> 26 | 27 | {router.pathname === '/' ? ( 28 | React Handbook 29 | ) : ( 30 | {`${pageProps.title} - React Handbook`} 31 | )} 32 | 33 | 38 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document' 2 | 3 | const modeScript = ` 4 | let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 5 | 6 | updateMode() 7 | darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions) 8 | window.addEventListener('storage', updateModeWithoutTransitions) 9 | 10 | function updateMode() { 11 | let isSystemDarkMode = darkModeMediaQuery.matches 12 | let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode) 13 | 14 | if (isDarkMode) { 15 | document.documentElement.classList.add('dark') 16 | } else { 17 | document.documentElement.classList.remove('dark') 18 | } 19 | 20 | if (isDarkMode === isSystemDarkMode) { 21 | delete window.localStorage.isDarkMode 22 | } 23 | } 24 | 25 | function disableTransitionsTemporarily() { 26 | document.documentElement.classList.add('[&_*]:!transition-none') 27 | window.setTimeout(() => { 28 | document.documentElement.classList.remove('[&_*]:!transition-none') 29 | }, 0) 30 | } 31 | 32 | function updateModeWithoutTransitions() { 33 | disableTransitionsTemporarily() 34 | updateMode() 35 | } 36 | ` 37 | 38 | export default function Document() { 39 | return ( 40 | 41 | 42 |