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

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
  • Hooks
  • Frontend Framework

扫一扫,分享到微信

微信分享二维码
Next.js Fundamentals
React Fundamentals
目录
  1. 1. React Hooks
    1. 1.1. Introduction to Hooks
      1. 1.1.1. Why Hooks?
    2. 1.2. Basic Hooks
      1. 1.2.1. useState
        1. 1.2.1.1. Array Destructuring
        2. 1.2.1.2. Multiple State Variables
        3. 1.2.1.3. Functional Updates
        4. 1.2.1.4. Lazy Initial State
      2. 1.2.2. useEffect
        1. 1.2.2.1. Effect with Cleanup
        2. 1.2.2.2. Controlling When Effects Run
        3. 1.2.2.3. Multiple Effects
      3. 1.2.3. useContext
    3. 1.3. Additional Hooks
      1. 1.3.1. useReducer
        1. 1.3.1.1. Lazy Initialization
        2. 1.3.1.2. When to Use
      2. 1.3.2. useCallback
      3. 1.3.3. useMemo
        1. 1.3.3.1. When to Use
      4. 1.3.4. useRef
        1. 1.3.4.1. UseRef for Variables
      5. 1.3.5. useImperativeHandle
      6. 1.3.6. useLayoutEffect
        1. 1.3.6.1. useEffect vs useLayoutEffect
      7. 1.3.7. useDebugValue
    4. 1.4. Custom Hooks
      1. 1.4.1. Common Custom Hooks
        1. 1.4.1.1. useFetch - For Data Fetching
        2. 1.4.1.2. useLocalStorage - For Persistent State
        3. 1.4.1.3. useForm - For Form Handling
    5. 1.5. Rules of Hooks
      1. 1.5.1. Only Call Hooks at the Top Level
      2. 1.5.2. Only Call Hooks from React Functions
    6. 1.6. Advanced Patterns with Hooks
      1. 1.6.1. State Reducer Pattern
      2. 1.6.2. Compound Components Pattern
      3. 1.6.3. Hooks Composition
    7. 1.7. Performance Optimization
      1. 1.7.1. When to Memoize
      2. 1.7.2. Avoiding Common Performance Mistakes
    8. 1.8. Testing Hooks
      1. 1.8.1. Basic Testing with React Testing Library
      2. 1.8.2. Testing Components that Use Hooks
    9. 1.9. Learning Resources

153 篇 | 133k
次 | 人
这里自动载入天数这里自动载入时分秒
2022-2025 loong loong | 新南威尔士龙龙号