├── 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 |
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 |
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 |
8 |
9 |
10 | use Hook Example
11 |
12 |
13 |
14 |
15 | useActionState Example
16 |
17 |
18 |
19 |
20 | useFormStatus Example
21 |
22 |
23 |
24 |
25 | useOptimistic Example
26 |
27 |
28 |
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 |
29 | Get Data
30 |
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 |
29 | Submit
30 |
31 | {pending &&
Submitting {data?.get('username') as string}...
}
32 |
33 | );
34 | };
35 |
36 | const UseFormStatusComponent = () => {
37 | return (
38 |
39 |
useFormStatus Example
40 |
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 |
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 |
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 |
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 |
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 |
112 | {isActive ? 'Stop' : 'Start'}
113 |
114 |
118 | Reset
119 |
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 |
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 |
95 | -1
96 |
97 |
101 | +1
102 |
103 |
107 | Reset
108 |
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 |
143 | Clear
144 |
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 |
174 | Add
175 |
176 |
177 |
178 | {items.length > 0 && (
179 |
180 |
Items:
181 | {items.map((item, index) => (
182 |
186 | {item}
187 | removeItem(index)}
189 | className="px-2 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
190 | >
191 | Remove
192 |
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 |
216 | {isVisible ? 'Hide' : 'Show'} Content
217 |
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 |
132 | Toggle Theme
133 |
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 | Username:
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 |
Counter: {count}
166 |
167 | setCount(prev => prev - 1)}
169 | className="px-3 py-1 bg-red-600 text-white rounded hover:bg-red-700"
170 | >
171 | -
172 |
173 | setCount(prev => prev + 1)}
175 | className="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700"
176 | >
177 | +
178 |
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 | Search Text:
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 |
233 | Clear All Data
234 |
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 | Search Products:
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 | Sort By:
146 | setSortBy(e.target.value as 'name' | 'price' | 'rating')}
149 | className="w-full p-2 border border-gray-300 rounded-lg"
150 | >
151 | Name
152 | Price
153 | Rating
154 |
155 |
156 |
157 |
158 | setShowOnlyInStock(e.target.checked)}
162 | className="rounded"
163 | />
164 | Show only in stock
165 |
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 |
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 | removeItem(index)}
226 | className="text-red-600 hover:text-red-800 text-sm"
227 | >
228 | Remove
229 |
230 |
231 |
232 |
${item.price}
233 |
×
234 |
235 | updateQuantity(index, item.quantity - 1)}
237 | className="px-2 py-1 bg-red-600 text-white rounded text-xs"
238 | >
239 | -
240 |
241 | {item.quantity}
242 | updateQuantity(index, item.quantity + 1)}
244 | className="px-2 py-1 bg-green-600 text-white rounded text-xs"
245 | >
246 | +
247 |
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 |
--------------------------------------------------------------------------------