├── vitest.setup.ts ├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── components │ ├── FooterLink.tsx │ └── FooterLink.test.tsx ├── globals.css ├── form-showcase │ └── page.tsx ├── react-19-hooks │ ├── page.tsx │ ├── use-hook │ │ └── page.tsx │ ├── use-form-status │ │ └── page.tsx │ ├── action-state │ │ └── page.tsx │ └── use-optimistic │ │ └── page.tsx ├── layout.tsx ├── search │ └── page.tsx ├── page.tsx └── useeffect-showcase │ ├── api-fetching │ └── page.tsx │ ├── page.tsx │ ├── subscriptions │ └── page.tsx │ ├── browser-events │ └── page.tsx │ ├── button-click-state │ └── page.tsx │ ├── external-state │ └── page.tsx │ ├── render-logic │ └── page.tsx │ └── props-calculation │ └── page.tsx ├── .husky ├── pre-commit └── install.mjs ├── .prettierignore ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── postcss.config.mjs ├── .prettierrc ├── vitest.config.ts ├── tailwind.config.ts ├── .eslintrc.json ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── package.json ├── .vscode └── launch.json └── README.md /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimeloper/nextjs-v15-starter/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run lint-staged 4 | exec >/dev/tty 2>&1 5 | 6 | npx lint-staged 7 | -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimeloper/nextjs-v15-starter/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | public 4 | .next 5 | *.min.js 6 | *.min.css 7 | dist 8 | build 9 | coverage -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dimeloper/nextjs-v15-starter/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 100, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /.husky/install.mjs: -------------------------------------------------------------------------------- 1 | // Skip Husky install in production and CI 2 | if (process.env.NODE_ENV === 'production' || process.env.CI === 'true') { 3 | process.exit(0); 4 | } 5 | 6 | const husky = (await import('husky')).default; 7 | console.log(husky()); 8 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | globals: true, 8 | environment: 'jsdom', 9 | setupFiles: ['./vitest.setup.ts'], 10 | coverage: { 11 | reporter: ['text', 'json', 'html'], 12 | exclude: ['node_modules/', 'vitest.setup.ts'], 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: 'var(--background)', 13 | foreground: 'var(--foreground)', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@typescript-eslint/recommended", "next/core-web-vitals", "prettier"], 3 | "plugins": ["@typescript-eslint", "prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "react/self-closing-comp": "error", 7 | "react/jsx-curly-brace-presence": [ 8 | "error", 9 | { "props": "never", "children": "never", "propElementValues": "always" } 10 | ], 11 | "jsx-quotes": ["off", "prefer-double"], 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "react/no-unescaped-entities": "off", 14 | "react-hooks/exhaustive-deps": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/components/FooterLink.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | interface FooterLinkProps { 4 | href: string; 5 | iconSrc: string; 6 | iconAlt: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | export function FooterLink({ href, iconSrc, iconAlt, children }: FooterLinkProps) { 11 | return ( 12 | 18 | {iconAlt} 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.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 | .idea 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # env files (can opt-in for commiting if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /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 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer components { 24 | .enhanced-text-visibility { 25 | & .bg-white :is(h1, h2, h3, h4, h5, h6, label, span, p), 26 | & .bg-white [class*='font-'], 27 | & .bg-gray-50 :is(h1, h2, h3, h4, h5, h6, label, span, p), 28 | & .bg-gray-50 [class*='font-'] { 29 | @apply text-gray-900; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Format 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | name: Lint and Format 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '20' 24 | cache: 'yarn' 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Check formatting 30 | run: yarn format:check 31 | 32 | - name: Run ESLint 33 | run: yarn lint 34 | 35 | - name: Run TypeScript check 36 | run: yarn tsc --noEmit 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '20' 24 | cache: 'yarn' 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Run tests 30 | run: yarn test:coverage 31 | 32 | - name: Upload coverage reports 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: coverage-report 36 | path: coverage/ 37 | if-no-files-found: error 38 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/form-showcase/page.tsx: -------------------------------------------------------------------------------- 1 | import Form from 'next/form'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |

Next v15 Form Component

7 |

Which saves us from a lot of boilerplate code.

8 |
9 | {/* On submission, the input value will be appended to 10 | the URL, e.g. /search?query=abc */} 11 | 16 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/react-19-hooks/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function React19HooksExamples() { 4 | return ( 5 |
6 |

React 19 Hooks Examples

7 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/react-19-hooks/use-hook/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { use } from 'react'; 5 | 6 | // This would typically be in a separate file 7 | const fetchData = async () => { 8 | // Simulate server delay 9 | await new Promise(resolve => setTimeout(resolve, 1000)); 10 | return 'Data fetched successfully!'; 11 | }; 12 | 13 | const DataContainer = ({ dataPromise }: { dataPromise: Promise }) => { 14 | const data = use(dataPromise); 15 | return
{data}
; 16 | }; 17 | 18 | export default function UseHookWithPromise() { 19 | const [dataPromise, setDataPromise] = useState | null>(null); 20 | 21 | const handleClick = () => { 22 | setDataPromise(fetchData()); 23 | }; 24 | 25 | return ( 26 |
27 |

use Hook Example

28 | 31 | {dataPromise && } 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import localFont from 'next/font/local'; 3 | import './globals.css'; 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900', 9 | }); 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900', 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Create Next App', 18 | description: 'Generated by create next app', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 29 |
30 |
{children}
31 |
32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default async function SearchPage({ 4 | searchParams, 5 | }: { 6 | searchParams: Promise<{ query: string }>; 7 | }) { 8 | const params = await searchParams; 9 | 10 | const query = params.query; 11 | 12 | return ( 13 |
14 |

Search Results

15 | {query ? ( 16 |

17 | You searched for: {query} 18 |

19 | ) : ( 20 |

No search query provided.

21 | )} 22 | {/* Here you would typically display search results */} 23 |

24 | In this compoennt we are asynchronously accessing the searchParams. 25 |

26 |
27 | 31 | Back to Search 32 | 33 | 37 | Take me Home 38 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/react-19-hooks/use-form-status/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormStatus } from 'react-dom'; 4 | 5 | // This would typically be in a separate file 6 | const submitAction = async () => { 7 | // Simulate server delay 8 | await new Promise(resolve => setTimeout(resolve, 2000)); 9 | }; 10 | 11 | const Form = () => { 12 | const { pending, data } = useFormStatus(); 13 | 14 | return ( 15 |
16 | 22 | 31 | {pending &&

Submitting {data?.get('username') as string}...

} 32 |
33 | ); 34 | }; 35 | 36 | const UseFormStatusComponent = () => { 37 | return ( 38 |
39 |

useFormStatus Example

40 |
{ 42 | await submitAction(); 43 | }} 44 | className="mb-4" 45 | > 46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default UseFormStatusComponent; 53 | -------------------------------------------------------------------------------- /app/components/FooterLink.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { FooterLink } from './FooterLink'; 4 | 5 | describe('FooterLink', () => { 6 | const defaultProps = { 7 | href: 'https://example.com', 8 | iconSrc: '/test-icon.svg', 9 | iconAlt: 'Test Icon', 10 | children: 'Test Link', 11 | }; 12 | 13 | it('renders with all required props', () => { 14 | render(); 15 | 16 | const link = screen.getByRole('link', { name: /test link/i }); 17 | expect(link).toBeInTheDocument(); 18 | expect(link).toHaveAttribute('href', 'https://example.com'); 19 | expect(link).toHaveAttribute('target', '_blank'); 20 | expect(link).toHaveAttribute('rel', 'noopener noreferrer'); 21 | }); 22 | 23 | it('renders the icon with correct attributes', () => { 24 | render(); 25 | 26 | const icon = screen.getByRole('img', { hidden: true }); 27 | expect(icon).toHaveAttribute('src'); 28 | expect(icon).toHaveAttribute('alt', 'Test Icon'); 29 | expect(icon).toHaveAttribute('width', '16'); 30 | expect(icon).toHaveAttribute('height', '16'); 31 | }); 32 | 33 | it('applies correct styling classes', () => { 34 | render(); 35 | 36 | const link = screen.getByRole('link', { name: /test link/i }); 37 | expect(link).toHaveClass( 38 | 'flex', 39 | 'items-center', 40 | 'gap-2', 41 | 'hover:underline', 42 | 'hover:underline-offset-4' 43 | ); 44 | }); 45 | 46 | it('renders children content', () => { 47 | const testContent = 'Custom Link Text'; 48 | render({testContent}); 49 | 50 | expect(screen.getByText(testContent)).toBeInTheDocument(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "lint:fix": "next lint --fix", 11 | "format": "prettier --write .", 12 | "format:check": "prettier --check .", 13 | "prepare": "node .husky/install.mjs", 14 | "tsc": "tsc", 15 | "test": "vitest", 16 | "test:coverage": "vitest run --coverage" 17 | }, 18 | "dependencies": { 19 | "next": "15.5.0", 20 | "react": "19.1.1", 21 | "react-dom": "19.1.1" 22 | }, 23 | "devDependencies": { 24 | "@testing-library/dom": "^10.4.0", 25 | "@testing-library/jest-dom": "^6.6.3", 26 | "@testing-library/react": "^16.3.0", 27 | "@testing-library/user-event": "^14.6.1", 28 | "@types/node": "^22", 29 | "@types/react": "19.1.11", 30 | "@types/react-dom": "19.1.7", 31 | "@vitejs/plugin-react": "^4.4.1", 32 | "@vitest/coverage-v8": "^3.1.2", 33 | "eslint": "^8", 34 | "eslint-config-next": "15.5.0", 35 | "eslint-config-prettier": "^9.1.0", 36 | "eslint-plugin-prettier": "^5.1.2", 37 | "husky": "^9.1.7", 38 | "jsdom": "^26.1.0", 39 | "lint-staged": "^15.5.1", 40 | "postcss": "^8", 41 | "prettier": "^3.3.3", 42 | "tailwindcss": "^3.4.1", 43 | "typescript": "^5", 44 | "vite": "^6.3.3", 45 | "vitest": "^3.1.2" 46 | }, 47 | "volta": { 48 | "node": "20.18.0", 49 | "yarn": "1.22.22" 50 | }, 51 | "resolutions": { 52 | "@types/react": "19.1.11", 53 | "@types/react-dom": "19.1.7" 54 | }, 55 | "lint-staged": { 56 | "*.{js,jsx,ts,tsx}": [ 57 | "prettier --write", 58 | "eslint --fix", 59 | "eslint" 60 | ], 61 | "*.{json,md,yml,yaml}": [ 62 | "prettier --write" 63 | ], 64 | "*.{css,scss}": [ 65 | "prettier --write" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/react-19-hooks/action-state/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useActionState } from 'react'; 4 | 5 | // This would typically be in a separate file 6 | const submitActionWithCurrentState = async (prevState: any, formData: FormData) => { 7 | const username = formData.get('username') as string; 8 | const age = Number(formData.get('age')); 9 | 10 | // Simulate server delay 11 | await new Promise(resolve => setTimeout(resolve, 1000)); 12 | 13 | if (prevState.users.some((user: any) => user.username === username)) { 14 | return { ...prevState, error: `User "${username}" already exists` }; 15 | } 16 | 17 | return { 18 | users: [...prevState.users, { username, age }], 19 | error: null, 20 | }; 21 | }; 22 | 23 | export default function ActionStateComponent() { 24 | const [state, formAction] = useActionState(submitActionWithCurrentState, { 25 | users: [], 26 | error: null, 27 | }); 28 | 29 | return ( 30 |
31 |

useActionState Example

32 |
33 |
34 | 40 | 46 | 52 |
53 |
54 |
{state?.error}
55 | {state?.users?.map((user: any) => ( 56 |
57 | Name: {user.username} Age: {user.age} 58 |
59 | ))} 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Start Dev Server", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 9 | "args": ["dev", "--turbopack"], 10 | "cwd": "${workspaceFolder}", 11 | "console": "integratedTerminal", 12 | "skipFiles": ["/**"] 13 | }, 14 | { 15 | "name": "Debug Next.js App", 16 | "type": "node", 17 | "request": "launch", 18 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 19 | "args": ["dev", "--turbopack"], 20 | "cwd": "${workspaceFolder}", 21 | "console": "integratedTerminal", 22 | "env": { 23 | "NODE_OPTIONS": "--inspect" 24 | }, 25 | "skipFiles": ["/**"], 26 | "runtimeArgs": ["--preserve-symlinks"] 27 | }, 28 | { 29 | "name": "Build", 30 | "type": "node", 31 | "request": "launch", 32 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 33 | "args": ["build"], 34 | "cwd": "${workspaceFolder}", 35 | "console": "integratedTerminal", 36 | "skipFiles": ["/**"] 37 | }, 38 | { 39 | "name": "Start Production", 40 | "type": "node", 41 | "request": "launch", 42 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 43 | "args": ["start"], 44 | "cwd": "${workspaceFolder}", 45 | "console": "integratedTerminal", 46 | "skipFiles": ["/**"] 47 | }, 48 | { 49 | "name": "Run Tests", 50 | "type": "node", 51 | "request": "launch", 52 | "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", 53 | "args": ["run"], 54 | "cwd": "${workspaceFolder}", 55 | "console": "integratedTerminal", 56 | "skipFiles": ["/**"] 57 | }, 58 | { 59 | "name": "Test with Coverage", 60 | "type": "node", 61 | "request": "launch", 62 | "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", 63 | "args": ["run", "--coverage"], 64 | "cwd": "${workspaceFolder}", 65 | "console": "integratedTerminal", 66 | "skipFiles": ["/**"] 67 | }, 68 | { 69 | "name": "Debug Tests", 70 | "type": "node", 71 | "request": "launch", 72 | "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", 73 | "args": ["--inspect-brk", "--no-coverage"], 74 | "cwd": "${workspaceFolder}", 75 | "console": "integratedTerminal", 76 | "skipFiles": ["/**"], 77 | "env": { 78 | "NODE_OPTIONS": "--inspect" 79 | } 80 | }, 81 | { 82 | "name": "Type Check", 83 | "type": "node", 84 | "request": "launch", 85 | "program": "${workspaceFolder}/node_modules/typescript/bin/tsc", 86 | "args": ["--noEmit"], 87 | "cwd": "${workspaceFolder}", 88 | "console": "integratedTerminal", 89 | "skipFiles": ["/**"] 90 | }, 91 | { 92 | "name": "Lint", 93 | "type": "node", 94 | "request": "launch", 95 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 96 | "args": ["lint"], 97 | "cwd": "${workspaceFolder}", 98 | "console": "integratedTerminal", 99 | "skipFiles": ["/**"] 100 | } 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /app/react-19-hooks/use-optimistic/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useOptimistic, useState } from 'react'; 4 | 5 | // This would typically be in a separate file 6 | const submitTitle = async (formData: FormData) => { 7 | // Simulate server delay 8 | await new Promise(resolve => setTimeout(resolve, 1000)); 9 | const newTitle = formData.get('title') as string; 10 | if (newTitle === 'error') { 11 | throw new Error('Title cannot be "error"'); 12 | } 13 | return newTitle; 14 | }; 15 | 16 | export default function OptimisticComponent() { 17 | const [title, setTitle] = useState('Title'); 18 | const [optimisticTitle, setOptimisticTitle] = useOptimistic(title); 19 | const [error, setError] = useState(null); 20 | const [titleHistory, setTitleHistory] = useState([]); 21 | const pending = title !== optimisticTitle; 22 | 23 | const handleSubmit = async (formData: FormData) => { 24 | setError(null); 25 | const newTitle = formData.get('title') as string; 26 | setOptimisticTitle(newTitle); 27 | try { 28 | const updatedTitle = await submitTitle(formData); 29 | setTitle(updatedTitle); 30 | setTitleHistory(prev => [updatedTitle, ...prev]); 31 | } catch (e) { 32 | setError((e as Error).message); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 |

useOptimistic Example

39 | 40 |
41 |

Current Title:

42 |
43 |

44 | {optimisticTitle} 45 |

46 | {pending && Updating...} 47 |
48 |
49 | 50 |
51 |
52 | 58 | 65 |
66 |
67 | 68 | {error && ( 69 |
{error}
70 | )} 71 | 72 | {titleHistory.length > 0 && ( 73 |
74 |

Title History

75 |
    76 | {titleHistory.map((title, index) => ( 77 |
  • 78 | 79 | {new Date().toLocaleTimeString()} 80 | 81 | {title} 82 |
  • 83 | ))} 84 |
85 |
86 | )} 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import { FooterLink } from './components/FooterLink'; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 | Next.js logo 17 |
    18 |
  • 19 | Welcome to v15! Experience the turbopack dev server by editing{' '} 20 | 21 | app/page.tsx 22 | 23 | . 24 |
  • 25 |
  • 26 | This is a starter project for Next.js 15, using volta to manage node 27 | versioning, yarn for dependency management and eslint v9. 28 |
  • 29 |
  • 30 | Check out the following pages to experience the new features of Next. 31 |
  • 32 |
33 | 34 |
35 | 39 | Form Showcase 40 | 41 | 45 | React 19 Hooks 46 | 47 | 51 | useEffect Guide 52 | 53 |
54 |
55 |
56 | 61 | Learn 62 | 63 | 68 | Examples 69 | 70 | 75 | Go to nextjs.org → 76 | 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /app/useeffect-showcase/api-fetching/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | interface User { 7 | id: number; 8 | name: string; 9 | email: string; 10 | company: { 11 | name: string; 12 | }; 13 | } 14 | 15 | export default function ApiFetching() { 16 | const [users, setUsers] = useState([]); 17 | const [loading, setLoading] = useState(true); 18 | const [error, setError] = useState(null); 19 | 20 | // ✅ CORRECT: Use useEffect for API calls 21 | useEffect(() => { 22 | async function fetchUsers() { 23 | try { 24 | setLoading(true); 25 | const response = await fetch('https://jsonplaceholder.typicode.com/users'); 26 | if (!response.ok) { 27 | throw new Error('Failed to fetch users'); 28 | } 29 | const userData = await response.json(); 30 | setUsers(userData); 31 | } catch (err) { 32 | setError(err instanceof Error ? err.message : 'An error occurred'); 33 | } finally { 34 | setLoading(false); 35 | } 36 | } 37 | 38 | fetchUsers(); 39 | }, []); // Empty dependency array means this runs once on mount 40 | 41 | return ( 42 |
43 |
44 |
45 | 49 | ← Back to useEffect Showcase 50 | 51 |
52 | 53 |

✅ Fetching Data from API

54 | 55 |
56 |

57 | Why useEffect is needed here: 58 |

59 |
    60 |
  • API calls are side effects that happen outside of React's rendering
  • 61 |
  • We need to fetch data after the component mounts
  • 62 |
  • The fetch operation is asynchronous
  • 63 |
  • We want to avoid infinite re-renders
  • 64 |
65 |
66 | 67 |
68 |

69 | User Data from API 70 |

71 | 72 | {loading && ( 73 |
74 |
75 | Loading users... 76 |
77 | )} 78 | 79 | {error && ( 80 |
81 |

Error: {error}

82 |
83 | )} 84 | 85 | {!loading && !error && ( 86 |
87 | {users.slice(0, 5).map(user => ( 88 |
89 |

{user.name}

90 |

{user.email}

91 |

{user.company.name}

92 |
93 | ))} 94 |
95 | )} 96 |
97 | 98 |
99 |

Code Example:

100 |
101 |             {`useEffect(() => {
102 |   async function fetchUsers() {
103 |     try {
104 |       setLoading(true);
105 |       const response = await fetch('/api/users');
106 |       const userData = await response.json();
107 |       setUsers(userData);
108 |     } catch (err) {
109 |       setError(err.message);
110 |     } finally {
111 |       setLoading(false);
112 |     }
113 |   }
114 | 
115 |   fetchUsers();
116 | }, []); // Empty deps = run once on mount`}
117 |           
118 |
119 |
120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /app/useeffect-showcase/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function UseEffectShowcase() { 4 | return ( 5 |
6 |
7 |

useEffect Showcase

8 | 9 |
10 |

11 | Learn when to use useEffect and when you don't need it with practical examples 12 |

13 |
14 | 15 |
16 | {/* When you SHOULD use useEffect */} 17 |
18 |

19 | ✅ When you SHOULD use useEffect 20 |

21 |
22 | 26 |

Fetching data from an API

27 |

28 | Example: Loading user data on component mount 29 |

30 | 31 | 32 | 36 |

Setting up subscriptions or intervals

37 |

Example: Real-time clock and cleanup

38 | 39 | 40 | 44 |

Listening for browser events

45 |

Example: Scroll and resize event listeners

46 | 47 | 48 | 52 |

Syncing external state

53 |

Example: LocalStorage and URL params sync

54 | 55 |
56 |
57 | 58 | {/* When you DON'T need useEffect */} 59 |
60 |

61 | ❌ When you DON'T need useEffect 62 |

63 |
64 | 68 |

Updating local state after button click

69 |

Example: Counter and form state updates

70 | 71 | 72 | 76 |

Responding to props with calculation

77 |

Example: Derived state from props

78 | 79 | 80 | 84 |

Triggering logic on render

85 |

86 | Example: Simple calculations and transformations 87 |

88 | 89 |
90 |
91 |
92 | 93 |
94 | 98 | ← Back to Home 99 | 100 |
101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /app/useeffect-showcase/subscriptions/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | export default function Subscriptions() { 7 | const [currentTime, setCurrentTime] = useState(new Date()); 8 | const [counter, setCounter] = useState(0); 9 | const [isActive, setIsActive] = useState(false); 10 | 11 | // ✅ CORRECT: Use useEffect for setting up intervals/timers 12 | useEffect(() => { 13 | const timer = setInterval(() => { 14 | setCurrentTime(new Date()); 15 | }, 1000); 16 | 17 | // Cleanup function to prevent memory leaks 18 | return () => { 19 | clearInterval(timer); 20 | }; 21 | }, []); // Empty dependency array - runs once on mount 22 | 23 | // ✅ CORRECT: Use useEffect for conditional subscriptions 24 | useEffect(() => { 25 | let interval: NodeJS.Timeout | null = null; 26 | 27 | if (isActive) { 28 | interval = setInterval(() => { 29 | setCounter(prev => prev + 1); 30 | }, 100); 31 | } 32 | 33 | // Cleanup function 34 | return () => { 35 | if (interval) { 36 | clearInterval(interval); 37 | } 38 | }; 39 | }, [isActive]); // Depends on isActive 40 | 41 | const toggleCounter = () => { 42 | setIsActive(!isActive); 43 | }; 44 | 45 | const resetCounter = () => { 46 | setCounter(0); 47 | setIsActive(false); 48 | }; 49 | 50 | return ( 51 |
52 |
53 |
54 | 58 | ← Back to useEffect Showcase 59 | 60 |
61 | 62 |

✅ Subscriptions & Intervals

63 | 64 |
65 |

66 | Why useEffect is needed here: 67 |

68 |
    69 |
  • Intervals and timeouts are side effects that need cleanup
  • 70 |
  • We need to prevent memory leaks by clearing intervals on unmount
  • 71 |
  • Setting up subscriptions happens outside of React's rendering cycle
  • 72 |
  • Cleanup prevents multiple intervals running simultaneously
  • 73 |
74 |
75 | 76 |
77 | {/* Real-time Clock */} 78 |
79 |

80 | Real-time Clock 81 |

82 |
83 |
84 | {currentTime.toLocaleTimeString()} 85 |
86 |
{currentTime.toLocaleDateString()}
87 |
88 |
89 |

90 | This clock updates every second using setInterval in useEffect. The interval is 91 | automatically cleaned up when the component unmounts. 92 |

93 |
94 |
95 | 96 | {/* Counter with Start/Stop */} 97 |
98 |

99 | Interval Counter 100 |

101 |
102 |
{counter}
103 |
104 | 114 | 120 |
121 |
122 |
123 |

124 | Status: {isActive ? 'Running' : 'Stopped'} 125 |

126 |

127 | The interval is created/destroyed based on the isActive state. 128 |

129 |
130 |
131 |
132 | 133 |
134 |

Code Examples:

135 |
136 |
137 |

138 | Clock Timer: 139 |

140 |
141 |                 {`useEffect(() => {
142 |   const timer = setInterval(() => {
143 |     setCurrentTime(new Date());
144 |   }, 1000);
145 | 
146 |   // Cleanup function prevents memory leaks
147 |   return () => {
148 |     clearInterval(timer);
149 |   };
150 | }, []); // Empty deps = run once on mount`}
151 |               
152 |
153 |
154 |

155 | Conditional Interval: 156 |

157 |
158 |                 {`useEffect(() => {
159 |   let interval = null;
160 | 
161 |   if (isActive) {
162 |     interval = setInterval(() => {
163 |       setCounter(prev => prev + 1);
164 |     }, 100);
165 |   }
166 | 
167 |   return () => {
168 |     if (interval) clearInterval(interval);
169 |   };
170 | }, [isActive]); // Re-run when isActive changes`}
171 |               
172 |
173 |
174 |
175 |
176 |
177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js v15 Features Examples 2 | 3 | This project demonstrates the usage of new React 19 hooks in a Next.js application, as well as new features introduced in Next.js 15. It includes comprehensive examples of various hooks, components, and React best practices including when and when not to use useEffect. 4 | 5 | ## Getting Started 6 | 7 | First, install the dependencies: 8 | 9 | ```bash 10 | yarn install 11 | ``` 12 | 13 | Then, run the development server (turbopack): 14 | 15 | ```bash 16 | yarn dev 17 | ``` 18 | 19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 20 | 21 | ## Project Structure 22 | 23 | The project is organized as follows: 24 | 25 | ```txt 26 | . 27 | ├── app/ 28 | │ ├── favicon.ico 29 | │ ├── globals.css 30 | │ ├── layout.tsx 31 | │ ├── page.tsx 32 | │ ├── components/ 33 | │ │ └── FooterLink.tsx 34 | │ ├── form-showcase/ 35 | │ │ └── page.tsx 36 | │ ├── search/ 37 | │ │ └── page.tsx 38 | │ ├── useeffect-showcase/ 39 | │ │ ├── page.tsx 40 | │ │ ├── api-fetching/ 41 | │ │ │ └── page.tsx 42 | │ │ ├── browser-events/ 43 | │ │ │ └── page.tsx 44 | │ │ ├── button-click-state/ 45 | │ │ │ └── page.tsx 46 | │ │ ├── external-state/ 47 | │ │ │ └── page.tsx 48 | │ │ ├── props-calculation/ 49 | │ │ │ └── page.tsx 50 | │ │ ├── render-logic/ 51 | │ │ │ └── page.tsx 52 | │ │ └── subscriptions/ 53 | │ │ └── page.tsx 54 | │ └── react-19-hooks/ 55 | │ ├── page.tsx 56 | │ ├── action-state/ 57 | │ │ └── page.tsx 58 | │ ├── use-optimistic/ 59 | │ │ └── page.tsx 60 | │ ├── use-hook/ 61 | │ │ └── page.tsx 62 | │ └── use-form-status/ 63 | │ └── page.tsx 64 | ├── public/ 65 | │ ├── next.svg 66 | │ └── vercel.svg 67 | ├── .eslintrc.json 68 | ├── .prettierrc 69 | ├── next.config.js 70 | ├── package.json 71 | ├── postcss.config.js 72 | ├── README.md 73 | ├── tailwind.config.ts 74 | └── tsconfig.json 75 | ``` 76 | 77 | ## Testing 78 | 79 | The project uses Vitest for testing React components. Tests are automatically run on pull requests and pushes to the main branch. 80 | 81 | ### Running Tests 82 | 83 | To run tests in watch mode (development): 84 | 85 | ```bash 86 | yarn test 87 | ``` 88 | 89 | To run tests once with coverage: 90 | 91 | ```bash 92 | yarn test:coverage 93 | ``` 94 | 95 | ### Test Structure 96 | 97 | Tests are co-located with their components in the `app/components` directory. For example: 98 | 99 | - `app/components/FooterLink.tsx` 100 | - `app/components/FooterLink.test.tsx` 101 | 102 | ### CI/CD 103 | 104 | Tests are automatically run in GitHub Actions on: 105 | 106 | - Pull requests targeting the main branch 107 | - Direct pushes to the main branch 108 | 109 | Coverage reports are generated and uploaded as artifacts in the GitHub Actions UI. 110 | 111 | ## Examples 112 | 113 | The project includes the following examples: 114 | 115 | ### React Best Practices 116 | 117 | 1. **useEffect Showcase**: A comprehensive guide on when to use and when NOT to use useEffect. 118 | 119 | - URL: `/useeffect-showcase` 120 | - **When you SHOULD use useEffect:** 121 | - API data fetching (`/useeffect-showcase/api-fetching`) 122 | - Setting up subscriptions/intervals (`/useeffect-showcase/subscriptions`) 123 | - Browser event listeners (`/useeffect-showcase/browser-events`) 124 | - External state synchronization (`/useeffect-showcase/external-state`) 125 | - **When you DON'T need useEffect:** 126 | - Button click state updates (`/useeffect-showcase/button-click-state`) 127 | - Props-based calculations (`/useeffect-showcase/props-calculation`) 128 | - Render-time logic (`/useeffect-showcase/render-logic`) 129 | 130 | ### Next.js 15 Features 131 | 132 | 1. **Next.js 15 Form Component**: Demonstrates the new Form component introduced in Next.js 15. 133 | 134 | - URL: `/form-showcase` 135 | 136 | 2. **Search with Async SearchParams**: Shows how to use async searchParams in Next.js 15. 137 | 138 | - URL: `/search` 139 | 140 | ### React 19 Hooks 141 | 142 | 1. **useActionState**: Demonstrates form submission with server-side actions and state management. 143 | 144 | - URL: `/react-19-hooks/action-state` 145 | 146 | 2. **useOptimistic**: Shows optimistic updates in the UI before server confirmation. 147 | 148 | - URL: `/react-19-hooks/use-optimistic` 149 | - Features: 150 | - Real-time optimistic UI updates 151 | - Title history tracking with timestamps 152 | - Error handling and validation 153 | - Loading states and animations 154 | - Dark mode support 155 | - Responsive design 156 | 157 | 3. **use**: Illustrates data fetching with the `use` hook. 158 | 159 | - URL: `/react-19-hooks/use-hook` 160 | 161 | 4. **useFormStatus**: Displays form submission status and pending state. 162 | - URL: `/react-19-hooks/use-form-status` 163 | 164 | ## Features Demonstrated 165 | 166 | - **useEffect Best Practices**: Comprehensive examples showing when to use and avoid useEffect 167 | - **Next.js 15 Form Component**: New form handling capabilities 168 | - **Async SearchParams**: Next.js 15 search parameter handling 169 | - **React 19 Hooks**: useActionState, useOptimistic, use, useFormStatus examples 170 | - **Enhanced Text Visibility**: Context-aware CSS system for better readability 171 | - **Tailwind CSS Styling**: Modern utility-first CSS framework 172 | - **Dark Mode Support**: Theme switching capabilities 173 | - **Responsive Design**: Mobile-first approach 174 | 175 | ## Code Quality and Development 176 | 177 | ### Linting and Formatting 178 | 179 | This project uses ESLint and Prettier for code quality and formatting. The setup includes: 180 | 181 | - Pre-commit hooks using Husky 182 | - Automatic formatting of staged files using lint-staged 183 | - ESLint for TypeScript/JavaScript linting 184 | - Prettier for consistent code formatting 185 | 186 | Available scripts: 187 | 188 | - `yarn lint` - Run ESLint 189 | - `yarn format` - Format all files using Prettier 190 | - `yarn format:check` - Check if files are formatted correctly 191 | 192 | Configuration files: 193 | 194 | - `.eslintrc.json` - ESLint configuration 195 | - `.prettierrc` - Prettier configuration with: 196 | - Single quotes 197 | - 2-space indentation 198 | - 100 character line length 199 | - ES5 trailing commas 200 | - And more best practices 201 | 202 | Pre-commit hooks ensure that: 203 | 204 | - All staged TypeScript/JavaScript files are linted and formatted 205 | - All staged JSON/Markdown files are properly formatted 206 | - Code meets the project's quality standards before being committed 207 | 208 | ## Learn More 209 | 210 | To learn more about Next.js and React, take a look at the following resources: 211 | 212 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 213 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 214 | - [React Documentation](https://reactjs.org/) - learn about React features and API. 215 | 216 | ## Deploy on Vercel 217 | 218 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 219 | 220 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 221 | -------------------------------------------------------------------------------- /app/useeffect-showcase/browser-events/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | export default function BrowserEvents() { 7 | const [scrollY, setScrollY] = useState(0); 8 | const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); 9 | const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); 10 | const [isOnline, setIsOnline] = useState(true); 11 | 12 | // ✅ CORRECT: Use useEffect for scroll events 13 | useEffect(() => { 14 | const handleScroll = () => { 15 | setScrollY(window.scrollY); 16 | }; 17 | 18 | window.addEventListener('scroll', handleScroll); 19 | 20 | // Cleanup: remove event listener 21 | return () => { 22 | window.removeEventListener('scroll', handleScroll); 23 | }; 24 | }, []); 25 | 26 | // ✅ CORRECT: Use useEffect for resize events 27 | useEffect(() => { 28 | const handleResize = () => { 29 | setWindowSize({ 30 | width: window.innerWidth, 31 | height: window.innerHeight, 32 | }); 33 | }; 34 | 35 | // Set initial size 36 | handleResize(); 37 | 38 | window.addEventListener('resize', handleResize); 39 | 40 | return () => { 41 | window.removeEventListener('resize', handleResize); 42 | }; 43 | }, []); 44 | 45 | // ✅ CORRECT: Use useEffect for mouse events 46 | useEffect(() => { 47 | const handleMouseMove = (e: MouseEvent) => { 48 | setMousePosition({ x: e.clientX, y: e.clientY }); 49 | }; 50 | 51 | document.addEventListener('mousemove', handleMouseMove); 52 | 53 | return () => { 54 | document.removeEventListener('mousemove', handleMouseMove); 55 | }; 56 | }, []); 57 | 58 | // ✅ CORRECT: Use useEffect for online/offline events 59 | useEffect(() => { 60 | const handleOnline = () => setIsOnline(true); 61 | const handleOffline = () => setIsOnline(false); 62 | 63 | // Set initial state 64 | setIsOnline(navigator.onLine); 65 | 66 | window.addEventListener('online', handleOnline); 67 | window.addEventListener('offline', handleOffline); 68 | 69 | return () => { 70 | window.removeEventListener('online', handleOnline); 71 | window.removeEventListener('offline', handleOffline); 72 | }; 73 | }, []); 74 | 75 | return ( 76 |
77 |
78 |
79 | 83 | ← Back to useEffect Showcase 84 | 85 |
86 | 87 |

✅ Browser Events

88 | 89 |
90 |

91 | Why useEffect is needed here: 92 |

93 |
    94 |
  • Event listeners are side effects that attach to the DOM
  • 95 |
  • We need to remove listeners on unmount to prevent memory leaks
  • 96 |
  • Browser events happen outside of React's rendering cycle
  • 97 |
  • Cleanup prevents duplicate event listeners
  • 98 |
99 |
100 | 101 |
102 | {/* Scroll Position */} 103 |
104 |

105 | Scroll Position 106 |

107 |
108 |
{Math.round(scrollY)}px
109 |
Scroll distance from top
110 |
111 |
117 |
118 |
119 |
120 | 121 | {/* Window Size */} 122 |
123 |

124 | Window Size 125 |

126 |
127 |
128 | {windowSize.width} × {windowSize.height} 129 |
130 |
Width × Height (pixels)
131 |
Try resizing your browser window
132 |
133 |
134 | 135 | {/* Mouse Position */} 136 |
137 |

138 | Mouse Position 139 |

140 |
141 |
142 | X: {mousePosition.x}, Y: {mousePosition.y} 143 |
144 |
Cursor coordinates
145 |
Move your mouse around the page
146 |
147 |
148 | 149 | {/* Online Status */} 150 |
151 |

152 | Connection Status 153 |

154 |
155 |
160 |
165 | {isOnline ? 'Online' : 'Offline'} 166 |
167 |
Try disconnecting your internet
168 |
169 |
170 |
171 | 172 | {/* Scroll content to demonstrate scroll tracking */} 173 |
174 |

175 | Scroll down to see the scroll tracker in action! 176 |

177 |
178 | {Array.from({ length: 20 }, (_, i) => ( 179 |
180 |

Content Block {i + 1}

181 |

182 | This is some content to make the page scrollable. The scroll position is being 183 | tracked in real-time using a scroll event listener set up with useEffect. 184 |

185 |
186 | ))} 187 |
188 |
189 | 190 |
191 |

Code Example:

192 |
193 |             {`useEffect(() => {
194 |   const handleScroll = () => {
195 |     setScrollY(window.scrollY);
196 |   };
197 | 
198 |   const handleResize = () => {
199 |     setWindowSize({
200 |       width: window.innerWidth,
201 |       height: window.innerHeight
202 |     });
203 |   };
204 | 
205 |   // Add event listeners
206 |   window.addEventListener('scroll', handleScroll);
207 |   window.addEventListener('resize', handleResize);
208 | 
209 |   // Cleanup function removes listeners
210 |   return () => {
211 |     window.removeEventListener('scroll', handleScroll);
212 |     window.removeEventListener('resize', handleResize);
213 |   };
214 | }, []); // Empty deps = setup once on mount`}
215 |           
216 |
217 |
218 |
219 | ); 220 | } 221 | -------------------------------------------------------------------------------- /app/useeffect-showcase/button-click-state/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | export default function ButtonClickState() { 7 | const [count, setCount] = useState(0); 8 | const [name, setName] = useState(''); 9 | const [items, setItems] = useState([]); 10 | const [newItem, setNewItem] = useState(''); 11 | const [isVisible, setIsVisible] = useState(false); 12 | 13 | // ❌ WRONG: Don't use useEffect for simple state updates 14 | // useEffect(() => { 15 | // setCount(count + 1); // This would cause infinite re-renders! 16 | // }, [count]); 17 | 18 | // ✅ CORRECT: Update state directly in event handlers 19 | const increment = () => { 20 | setCount(prev => prev + 1); 21 | }; 22 | 23 | const decrement = () => { 24 | setCount(prev => prev - 1); 25 | }; 26 | 27 | const reset = () => { 28 | setCount(0); 29 | }; 30 | 31 | // ✅ CORRECT: Handle form state directly 32 | const handleNameChange = (e: React.ChangeEvent) => { 33 | setName(e.target.value); 34 | }; 35 | 36 | const clearName = () => { 37 | setName(''); 38 | }; 39 | 40 | // ✅ CORRECT: Update arrays directly in event handlers 41 | const addItem = () => { 42 | if (newItem.trim()) { 43 | setItems(prev => [...prev, newItem.trim()]); 44 | setNewItem(''); 45 | } 46 | }; 47 | 48 | const removeItem = (index: number) => { 49 | setItems(prev => prev.filter((_, i) => i !== index)); 50 | }; 51 | 52 | const toggleVisibility = () => { 53 | setIsVisible(prev => !prev); 54 | }; 55 | 56 | return ( 57 |
58 |
59 |
60 | 64 | ← Back to useEffect Showcase 65 | 66 |
67 | 68 |

❌ Don't Use useEffect for Button Clicks

69 | 70 |
71 |

72 | Why useEffect is NOT needed here: 73 |

74 |
    75 |
  • Button clicks and form interactions are synchronous user events
  • 76 |
  • State updates can happen directly in event handlers
  • 77 |
  • No side effects or async operations are involved
  • 78 |
  • Using useEffect would cause unnecessary re-renders or infinite loops
  • 79 |
80 |
81 | 82 |
83 | {/* Counter Example */} 84 |
85 |

86 | Simple Counter 87 |

88 |
89 |
{count}
90 |
91 | 97 | 103 | 109 |
110 |
111 |
112 |

113 | ✅ State updates happen directly in onClick handlers - no useEffect needed! 114 |

115 |
116 |
117 | 118 | {/* Form Input Example */} 119 |
120 |

121 | Form Input 122 |

123 |
124 |
125 | 132 |
133 |
134 | {name && ( 135 |

136 | Hello, {name}! 137 |

138 | )} 139 | 145 |
146 |
147 |
148 |

149 | ✅ Form state updates directly in onChange - no useEffect needed! 150 |

151 |
152 |
153 |
154 | 155 | {/* List Management Example */} 156 |
157 |

158 | List Management 159 |

160 |
161 |
162 | setNewItem(e.target.value)} 166 | placeholder="Add a new item" 167 | className="flex-1 p-2 border border-gray-300 rounded-lg" 168 | onKeyDown={e => e.key === 'Enter' && addItem()} 169 | /> 170 | 176 |
177 | 178 | {items.length > 0 && ( 179 |
180 |

Items:

181 | {items.map((item, index) => ( 182 |
186 | {item} 187 | 193 |
194 | ))} 195 |
196 | )} 197 | 198 |
199 |

200 | ✅ Array updates happen directly in event handlers - no useEffect needed! 201 |

202 |
203 |
204 |
205 | 206 | {/* Visibility Toggle Example */} 207 |
208 |

209 | Toggle Visibility 210 |

211 |
212 | 218 | 219 | {isVisible && ( 220 |
221 |

Hidden Content

222 |

223 | This content is conditionally rendered based on state. No useEffect needed for 224 | simple show/hide logic! 225 |

226 |
227 | )} 228 | 229 |
230 |

231 | ✅ Boolean state toggles directly in onClick - no useEffect needed! 232 |

233 |
234 |
235 |
236 | 237 |
238 |

Code Examples:

239 |
240 |
241 |

242 | ❌ WRONG (causes infinite loops): 243 |

244 |
245 |                 {`// DON'T DO THIS!
246 | useEffect(() => {
247 |   setCount(count + 1); // Infinite re-renders!
248 | }, [count]);
249 | 
250 | const handleClick = () => {
251 |   // This will trigger the useEffect above
252 | }`}
253 |               
254 |
255 |
256 |

257 | ✅ RIGHT (direct state update): 258 |

259 |
260 |                 {`// Simple and clean!
261 | const handleClick = () => {
262 |   setCount(prev => prev + 1); // Direct state update
263 | };
264 | 
265 | const handleInputChange = (e) => {
266 |   setName(e.target.value); // Direct state update
267 | };`}
268 |               
269 |
270 |
271 |
272 |
273 |
274 | ); 275 | } 276 | -------------------------------------------------------------------------------- /app/useeffect-showcase/external-state/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { useSearchParams, useRouter } from 'next/navigation'; 5 | import Link from 'next/link'; 6 | 7 | export default function ExternalState() { 8 | const [theme, setTheme] = useState('light'); 9 | const [username, setUsername] = useState(''); 10 | const [count, setCount] = useState(0); 11 | const [searchText, setSearchText] = useState(''); 12 | 13 | const searchParams = useSearchParams(); 14 | const router = useRouter(); 15 | 16 | // ✅ CORRECT: Use useEffect for localStorage sync 17 | useEffect(() => { 18 | // Read from localStorage on mount 19 | const savedTheme = localStorage.getItem('theme'); 20 | const savedUsername = localStorage.getItem('username'); 21 | const savedCount = localStorage.getItem('count'); 22 | 23 | if (savedTheme) setTheme(savedTheme); 24 | if (savedUsername) setUsername(savedUsername); 25 | if (savedCount) setCount(parseInt(savedCount, 10)); 26 | }, []); 27 | 28 | // ✅ CORRECT: Use useEffect to sync state to localStorage 29 | useEffect(() => { 30 | localStorage.setItem('theme', theme); 31 | }, [theme]); 32 | 33 | useEffect(() => { 34 | localStorage.setItem('username', username); 35 | }, [username]); 36 | 37 | useEffect(() => { 38 | localStorage.setItem('count', count.toString()); 39 | }, [count]); 40 | 41 | // ✅ CORRECT: Use useEffect for URL params sync 42 | useEffect(() => { 43 | const searchFromUrl = searchParams.get('search') || ''; 44 | setSearchText(searchFromUrl); 45 | }, [searchParams]); 46 | 47 | // ✅ CORRECT: Use useEffect to sync state to URL 48 | useEffect(() => { 49 | const params = new URLSearchParams(searchParams.toString()); 50 | 51 | if (searchText) { 52 | params.set('search', searchText); 53 | } else { 54 | params.delete('search'); 55 | } 56 | 57 | const newUrl = `${window.location.pathname}?${params.toString()}`; 58 | router.replace(newUrl, { scroll: false }); 59 | }, [searchText, searchParams, router]); 60 | 61 | const toggleTheme = () => { 62 | setTheme(prev => (prev === 'light' ? 'dark' : 'light')); 63 | }; 64 | 65 | const clearData = () => { 66 | setTheme('light'); 67 | setUsername(''); 68 | setCount(0); 69 | setSearchText(''); 70 | localStorage.clear(); 71 | }; 72 | 73 | return ( 74 |
79 |
80 |
81 | 85 | ← Back to useEffect Showcase 86 | 87 |
88 | 89 |

✅ Syncing External State

90 | 91 |
92 |

93 | Why useEffect is needed here: 94 |

95 |
    96 |
  • localStorage and URL params exist outside React's state system
  • 97 |
  • We need to read external state on component mount
  • 98 |
  • We need to sync React state changes back to external systems
  • 99 |
  • These operations are side effects that should happen after render
  • 100 |
101 |
102 | 103 |
104 | {/* Theme Management */} 105 |
110 |

111 | Theme Persistence 112 |

113 |
114 |
115 | Current theme: 116 | 121 | {theme} 122 | 123 |
124 | 134 |

135 | Theme preference is saved to localStorage and persists across page reloads. 136 |

137 |
138 |
139 | 140 | {/* User Data */} 141 |
146 |

147 | User Data 148 |

149 |
150 |
151 | 152 | setUsername(e.target.value)} 156 | placeholder="Enter your username" 157 | className={`w-full p-2 border rounded-lg ${ 158 | theme === 'dark' 159 | ? 'bg-gray-700 border-gray-600 text-white' 160 | : 'bg-white border-gray-300 text-black' 161 | }`} 162 | /> 163 |
164 |
165 | 166 |
167 | 173 | 179 |
180 |
181 |

182 | User data is automatically saved to localStorage. 183 |

184 |
185 |
186 |
187 | 188 | {/* URL Search Params */} 189 |
194 |

195 | URL Search Parameters 196 |

197 |
198 |
199 | 200 | setSearchText(e.target.value)} 204 | placeholder="Type to update URL..." 205 | className={`w-full p-2 border rounded-lg ${ 206 | theme === 'dark' 207 | ? 'bg-gray-700 border-gray-600 text-white' 208 | : 'bg-white border-gray-300 text-black' 209 | }`} 210 | /> 211 |
212 |
213 |

214 | Current URL:{' '} 215 | 216 | {typeof window !== 'undefined' ? window.location.href : ''} 217 | 218 |

219 |
220 |

221 | The search text is synced with URL parameters. Try refreshing the page or sharing the 222 | URL. 223 |

224 |
225 |
226 | 227 | {/* Controls */} 228 |
229 | 235 |
236 | 237 |
242 |

Code Examples:

243 |
244 |
245 |

246 | Reading from localStorage on mount: 247 |

248 |
249 |                 {`useEffect(() => {
250 |   // Read from localStorage on mount
251 |   const savedTheme = localStorage.getItem('theme');
252 |   if (savedTheme) setTheme(savedTheme);
253 | }, []); // Empty deps = run once on mount`}
254 |               
255 |
256 |
257 |

258 | Syncing state to localStorage: 259 |

260 |
261 |                 {`useEffect(() => {
262 |   localStorage.setItem('theme', theme);
263 | }, [theme]); // Run when theme changes`}
264 |               
265 |
266 |
267 |

268 | Syncing with URL parameters: 269 |

270 |
271 |                 {`useEffect(() => {
272 |   const params = new URLSearchParams();
273 |   if (searchText) params.set('search', searchText);
274 |   
275 |   const newUrl = \`\${pathname}?\${params}\`;
276 |   router.replace(newUrl, { scroll: false });
277 | }, [searchText]); // Run when searchText changes`}
278 |               
279 |
280 |
281 |
282 |
283 |
284 | ); 285 | } 286 | -------------------------------------------------------------------------------- /app/useeffect-showcase/render-logic/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | export default function RenderLogic() { 7 | const [searchTerm, setSearchTerm] = useState(''); 8 | const [sortBy, setSortBy] = useState<'name' | 'price' | 'rating'>('name'); 9 | const [showOnlyInStock, setShowOnlyInStock] = useState(false); 10 | 11 | // Sample data 12 | const products = [ 13 | { id: 1, name: 'Laptop Pro', price: 1299, rating: 4.8, inStock: true, category: 'Electronics' }, 14 | { 15 | id: 2, 16 | name: 'Wireless Mouse', 17 | price: 49, 18 | rating: 4.2, 19 | inStock: false, 20 | category: 'Electronics', 21 | }, 22 | { 23 | id: 3, 24 | name: 'Mechanical Keyboard', 25 | price: 149, 26 | rating: 4.6, 27 | inStock: true, 28 | category: 'Electronics', 29 | }, 30 | { id: 4, name: 'Monitor 4K', price: 399, rating: 4.4, inStock: true, category: 'Electronics' }, 31 | { id: 5, name: 'Office Chair', price: 249, rating: 4.1, inStock: false, category: 'Furniture' }, 32 | { id: 6, name: 'Desk Lamp', price: 79, rating: 4.0, inStock: true, category: 'Furniture' }, 33 | ]; 34 | 35 | // ❌ WRONG: Don't use useEffect for filtering/sorting 36 | // const [filteredProducts, setFilteredProducts] = useState(products); 37 | // useEffect(() => { 38 | // let filtered = products.filter(product => 39 | // product.name.toLowerCase().includes(searchTerm.toLowerCase()) 40 | // ); 41 | // if (showOnlyInStock) { 42 | // filtered = filtered.filter(product => product.inStock); 43 | // } 44 | // setFilteredProducts(filtered); 45 | // }, [searchTerm, showOnlyInStock]); 46 | 47 | // ✅ CORRECT: Calculate during render 48 | const filteredAndSortedProducts = (() => { 49 | // Filter by search term 50 | let filtered = products.filter(product => 51 | product.name.toLowerCase().includes(searchTerm.toLowerCase()) 52 | ); 53 | 54 | // Filter by stock status 55 | if (showOnlyInStock) { 56 | filtered = filtered.filter(product => product.inStock); 57 | } 58 | 59 | // Sort products 60 | return filtered.sort((a, b) => { 61 | switch (sortBy) { 62 | case 'name': 63 | return a.name.localeCompare(b.name); 64 | case 'price': 65 | return a.price - b.price; 66 | case 'rating': 67 | return b.rating - a.rating; 68 | default: 69 | return 0; 70 | } 71 | }); 72 | })(); 73 | 74 | // ✅ CORRECT: Simple transformations during render 75 | const stats = { 76 | total: filteredAndSortedProducts.length, 77 | inStock: filteredAndSortedProducts.filter(p => p.inStock).length, 78 | outOfStock: filteredAndSortedProducts.filter(p => !p.inStock).length, 79 | averagePrice: 80 | filteredAndSortedProducts.length > 0 81 | ? filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) / 82 | filteredAndSortedProducts.length 83 | : 0, 84 | averageRating: 85 | filteredAndSortedProducts.length > 0 86 | ? filteredAndSortedProducts.reduce((sum, p) => sum + p.rating, 0) / 87 | filteredAndSortedProducts.length 88 | : 0, 89 | }; 90 | 91 | // ✅ CORRECT: Generate CSS classes based on state 92 | const getStockBadgeClass = (inStock: boolean) => { 93 | return inStock 94 | ? 'bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs' 95 | : 'bg-red-100 text-red-800 px-2 py-1 rounded-full text-xs'; 96 | }; 97 | 98 | // ✅ CORRECT: Format values during render 99 | const formatPrice = (price: number) => `$${price.toFixed(2)}`; 100 | const formatRating = (rating: number) => `⭐ ${rating.toFixed(1)}`; 101 | 102 | return ( 103 |
104 |
105 |
106 | 110 | ← Back to useEffect Showcase 111 | 112 |
113 | 114 |

❌ Don't Use useEffect for Render Logic

115 | 116 |
117 |

118 | Why useEffect is NOT needed here: 119 |

120 |
    121 |
  • Filtering, sorting, and transforming data can happen during render
  • 122 |
  • React will re-calculate when dependencies (state/props) change
  • 123 |
  • Simple calculations are fast and don't need optimization
  • 124 |
  • Using useEffect would cause unnecessary extra renders
  • 125 |
126 |
127 | 128 | {/* Controls */} 129 |
130 |

131 | Product Filters & Search 132 |

133 |
134 |
135 | 136 | setSearchTerm(e.target.value)} 140 | placeholder="Type to search..." 141 | className="w-full p-2 border border-gray-300 rounded-lg" 142 | /> 143 |
144 |
145 | 146 | 155 |
156 |
157 | 166 |
167 |
168 |
169 | 170 | {/* Statistics */} 171 |
172 |

173 | Statistics (Calculated on Render) 174 |

175 |
176 |
177 |
{stats.total}
178 |
Total Products
179 |
180 |
181 |
{stats.inStock}
182 |
In Stock
183 |
184 |
185 |
{stats.outOfStock}
186 |
Out of Stock
187 |
188 |
189 |
190 | {formatPrice(stats.averagePrice)} 191 |
192 |
Avg Price
193 |
194 |
195 |
196 | {formatRating(stats.averageRating)} 197 |
198 |
Avg Rating
199 |
200 |
201 |
202 |

203 | ✅ All statistics calculated during render - no useEffect needed! 204 |

205 |
206 |
207 | 208 | {/* Product List */} 209 |
210 |

211 | Products ({filteredAndSortedProducts.length} found) 212 |

213 | 214 | {filteredAndSortedProducts.length === 0 ? ( 215 |
216 | No products found matching your criteria 217 |
218 | ) : ( 219 |
220 | {filteredAndSortedProducts.map(product => ( 221 |
222 |
223 |

{product.name}

224 | 225 | {product.inStock ? 'In Stock' : 'Out of Stock'} 226 | 227 |
228 |
229 |

Category: {product.category}

230 |

Price: {formatPrice(product.price)}

231 |

Rating: {formatRating(product.rating)}

232 |
233 |
234 | ))} 235 |
236 | )} 237 | 238 |
239 |

240 | ✅ Products filtered and sorted during render - no useEffect needed! 241 |

242 |
243 |
244 | 245 |
246 |

Code Examples:

247 |
248 |
249 |

250 | ❌ WRONG: 251 |

252 |
253 |                 {`// DON'T DO THIS!
254 | const [filteredProducts, setFilteredProducts] = useState([]);
255 | 
256 | useEffect(() => {
257 |   const filtered = products.filter(product =>
258 |     product.name.includes(searchTerm)
259 |   );
260 |   setFilteredProducts(filtered);
261 | }, [products, searchTerm]); // Extra re-render!`}
262 |               
263 |
264 |
265 |

266 | ✅ CORRECT: 267 |

268 |
269 |                 {`// Simple and efficient!
270 | const filteredProducts = products.filter(product =>
271 |   product.name.toLowerCase().includes(searchTerm.toLowerCase())
272 | );
273 | 
274 | const sortedProducts = filteredProducts.sort((a, b) => {
275 |   return sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name);
276 | });
277 | 
278 | // For expensive operations, use useMemo:
279 | const expensiveCalculation = useMemo(() => {
280 |   return heavyProcessing(data);
281 | }, [data]);`}
282 |               
283 |
284 |
285 |
286 |
287 |
288 | ); 289 | } 290 | -------------------------------------------------------------------------------- /app/useeffect-showcase/props-calculation/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useMemo } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | // Example components that derive state from props 7 | 8 | interface UserCardProps { 9 | firstName: string; 10 | lastName: string; 11 | age: number; 12 | email: string; 13 | } 14 | 15 | // ✅ CORRECT: Derive state directly during render 16 | function UserCard({ firstName, lastName, age, email }: UserCardProps) { 17 | // ❌ WRONG: Don't use useEffect for simple calculations 18 | // const [fullName, setFullName] = useState(''); 19 | // useEffect(() => { 20 | // setFullName(`${firstName} ${lastName}`); 21 | // }, [firstName, lastName]); 22 | 23 | // ✅ CORRECT: Calculate during render 24 | const fullName = `${firstName} ${lastName}`; 25 | const initials = `${firstName[0]}${lastName[0]}`.toUpperCase(); 26 | const isAdult = age >= 18; 27 | const emailDomain = email.split('@')[1]; 28 | 29 | return ( 30 |
31 |
32 |
33 | {initials} 34 |
35 |
36 |

{fullName}

37 |

{email}

38 |
39 |
40 |
41 |

42 | Age: {age} ({isAdult ? 'Adult' : 'Minor'}) 43 |

44 |

Domain: {emailDomain}

45 |
46 |
47 | ); 48 | } 49 | 50 | interface ShoppingCartProps { 51 | items: Array<{ name: string; price: number; quantity: number }>; 52 | } 53 | 54 | // ✅ CORRECT: Use useMemo for expensive calculations 55 | function ShoppingCart({ items }: ShoppingCartProps) { 56 | // ❌ WRONG: Don't use useEffect for calculations 57 | // const [total, setTotal] = useState(0); 58 | // useEffect(() => { 59 | // const newTotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0); 60 | // setTotal(newTotal); 61 | // }, [items]); 62 | 63 | // ✅ CORRECT: Calculate during render (or use useMemo for expensive operations) 64 | const total = useMemo(() => { 65 | return items.reduce((sum, item) => sum + item.price * item.quantity, 0); 66 | }, [items]); 67 | 68 | const itemCount = items.reduce((sum, item) => sum + item.quantity, 0); 69 | const averagePrice = itemCount > 0 ? total / itemCount : 0; 70 | 71 | return ( 72 |
73 |

Shopping Cart

74 | {items.length === 0 ? ( 75 |

Cart is empty

76 | ) : ( 77 | <> 78 |
79 | {items.map((item, index) => ( 80 |
81 | 82 | {item.name} x{item.quantity} 83 | 84 | ${(item.price * item.quantity).toFixed(2)} 85 |
86 | ))} 87 |
88 |
89 |
90 | Items: {itemCount} 91 | Avg: ${averagePrice.toFixed(2)} 92 |
93 |
94 | Total: 95 | ${total.toFixed(2)} 96 |
97 |
98 | 99 | )} 100 |
101 | ); 102 | } 103 | 104 | export default function PropsCalculation() { 105 | const [userForm, setUserForm] = useState({ 106 | firstName: 'John', 107 | lastName: 'Doe', 108 | age: 25, 109 | email: 'john.doe@example.com', 110 | }); 111 | 112 | const [cartItems, setCartItems] = useState([ 113 | { name: 'Laptop', price: 999.99, quantity: 1 }, 114 | { name: 'Mouse', price: 29.99, quantity: 2 }, 115 | { name: 'Keyboard', price: 79.99, quantity: 1 }, 116 | ]); 117 | 118 | const updateQuantity = (index: number, newQuantity: number) => { 119 | setCartItems(prev => 120 | prev.map((item, i) => (i === index ? { ...item, quantity: Math.max(0, newQuantity) } : item)) 121 | ); 122 | }; 123 | 124 | const removeItem = (index: number) => { 125 | setCartItems(prev => prev.filter((_, i) => i !== index)); 126 | }; 127 | 128 | return ( 129 |
130 |
131 |
132 | 136 | ← Back to useEffect Showcase 137 | 138 |
139 | 140 |

❌ Don't Use useEffect for Props Calculations

141 | 142 |
143 |

144 | Why useEffect is NOT needed here: 145 |

146 |
    147 |
  • Calculations based on props/state can happen during render
  • 148 |
  • React re-renders when props change, so calculations stay in sync
  • 149 |
  • Using useEffect adds unnecessary complexity and extra renders
  • 150 |
  • For expensive calculations, use useMemo instead of useEffect
  • 151 |
152 |
153 | 154 |
155 | {/* User Form */} 156 |
157 |

User Information

158 |
159 |
160 | 161 | setUserForm(prev => ({ ...prev, firstName: e.target.value }))} 165 | className="w-full p-2 border border-gray-300 rounded" 166 | /> 167 |
168 |
169 | 170 | setUserForm(prev => ({ ...prev, lastName: e.target.value }))} 174 | className="w-full p-2 border border-gray-300 rounded" 175 | /> 176 |
177 |
178 | 179 | 183 | setUserForm(prev => ({ ...prev, age: parseInt(e.target.value) || 0 })) 184 | } 185 | className="w-full p-2 border border-gray-300 rounded" 186 | /> 187 |
188 |
189 | 190 | setUserForm(prev => ({ ...prev, email: e.target.value }))} 194 | className="w-full p-2 border border-gray-300 rounded" 195 | /> 196 |
197 |
198 |
199 | 200 | {/* User Card Display */} 201 |
202 |

Derived Display

203 | 204 |
205 |

206 | ✅ Full name, initials, adult status, and domain are calculated during render! 207 |

208 |
209 |
210 |
211 | 212 | {/* Shopping Cart Example */} 213 |
214 |

Shopping Cart with Calculations

215 |
216 | {/* Cart Controls */} 217 |
218 |

Manage Items

219 |
220 | {cartItems.map((item, index) => ( 221 |
222 |
223 | {item.name} 224 | 230 |
231 |
232 | ${item.price} 233 | × 234 |
235 | 241 | {item.quantity} 242 | 248 |
249 |
250 |
251 | ))} 252 |
253 |
254 | 255 | {/* Cart Display */} 256 |
257 |

Cart Summary

258 | 259 |
260 |

261 | ✅ Total, item count, and average price calculated with useMemo for performance! 262 |

263 |
264 |
265 |
266 |
267 | 268 |
269 |

Code Examples:

270 |
271 |
272 |

273 | ❌ WRONG (unnecessary effect): 274 |

275 |
276 |                 {`// DON'T DO THIS!
277 | const [fullName, setFullName] = useState('');
278 | 
279 | useEffect(() => {
280 |   setFullName(\`\${firstName} \${lastName}\`);
281 | }, [firstName, lastName]);`}
282 |               
283 |
284 |
285 |

286 | ✅ CORRECT: 287 |

288 |
289 |                 {`// Simple calculation during render
290 | const fullName = \`\${firstName} \${lastName}\`;
291 | const isAdult = age >= 18;
292 | 
293 | // For expensive calculations, use useMemo
294 | const total = useMemo(() => {
295 |   return items.reduce((sum, item) => 
296 |     sum + (item.price * item.quantity), 0
297 |   );
298 | }, [items]);`}
299 |               
300 |
301 |
302 |
303 |
304 |
305 | ); 306 | } 307 | --------------------------------------------------------------------------------