✨React.js Hacks: Part 2 – Next-Level Tricks for Clean & Powerful Code
Hey again! 👋
You loved the first round of React.js hacks, and you’ve been crushing it with && conditionals and object spreads. But you’re ready to level up. You’re starting to feel the pain of prop drilling, confusing state flows, and those pesky unnecessary re-renders.
This isn’t about the basics anymore. This is Part 2: The Pro Edition. These are the hacks that separate solid React developers from truly exceptional ones. They’ll help you write more performant, maintainable, and downright elegant code.
Let’s dive into the next set of game-changers.
1. The useCallback & useMemo Performance Duo (No More Guesswork)
The Problem: Your component re-renders every time, even when nothing important has changed. You’re passing functions and objects as props or dependencies, and it’s causing unnecessary computation and child re-renders.
The Hack: Use useCallback for functions and useMemo for expensive calculations to memoize values and prevent them from being recreated on every render.
// 🚫 Recreates this function on every single render
const handleClick = () => {
setCount(count + 1);
};
// ✅ useCallback memoizes the function itself
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // Only recreate if `count` changes
// 🚫 Recalculates this expensive thing on every render
const expensiveValue = someArray.filter(...).map(...).find(...);
// ✅ useMemo memoizes the result of the calculation
const expensiveValue = useMemo(() => {
return someArray.filter(...).map(...).find(...);
}, [someArray]); // Only recalculate if `someArray` changes
When to use it: Don’t just slap it everywhere! Use these hooks when:
-
You pass a function as a prop to a heavily optimized child component (e.g., wrapped in React.memo).
-
You’re performing a computationally expensive operation.
-
The value is a dependency of another hook (like useEffect).
2. The Custom Hook Power Play (Stop Repeating Yourself)
The Problem: You find yourself writing the same logic over and over in different components: data fetching, form handling, listening to window size, etc.
The Hack: Custom Hooks. Extract your component logic into reusable functions. They’re just JavaScript functions that can call other hooks.
// 🚫 Duplicated fetching logic in multiple components
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then(setUser)
.finally(() => setLoading(false));
}, []);
// ... component JSX
}
// ✅ Create a reusable useFetch hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then(setData)
.finally(() => setLoading(false));
}, [url]);
return { data, loading };
}
// 🎉 Now use it in any component!
function UserProfile() {
const { data: user, loading } = useFetch('/api/user');
// ... clean component JSX
}
Why it’s awesome: It makes your components incredibly clean and turns complex logic into a simple one-liner. It’s the ultimate form of code reuse in React.
3. The useReducer Upgrade for Complex State
The Problem: Your component’s useState is getting out of hand. You have multiple pieces of state that change together, or your state updates are complex with lots of nested objects.
The Hack: Use useReducer for state that involves complex logic or multiple sub-values. It’s like useState but for when you need a more powerful, predictable way to manage state transitions.
// 🚫 Messy useState with complex updates
const [state, setState] = useState({
loading: false,
data: null,
error: null,
});
const fetchData = async () => {
setState({ ...state, loading: true, error: null }); // 🤮
try {
const data = await someApiCall();
setState({ ...state, loading: false, data: data });
} catch (error) {
setState({ ...state, loading: false, error: error.message });
}
};
// ✅ Clean, predictable useReducer
const initialState = { loading: false, data: null, error: null };
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchData = async () => {
dispatch({ type: 'FETCH_START' }); // 😍 So clear!
try {
const data = await someApiCall();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
}
Why it’s awesome: It centralizes your state logic, makes it more predictable and easier to test, and keeps your component code clean.
4. The Prop Spreading Cautionary Tale
The Problem: You’re passing a dozen props down to a child component. Your code is cluttered with propName={propName}.
// 🚫 So repetitive!
<ChildComponent
title={title}
description={description}
onClick={onClick}
userId={userId}
className="card"
data-testid="child"
/>
The Hack (with a WARNING): You can use the spread operator {…props}. But be incredibly careful. Only do this when you are intentionally passing all props through to a child.
// ✅ Much cleaner, but KNOW what you're spreading!
const parentProps = { title, description, onClick, userId };
<ChildComponent {...parentProps} className="card" data-testid="child" />
⚠️ The Critical Warning: Never blindly spread props ({…props}) from a component’s function parameters. You can accidentally pass unnecessary or even harmful props down to DOM elements, which React will warn you about (e.g., passing a theme prop to a
).
When to use it: When you are acting as a pure “pass-through” component or building a wrapper around a native element.
5. The Lazy Load & Suspense Trick for Blazing Speed
The Problem: Your main JavaScript bundle is huge because it contains every single component. Your users are waiting forever for the initial page to load.
The Hack: Use React.lazy and Suspense to code-split your application and lazily load components only when they are needed.
// 🚫 Heavy component is in the main bundle
import HeavyComponent from './HeavyComponent';
// ✅ This component is split into a separate chunk and loaded on demand
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function MyApp() {
return (
<div>
<Suspense fallback={<div>Loading a awesome component...</div>}>
{/* HeavyComponent won't load until it's rendered */}
<HeavyComponent />
</Suspense>
</div>
);
}
Why it’s awesome: It dramatically reduces your initial bundle size, leading to much faster load times and a better user experience. It’s a cornerstone of modern, performance-first React.
Mastering React is a journey. These “pro” hacks aren’t just about writing less code; they’re about writing smarter, more resilient, and more scalable code.
Don’t feel pressured to use them all at once. Pick one that solves a problem you’re facing right now and try it out.
What’s your favorite advanced React hack? Share it in the comments below!
Check out Part 1 here: