I've been building UserJot (a user feedback and roadmap platform) for the past several months, and it's been my deepest dive into React yet. After many hours of writing production React code, dealing with performance issues, refactoring components, and discovering better patterns, I've accumulated a collection of tricks that have genuinely improved my development workflow.
These aren't just theoretical concepts. They're practical patterns I use daily. Some saved me from bugs, others made my code cleaner, and a few completely changed how I approach React development. Here are 20 tricks that I think every React developer should have in their toolkit.
1. Use the key prop to reset component state
When you need to completely reset a component's internal state, just change its key. This forces React to unmount and remount the component with fresh state.
function App() {
const [userId, setUserId] = useState(1);
return (
<UserProfile
key={userId} // Changing userId will reset UserProfile's state
userId={userId}
/>
);
}
2. Conditional rendering with short-circuit evaluation
Instead of ternary operators that return null, use && for cleaner conditional rendering. Just watch out for falsy values like 0 that might render unexpectedly.
// Good
{isLoggedIn && <UserDashboard />}
// Careful with numbers
{count > 0 && <Badge count={count} />} // Use comparison to avoid rendering "0"
3. Use optional chaining in event handlers
Prevent crashes when dealing with potentially undefined refs or DOM elements by using optional chaining.
const handleClick = () => {
inputRef.current?.focus();
formRef.current?.scrollIntoView({ behavior: 'smooth' });
};
4. Destructure props with default values
Set default values right in the destructuring pattern to keep your components defensive and reduce the need for defaultProps.
function Button({
text = 'Click me',
onClick = () => {},
disabled = false
}) {
return (
<button onClick={onClick} disabled={disabled}>
{text}
</button>
);
}
5. Use useId for accessible forms
React 18's useId hook generates stable unique IDs perfect for linking form labels to inputs without worrying about SSR mismatches.
function FormField({ label, type = 'text' }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} />
</>
);
}
6. Memoize expensive computations with useMemo
Don't recreate complex objects or run expensive calculations on every render. Use useMemo to cache results between renders.
const expensiveData = useMemo(() => {
return processLargeDataset(rawData);
}, [rawData]); // Only recompute when rawData changes
7. Use useCallback for stable function references
Prevent unnecessary re-renders in child components by keeping function references stable with useCallback.
const handleSearch = useCallback((query) => {
// Search logic here
setResults(searchItems(query));
}, [searchItems]); // Dependencies ensure the function updates when needed
// Now SearchBar won't re-render unless handleSearch actually changes
<SearchBar onSearch={handleSearch} />
8. Create custom hooks for reusable logic
Extract component logic into custom hooks to share stateful logic between components without prop drilling or context overhead.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
9. Use React.lazy for code splitting
Split your bundle by lazy loading components that aren't immediately needed. Combine with Suspense for loading states.
const Analytics = React.lazy(() => import('./Analytics'));
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<Analytics />
</Suspense>
);
}
10. Optimize lists with React.memo
For large lists, wrap list items in React.memo to prevent unnecessary re-renders when the parent updates.
const ListItem = React.memo(({ item, onSelect }) => {
return (
<li onClick={() => onSelect(item.id)}>
{item.name}
</li>
);
}, (prevProps, nextProps) => {
// Custom comparison function (optional)
return prevProps.item.id === nextProps.item.id;
});
11. Use refs for DOM measurements
Need element dimensions or positions? Use refs with useLayoutEffect to measure DOM elements after they render.
function MeasuredComponent() {
const ref = useRef();
const [dimensions, setDimensions] = useState({});
useLayoutEffect(() => {
if (ref.current) {
const { width, height } = ref.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return <div ref={ref}>Content</div>;
}
12. Compose components with children prop patterns
Create flexible wrapper components using children patterns for better composition.
function Card({ children, footer }) {
return (
<div className="card">
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card footer={<Button>Save</Button>}>
<h2>Title</h2>
<p>Content goes here</p>
</Card>
13. Handle async effects with cleanup
Always clean up async operations in useEffect to prevent memory leaks and state updates on unmounted components.
useEffect(() => {
let cancelled = false;
async function fetchData() {
const data = await api.getData();
if (!cancelled) {
setData(data);
}
}
fetchData();
return () => {
cancelled = true;
};
}, []);
14. Use portal for modals and tooltips
Render components outside their parent DOM hierarchy while keeping them in the React tree with createPortal.
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-backdrop">
<div className="modal">{children}</div>
</div>,
document.getElementById('modal-root')
);
}
15. Debounce expensive operations
Use debouncing for search inputs or other expensive operations triggered by user input.
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
const debouncedSearch = useDebounce(searchTerm, 300);
16. Use reducer for complex state logic
When useState gets unwieldy with multiple related state updates, switch to useReducer for better organization.
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'setStep':
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<input
value={state.step}
onChange={(e) => dispatch({
type: 'setStep',
payload: parseInt(e.target.value)
})}
/>
</>
);
}
17. Forward refs for component APIs
Use forwardRef to expose DOM refs or imperative methods from your components.
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => inputRef.current.value = ''
}));
return <input ref={inputRef} {...props} />;
});
// Parent component can now call fancyInputRef.current.focus()
18. Use ErrorBoundary for graceful error handling
Catch JavaScript errors anywhere in the component tree and display a fallback UI instead of crashing the entire app.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong. Please refresh the page.</h2>;
}
return this.props.children;
}
}
// Wrap your app or specific sections
<ErrorBoundary>
<App />
</ErrorBoundary>
19. Optimize context with split contexts
Prevent unnecessary re-renders by splitting frequently changing values from stable ones into separate contexts.
// Instead of one large context
const AppContext = React.createContext();
// Split into logical groups
const UserContext = React.createContext();
const ThemeContext = React.createContext();
const SettingsContext = React.createContext();
// Components only re-render when their specific context changes
20. Use Suspense for data fetching
Combine Suspense with data fetching libraries that support it for cleaner loading states.
// With a Suspense-enabled data fetching library
function UserProfile({ userId }) {
const user = useFetch(`/api/users/${userId}`); // Throws promise while loading
return <div>{user.name}</div>;
}
// Parent handles loading state
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId={1} />
</Suspense>
Wrapping up
These patterns have been invaluable while building UserJot and handling everything from complex state management to performance optimizations. Many of these tricks came from real problems I faced, like managing complex feedback board states, handling real-time updates, and keeping the UI responsive as data scales.
If you're building a SaaS product and need a way to collect user feedback, manage your roadmap, or keep users updated with changelogs, check out UserJot. It's built using many of these patterns, and I'm always discovering new ones as the platform grows.
Top comments (2)
Thank you for sharing this!
And that's why I use Vue 👀 📗