React Hooks Fundamentals: State, Effects & Best Practices
🧠 Understanding React State & Hooks
React has a set of functions called hooks that allow you to add additional logic such as state to your components. You can think of state as any information in your UI that changes over time, usually triggered by user interaction.
Key Differences:
- Props are read-only information that’s passed to components
- State is information that can change over time, usually triggered by user interaction
Important Rule: You can pass state information to children components as props, but the logic for updating the state should be kept within the component where state was initially created.
Resource: 10 React Hooks Explained
Hook Rules
- Hooks in React start with the word “use”
- Can only be used in functional components
- Must be used right after the function declaration
🐛 Development Mode Behavior
Note: In React development mode (and Next.js), console logs and effects may run twice. This is intentional behavior called “Strict Mode” that helps detect side effects and ensures your components work correctly. This double execution only happens in development - production builds run normally.
🪝 Essential React Hooks
useState - Handle Reactive Data
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Alternative using previous state:
function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(prev => prev + 1)}>
{count}
</button>
);
}
When count
changes, the component will re-render.
Initialization Strategies
Direct Value (re-renders on every change):
const [count, setCount] = useState(0);
Function (only renders on first load - better for expensive computations):
const [count, setCount] = useState(() => {
return 1/2 * 7671265;
});
Updating State Correctly
When updating state multiple times, use the function version to avoid stale state:
const countPlus = () => {
setCount(prevCount => prevCount + 1);
}
<button onClick={countPlus}>Increment</button>
useEffect - Side Effects & Lifecycle
useEffect allows you to mimic class component lifecycle methods:
// Class component lifecycle equivalent
componentDidMount() {
// initialized
}
componentDidUpdate() {
// state updated
}
componentWillUnmount() {
// destroyed
}
useEffect Patterns
Run on every render:
useEffect(() => {
alert('hello');
});
Run only once (on mount):
useEffect(() => {
alert('hello');
}, []); // Empty dependency array
Run when specific state changes:
useEffect(() => {
alert('count changed');
}, [count]); // Runs when count updates
With cleanup (unmount):
useEffect(() => {
alert('hello side effect!');
return () => alert('goodbye component!');
}, []);
This demonstrates cleanup functions:
hello side effect!
runs when component mountsgoodbye component!
runs when component unmounts (cleanup phase)
useLayoutEffect
Similar to useEffect but runs synchronously after rendering the component but before painting to the screen. This means the system blocks until the callback finishes execution.
Use case: When you need to read layout properties or make DOM mutations that affect layout before the browser paints.
import { useLayoutEffect, useRef, useState } from 'react';
function MeasureComponent() {
const divRef = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
// This runs before the browser paints
if (divRef.current) {
const rect = divRef.current.getBoundingClientRect();
setHeight(rect.height);
}
}, []);
return (
<div ref={divRef}>
<p>This div's height is: {height}px</p>
</div>
);
}
Key difference: useEffect would cause a flicker as the height updates after painting, while useLayoutEffect prevents this by measuring before the browser paints.