├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file-text.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── countries-broken │ │ └── page.tsx │ ├── countries-fixed │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── providers.tsx │ └── simple-cases │ │ └── page.tsx ├── components │ ├── button.tsx │ ├── countries-broken.tsx │ ├── countries-fixed.tsx │ ├── input.tsx │ ├── simple-cases.tsx │ ├── table.tsx │ └── very-slow-component.tsx └── helpers │ ├── cn.ts │ ├── log-on-re-render.ts │ └── resources.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Compiler test run 2 | 3 | This is a few code examples of the React Compiler working and not working. They are written to illustrate the article [I tried React Compiler today, and guess what... 😉](https://developerway.com/posts/i-tried-react-compiler) 4 | 5 | ## Getting Started 6 | 7 | Install the dependencies: 8 | 9 | ```bash 10 | npm install --force 11 | ``` 12 | 13 | Run the development server: 14 | 15 | ```bash 16 | npm run dev 17 | ``` 18 | 19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 20 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | reactCompiler: true 5 | } 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-compiler-test", 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 | "dependencies": { 12 | "@radix-ui/react-slot": "^1.0.2", 13 | "@tanstack/react-query": "^5.35.1", 14 | "class-variance-authority": "^0.7.0", 15 | "clsx": "^2.1.1", 16 | "next": "15.0.0-canary.1", 17 | "react": "19.0.0-rc-f994737d14-20240522", 18 | "react-dom": "19.0.0-rc-f994737d14-20240522", 19 | "tailwind-merge": "^2.3.0" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20", 23 | "@types/react": "^18", 24 | "@types/react-dom": "^18", 25 | "babel-plugin-react-compiler": "^0.0.0-experimental-487cb0e-20240529", 26 | "eslint": "^8", 27 | "eslint-config-next": "15.0.0-canary.1", 28 | "postcss": "^8", 29 | "prettier": "^3.2.5", 30 | "tailwindcss": "^3.4.1", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/countries-broken/page.tsx: -------------------------------------------------------------------------------- 1 | import { CountriesBroken } from "@/components/countries-broken"; 2 | 3 | export default async function Page() { 4 | return ( 5 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/countries-fixed/page.tsx: -------------------------------------------------------------------------------- 1 | import { CountriesFixed } from "@/components/countries-fixed"; 2 | 3 | export default async function Page() { 4 | return ( 5 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerway/react-compiler-test/07928e40b0b932bd9f34ddff05259681020d5f1b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerway/react-compiler-test/07928e40b0b932bd9f34ddff05259681020d5f1b/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developerway/react-compiler-test/07928e40b0b932bd9f34ddff05259681020d5f1b/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | --primary: 222.2 47.4% 11.2%; 9 | --primary-foreground: 210 40% 98%; 10 | --secondary: 210 40% 96.1%; 11 | --secondary-foreground: 222.2 47.4% 11.2%; 12 | --accent: 210 40% 96.1%; 13 | --accent-foreground: 222.2 47.4% 11.2%; 14 | --destructive: 0 100% 50%; 15 | --destructive-foreground: 210 40% 98%; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | :root { 20 | --background: #0a0a0a; 21 | --foreground: #ededed; 22 | } 23 | } 24 | 25 | body { 26 | color: var(--foreground); 27 | background: var(--background); 28 | font-family: Arial, Helvetica, sans-serif; 29 | } 30 | 31 | h3 { 32 | font-size: 1.4rem; 33 | font-weight: 600; 34 | margin: 1rem 0; 35 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import "./globals.css"; 5 | import { Providers } from "./providers"; 6 | 7 | const geistSans = localFont({ 8 | src: "./fonts/GeistVF.woff", 9 | variable: "--font-geist-sans", 10 | weight: "100 900", 11 | }); 12 | const geistMono = localFont({ 13 | src: "./fonts/GeistMonoVF.woff", 14 | variable: "--font-geist-mono", 15 | weight: "100 900", 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Create Next App", 20 | description: "Generated by create next app", 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { CountriesBroken } from "@/components/countries-broken"; 2 | import Link from "next/link"; 3 | 4 | export default async function Page() { 5 | return ( 6 |
10 |

11 | Code examples to test out the React Compiler 12 |

13 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | 6 | function makeQueryClient() { 7 | return new QueryClient({ 8 | defaultOptions: { 9 | queries: { 10 | // With SSR, we usually want to set some default staleTime 11 | // above 0 to avoid refetching immediately on the client 12 | staleTime: 60 * 1000, 13 | }, 14 | }, 15 | }); 16 | } 17 | 18 | let browserQueryClient: QueryClient | undefined = undefined; 19 | 20 | function getQueryClient() { 21 | if (typeof window === "undefined") { 22 | // Server: always make a new query client 23 | return makeQueryClient(); 24 | } else { 25 | // Browser: make a new query client if we don't already have one 26 | // This is very important so we don't re-make a new client if React 27 | // suspends during the initial render. This may not be needed if we 28 | // have a suspense boundary BELOW the creation of the query client 29 | if (!browserQueryClient) browserQueryClient = makeQueryClient(); 30 | return browserQueryClient; 31 | } 32 | } 33 | 34 | export function Providers({ children }: { children: React.ReactNode }) { 35 | // NOTE: Avoid useState when initializing the query client if you don't 36 | // have a suspense boundary between this and the code that may 37 | // suspend because React will throw away the client on the initial 38 | // render if it suspends and there is no boundary 39 | const queryClient = getQueryClient(); 40 | 41 | return ( 42 | {children} 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/simple-cases/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SimpleCase1, 3 | SimpleCase1Memo, 4 | SimpleCase2, 5 | SimpleCase2Memo, 6 | SimpleCase2WithCompiler, 7 | SimpleCase3, 8 | SimpleCase3Memo, 9 | SimpleCase3WithCompiler, 10 | SimpleCaseWithCompiler, 11 | } from "@/components/simple-cases"; 12 | import { ReactElement } from "react"; 13 | 14 | const Panel = ({ children }: { children: ReactElement }) => { 15 | return ( 16 |
17 | {children} 18 |
19 | ); 20 | }; 21 | export default async function Page() { 22 | return ( 23 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "@/helpers/cn"; 5 | import { useLogOnReRender } from "@/helpers/log-on-re-render"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | useLogOnReRender("Button"); 47 | return ( 48 | 53 | ); 54 | }, 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /src/components/countries-broken.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Table, 5 | TableBody, 6 | TableCaption, 7 | TableCell, 8 | TableHead, 9 | TableHeader, 10 | TableRow, 11 | } from "@/components/table"; 12 | import Link from "next/link"; 13 | import React, { useState } from "react"; 14 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 15 | import { addCountry, deleteCountry, getCountries } from "@/helpers/resources"; 16 | import { Input } from "@/components/input"; 17 | import { Button } from "@/components/button"; 18 | 19 | export const CountriesBroken = () => { 20 | const [value, setValue] = useState(""); 21 | 22 | const queryClient = useQueryClient(); 23 | 24 | const { data: countries } = useQuery({ 25 | queryKey: ["countries"], 26 | queryFn: async () => { 27 | return getCountries(); 28 | }, 29 | }); 30 | 31 | const deleteCountryMutation = useMutation({ 32 | mutationFn: deleteCountry, 33 | onSuccess: async (_, name) => { 34 | queryClient.setQueryData( 35 | ["countries"], 36 | countries?.filter((country) => country.name !== name), 37 | ); 38 | }, 39 | }); 40 | 41 | const onDelete = (name: string) => { 42 | deleteCountryMutation.mutate(name); 43 | }; 44 | 45 | const addCountryMutation = useMutation({ 46 | mutationFn: addCountry, 47 | onSuccess: async (response) => { 48 | queryClient.setQueryData(["countries"], [...(countries || []), response]); 49 | }, 50 | }); 51 | 52 | const onAddCountry = () => { 53 | addCountryMutation.mutate(value); 54 | setValue(""); 55 | }; 56 | 57 | return ( 58 |
59 |

Compiler example: memoization is broken

60 |

61 |

    62 |
  • Type in input fields - all rows and cells with re-render
  • 63 |
  • Click Add button - all rows and cells with re-render
  • 64 |
  • Click Delete button - all rows and cells with re-render
  • 65 |
66 |

67 | 68 | Supported countries list. 69 | 70 | 71 | Name 72 | Action 73 | 74 | 75 | 76 | {countries?.map(({ name }, index) => ( 77 | 78 | 79 | {name} 80 | 81 | 82 | 85 | 86 | 87 | ))} 88 | 89 |
90 |
91 | setValue(e.target.value)} 96 | /> 97 | 98 |
99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /src/components/countries-fixed.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | /* eslint-disable react/no-unescaped-entities */ 3 | import { 4 | Table, 5 | TableBody, 6 | TableCaption, 7 | TableCell, 8 | TableHead, 9 | TableHeader, 10 | TableRow, 11 | } from "@/components/table"; 12 | import Link from "next/link"; 13 | import React, { useState } from "react"; 14 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 15 | import { addCountry, deleteCountry, getCountries } from "@/helpers/resources"; 16 | import { Input } from "@/components/input"; 17 | import { Button } from "@/components/button"; 18 | import { useLogOnReRender } from "@/helpers/log-on-re-render"; 19 | 20 | const CountryRow = ({ 21 | name, 22 | onDelete, 23 | }: { 24 | name: string; 25 | onDelete: (n: string) => void; 26 | }) => { 27 | useLogOnReRender("CountryRow"); 28 | return ( 29 | 30 | 31 | {name} 32 | 33 | 34 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export const CountriesFixed = () => { 43 | const [value, setValue] = useState(""); 44 | 45 | const queryClient = useQueryClient(); 46 | 47 | const { data: countries } = useQuery({ 48 | queryKey: ["countries"], 49 | queryFn: async () => { 50 | return getCountries(); 51 | }, 52 | }); 53 | 54 | const { mutate: deleteCountryMutation } = useMutation({ 55 | mutationFn: deleteCountry, 56 | onSuccess: async (_, name) => { 57 | queryClient.setQueryData( 58 | ["countries"], 59 | countries?.filter((country) => country.name !== name), 60 | ); 61 | }, 62 | }); 63 | 64 | const onDelete = (name: string) => { 65 | deleteCountryMutation(name); 66 | }; 67 | 68 | const addCountryMutation = useMutation({ 69 | mutationFn: addCountry, 70 | onSuccess: async (response) => { 71 | queryClient.setQueryData(["countries"], [...(countries || []), response]); 72 | }, 73 | }); 74 | 75 | const onAddCountry = () => { 76 | addCountryMutation.mutate(value); 77 | setValue(""); 78 | }; 79 | 80 | return ( 81 |
82 |

Compiler example: memoization is fixed

83 |
    84 |
  • Type in input fields - rows and cells don't re-render
  • 85 |
  • Click Add button - rows and cells don't re-render
  • 86 |
  • Click Delete button - rows and cells don't re-render
  • 87 |
88 | 89 | Supported countries list. 90 | 91 | 92 | Name 93 | Action 94 | 95 | 96 | 97 | {countries?.map(({ name }) => ( 98 | 99 | ))} 100 | 101 |
102 |
103 | setValue(e.target.value)} 108 | /> 109 | 110 |
111 |
112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /src/components/input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cn } from "@/helpers/cn"; 3 | 4 | const Input = React.forwardRef( 5 | ({ className, type, ...props }, ref) => { 6 | return ( 7 | 16 | ); 17 | }, 18 | ); 19 | Input.displayName = "Input"; 20 | 21 | export { Input }; 22 | 23 | export interface InputProps 24 | extends React.InputHTMLAttributes {} 25 | -------------------------------------------------------------------------------- /src/components/simple-cases.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { 4 | ReactElement, 5 | useCallback, 6 | useEffect, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | import { VerySlowComponent, wait } from "@/components/very-slow-component"; 11 | import { Button } from "@/components/button"; 12 | 13 | const Dialog = () => { 14 | return ( 15 |
16 | Imagine that this is a dialog 😉 17 |
18 | ); 19 | }; 20 | 21 | const VerySlowComponentMemo = React.memo(VerySlowComponent); 22 | 23 | export const SimpleCase1 = () => { 24 | "use no memo"; 25 | const [isOpen, setIsOpen] = useState(false); 26 | 27 | return ( 28 | <> 29 |

Simple case: example 1

30 |

31 | Click the button, the dialog will show up with a delay. Slow component 32 | re-render will be visible in the console. 33 |

34 | 35 | {isOpen && } 36 | 37 | 38 | ); 39 | }; 40 | 41 | export const SimpleCase1Memo = () => { 42 | "use no memo"; 43 | const [isOpen, setIsOpen] = useState(false); 44 | 45 | return ( 46 | <> 47 |

48 | Simple case: example 1 (Memoized manually) 49 |

50 |

51 | Click the button, the dialog will show up without the delay. Slow 52 | component re-render will not be visible in the console. 53 |

54 | 55 | {isOpen && } 56 | 57 | 58 | ); 59 | }; 60 | 61 | export const SimpleCaseWithCompiler = () => { 62 | const [isOpen, setIsOpen] = useState(false); 63 | 64 | return ( 65 | <> 66 |

Simple case: example 1 (with Compiler)

67 |

68 | Click the button, the dialog will show up without the delay. Slow 69 | component re-render will not be visible in the console. 70 |

71 | 72 | {isOpen && } 73 | 74 | 75 | ); 76 | }; 77 | 78 | export const SimpleCase2 = () => { 79 | "use no memo"; 80 | const [isOpen, setIsOpen] = useState(false); 81 | 82 | const onSubmit = () => {}; 83 | const data = [{ id: "bla" }]; 84 | 85 | return ( 86 | <> 87 |

Simple case: example 2

88 |

89 | Click the button, the dialog will show up with a delay. Slow component 90 | re-render will be visible in the console. 91 |

92 | 93 | {isOpen && } 94 | 95 | 96 | ); 97 | }; 98 | 99 | export const SimpleCase2Memo = () => { 100 | "use no memo"; 101 | const [isOpen, setIsOpen] = useState(false); 102 | 103 | const onSubmit = useCallback(() => {}, []); 104 | const data = useMemo(() => [{ id: "bla" }], []); 105 | 106 | return ( 107 | <> 108 |

109 | Simple case: example 2 (Memoized manually) 110 |

111 |

112 | Click the button, the dialog will show up without the delay. Slow 113 | component re-render will not be visible in the console. 114 |

115 | 116 | {isOpen && } 117 | 118 | 119 | ); 120 | }; 121 | 122 | export const SimpleCase2WithCompiler = () => { 123 | const [isOpen, setIsOpen] = useState(false); 124 | 125 | const onSubmit = () => {}; 126 | const data = [{ id: "bla" }]; 127 | 128 | return ( 129 | <> 130 |

Simple case: example 2 (with Compiler)

131 |

132 | Click the button, the dialog will show up without the delay. Slow 133 | component re-render will not be visible in the console. 134 |

135 | 136 | {isOpen && } 137 | 138 | 139 | ); 140 | }; 141 | 142 | const SomeOtherComponent = () =>
Some other component
; 143 | export const SimpleCase3 = () => { 144 | "use no memo"; 145 | const [isOpen, setIsOpen] = useState(false); 146 | 147 | return ( 148 | <> 149 |

Simple case: example 3

150 |

151 | Click the button, the dialog will show up with a delay. Slow component 152 | re-render will be visible in the console. 153 |

154 | 155 | {isOpen && } 156 | 157 | 158 | 159 | 160 | ); 161 | }; 162 | 163 | export const SimpleCase3Memo = () => { 164 | "use no memo"; 165 | const [isOpen, setIsOpen] = useState(false); 166 | 167 | const child = useMemo(() => , []); 168 | 169 | return ( 170 | <> 171 |

172 | Simple case: example 3 (Memoized manually) 173 |

174 |

175 | Click the button, the dialog will show up without the delay. Slow 176 | component re-render will not be visible in the console. 177 |

178 | 179 | {isOpen && } 180 | {child} 181 | 182 | ); 183 | }; 184 | 185 | export const SimpleCase3WithCompiler = () => { 186 | const [isOpen, setIsOpen] = useState(false); 187 | 188 | return ( 189 | <> 190 |

Simple case: example 3 (with Compiler)

191 |

192 | Click the button, the dialog will show up without the delay. Slow 193 | component re-render will not be visible in the console. 194 |

195 | 196 | {isOpen && } 197 | 198 | 199 | 200 | 201 | ); 202 | }; 203 | -------------------------------------------------------------------------------- /src/components/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ClassValue, clsx } from "clsx"; 3 | import { cn } from "@/helpers/cn"; 4 | import { useLogOnReRender } from "@/helpers/log-on-re-render"; 5 | 6 | const Table = React.forwardRef< 7 | HTMLTableElement, 8 | React.HTMLAttributes 9 | >(({ className, ...props }, ref) => { 10 | useLogOnReRender("Table"); 11 | return ( 12 |
13 | 18 | 19 | ); 20 | }); 21 | Table.displayName = "Table"; 22 | 23 | const TableHeader = React.forwardRef< 24 | HTMLTableSectionElement, 25 | React.HTMLAttributes 26 | >(({ className, ...props }, ref) => { 27 | useLogOnReRender("TableHeader"); 28 | return ( 29 | 30 | ); 31 | }); 32 | TableHeader.displayName = "TableHeader"; 33 | 34 | const TableBody = React.forwardRef< 35 | HTMLTableSectionElement, 36 | React.HTMLAttributes 37 | >(({ className, ...props }, ref) => { 38 | useLogOnReRender("TableBody"); 39 | return ( 40 | 45 | ); 46 | }); 47 | TableBody.displayName = "TableBody"; 48 | 49 | const TableFooter = React.forwardRef< 50 | HTMLTableSectionElement, 51 | React.HTMLAttributes 52 | >(({ className, ...props }, ref) => { 53 | useLogOnReRender("TableFooter"); 54 | return ( 55 | tr]:last:border-b-0", 59 | className, 60 | )} 61 | {...props} 62 | /> 63 | ); 64 | }); 65 | TableFooter.displayName = "TableFooter"; 66 | 67 | const TableRow = React.forwardRef< 68 | HTMLTableRowElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => { 71 | useLogOnReRender("TableRow"); 72 | return ( 73 | 81 | ); 82 | }); 83 | TableRow.displayName = "TableRow"; 84 | 85 | const TableHead = React.forwardRef< 86 | HTMLTableCellElement, 87 | React.ThHTMLAttributes 88 | >(({ className, ...props }, ref) => { 89 | useLogOnReRender("TableHead"); 90 | return ( 91 |
[role=checkbox]]:translate-y-[2px]", 95 | className, 96 | )} 97 | {...props} 98 | /> 99 | ); 100 | }); 101 | TableHead.displayName = "TableHead"; 102 | 103 | const TableCell = React.forwardRef< 104 | HTMLTableCellElement, 105 | React.TdHTMLAttributes 106 | >(({ className, ...props }, ref) => { 107 | useLogOnReRender("TableCell"); 108 | return ( 109 | [role=checkbox]]:translate-y-[2px]", 113 | className, 114 | )} 115 | {...props} 116 | /> 117 | ); 118 | }); 119 | TableCell.displayName = "TableCell"; 120 | 121 | const TableCaption = React.forwardRef< 122 | HTMLTableCaptionElement, 123 | React.HTMLAttributes 124 | >(({ className, ...props }, ref) => { 125 | useLogOnReRender("TableCaption"); 126 | return ( 127 |
132 | ); 133 | }); 134 | TableCaption.displayName = "TableCaption"; 135 | 136 | export { 137 | Table, 138 | TableBody, 139 | TableCaption, 140 | TableCell, 141 | TableFooter, 142 | TableHead, 143 | TableHeader, 144 | TableRow, 145 | }; 146 | -------------------------------------------------------------------------------- /src/components/very-slow-component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | export const wait = (ms: number) => { 4 | const start = Date.now(); 5 | let now = start; 6 | 7 | while (now - start < ms) now = Date.now(); 8 | }; 9 | 10 | export const VerySlowComponent = (_: any) => { 11 | wait(300); 12 | useEffect(() => { 13 | console.log("re-render slow component"); 14 | }); 15 | 16 | return
Slow component
; 17 | }; 18 | -------------------------------------------------------------------------------- /src/helpers/cn.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/log-on-re-render.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useLogOnReRender = (name: string) => { 4 | useEffect(() => { 5 | console.info(`Component ${name} re-rendered`); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/resources.ts: -------------------------------------------------------------------------------- 1 | type Country = { 2 | name: string; 3 | cities: string[]; 4 | }; 5 | 6 | const countries: Country[] = [ 7 | { 8 | name: "USA", 9 | cities: ["New York", "Los Angeles"], 10 | }, 11 | { 12 | name: "Canada", 13 | cities: ["Toronto", "Vancouver"], 14 | }, 15 | { 16 | name: "Australia", 17 | cities: ["Sydney", "Melbourne"], 18 | }, 19 | { 20 | name: "Germany", 21 | cities: ["Berlin", "Munich"], 22 | }, 23 | { 24 | name: "India", 25 | cities: ["Delhi", "Mumbai"], 26 | }, 27 | ]; 28 | 29 | export const getCountries = async (): Promise => { 30 | // just a dummy return to imitate async fetch request 31 | return countries; 32 | }; 33 | 34 | export const deleteCountry = async (name: string): Promise => { 35 | // just a dummy return to imitate async fetch request 36 | return countries; 37 | }; 38 | 39 | export const addCountry = async (name: string): Promise => { 40 | // just a dummy return to imitate async fetch request 41 | return { name: name, cities: [] }; 42 | }; 43 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | primary: "hsl(var(--primary))", 15 | "primary-foreground": "hsl(var(--primary-foreground))", 16 | secondary: "hsl(var(--secondary))", 17 | "secondary-foreground": "hsl(var(--secondary-foreground))", 18 | accent: "hsl(var(--accent))", 19 | "accent-foreground": "hsl(var(--accent-foreground))", 20 | destructive: "hsl(var(--accent))", 21 | "destructive-foreground": "hsl(var(--destructive-foreground))", 22 | }, 23 | }, 24 | }, 25 | plugins: [], 26 | }; 27 | export default config; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------