Best Practices and Architectural Patterns
Learn React best practices, architectural patterns, and code organization strategies for building maintainable, scalable React applications.
Overview
In this lesson, you'll help Ollie the Owl build a reusable Accordion component library the right way. You'll see how to compose an Accordion with items (a compound component pattern), handle errors inside panels, split code for heavy content, and organize your files for scale. We'll also cover performance tips (memoization, keys) and using React DevTools to profile your components.
Table of Contents:
- Building the Accordion with compound components
- Controlling open/close behavior
- Conditional rendering of panel content
- Handling errors with an Error Boundary
- Code splitting and lazy loading heavy content
- Organizing files and folders for scalability
- Optimizing performance and profiling
- Best practices & common mistakes
- Recap
Building the accordion with compound components
We start with an Accordion parent and multiple AccordionItem children. Each item has a title and content that toggles open/closed. We'll use React context so the parent can manage state and share it with each item (a common pattern in component libraries).
Loading syntax highlighting...
Each AccordionItem reads from context to know if it's open, and shows its content conditionally:
Loading syntax highlighting...
Explanation: We created an AccordionContext so the parent holds the openIndex and a setter. Each item gets its index prop and checks if it matches openIndex. Clicking the header toggles that index. This compound component pattern keeps state logic in the parent and lets children share it, avoiding prop-drilling. We can use it like:
Loading syntax highlighting...
Don't forget to give each item a stable key when rendering lists. For example:
<AccordionItem key={item.id} ...>
to avoid React warnings and bugs (keys must be unique and not generated on the fly).
Controlling open/close behavior
By default, our Accordion is uncontrolled (it manages its own state). Sometimes you want it controlled from outside (e.g. only one item open or an "all collapsed" mode). You can accept optional props like defaultIndex, openIndex, or onChange to make it controllable. For example:
Loading syntax highlighting...
Explanation: We added defaultIndex, openIndex and onChange props. If openIndex is passed, the component is controlled and calls onChange. Otherwise it manages its own state with useState. This makes our Accordion flexible.
Don't mutate state directly. Always use setState (or in this case, context setters) to update. Mutating objects or arrays in state can cause React to skip re-renders or break the UI. React state should be treated as immutable.
Conditional rendering of panel content
We only render a panel's content when its section is open. This is simple conditional rendering: we did {isOpen && <div>Content</div>}
. For more complex UIs, conditional rendering can include toggling classes, swapping components, or using useEffect hooks for lazy loading.
For example, we might want to mount heavy content only when needed. We already have:
Loading syntax highlighting...
If children is a dynamic component, it won't render or run until isOpen is true. This keeps the DOM lightweight and avoids unnecessary work.
⚠️ Always include unique key props in lists of items. Forgetting keys can cause React to reuse DOM elements incorrectly. Never use array index or random values as keys, as that defeats their purpose. Use stable IDs or identifiers from your data.
Handling errors with an error boundary
If an AccordionItem contains components that might throw an error (e.g. fetching data or unstable code), we don't want one item's crash to break the entire accordion. We can wrap items in an Error Boundary. Error boundaries are special React components (class-based) that catch exceptions in their descendants.
Loading syntax highlighting...
We can use it like:
Loading syntax highlighting...
Explanation: By wrapping AccordionItem (or the children) in <ErrorBoundary>
, any error inside StatsChart is caught. Instead of crashing the app, the fallback UI is shown. As React docs explain, error boundaries "are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI". We only need a few lines of code to protect the component tree.
Code splitting and lazy loading heavy content
If an accordion panel has very heavy or rarely-used content (like a big chart library or a media player), we should split that code so it doesn't slow down the initial load. React's React.lazy and Suspense make this easy. For example:
Loading syntax highlighting...
Explanation: We replaced inline content with <ExpensiveChart>
which is loaded on demand. Until it loads, the <Suspense>
fallback is shown. As noted by web performance experts, "the React.lazy method makes it easy to code-split a React application on a component level using dynamic imports". This way, the main Accordion bundle stays small and only fetches extra code when a user expands that panel.
⚠️ Remember that lazy-loaded components need a
<Suspense>
boundary. Otherwise React will error if the component isn't yet loaded.
Organizing files and folders for scalability
A clear project structure helps collaboration and maintenance. For example, we might put all accordion-related code in one folder:
File Structuresrccomponents│├─Accordion││├─Accordion.tsx││├─AccordionItem.tsx││├─ErrorBoundary.tsx││└─index.ts
In index.ts we can export the components:
Loading syntax highlighting...
Explanation: Grouping by feature (the Accordion) keeps related files together. We export through an index.ts so other code can do import { Accordion } from './components/Accordion';
. A good folder structure—tailored to your app—ensures the codebase stays scalable and maintainable. As one guide puts it, "Choosing the right folder structure… is essential for building scalable, maintainable applications.". Keep it consistent: perhaps have folders for components/, hooks/, utils/, etc., as your app grows.
Optimizing performance and profiling
Even with the right structure, React components can be slow if not optimized. Some tips:
- Use React.memo on pure functional components. If an AccordionItem only cares about its props, wrapping it with
React.memo()
prevents unnecessary re-renders when its props haven't changed. - Avoid expensive calculations during render. If you compute derived data, use useMemo. But as React docs warn: "Only rely on useMemo as a performance optimization. If your code doesn't work without it, find the underlying problem first". In practice, only memoize heavy operations (e.g. sorting a large list) so you skip redoing it on every render.
- Use stable keys and avoid inline functions as props. Unstable references cause child components to re-render. For callbacks, you can use useCallback if passing functions to deep children.
- Use React DevTools/Profiler. The React Profiler (in DevTools or with the
<Profiler>
API) lets you measure render times. For example, the official docs say: "<Profiler>
lets you measure rendering performance of a React tree programmatically.". In the browser, open React DevTools' Profiler to see which component takes the most time on updates. This can reveal "costly" components or repeated renders. Profiling helps you target the actual slow parts, rather than guessing.
With these optimizations, your Accordion will stay snappy even in a large app.
⚠️ Don't forget to test performance. Use React DevTools (Profiler) or console timers to check if a part is slow before and after optimizations. Premature optimization can waste effort, but missed optimizations can make the UI janky.
Best practices & common mistakes – Avoid dumb bugs
- Props vs State: Decide if a component is controlled or uncontrolled. Avoid mixing both patterns incorrectly.
- State immutability: Never mutate state/props directly. Always create new arrays/objects when updating state.
- Hook rules: Don't call hooks inside loops or conditionally. Hooks must be at the top level of your component.
- Dependency arrays: Always include dependencies in useEffect/useCallback. A missing dependency can cause stale values; unnecessary deps can cause infinite loops.
- Keys in lists: As mentioned, keys must be stable and unique. Avoid using an item's index or random values.
- Error boundaries limits: Remember error boundaries catch rendering errors only. They won't catch errors in event handlers or async code. (You still need try/catch in promises or error handlers there.)
- Suspense fallback: Provide a user-friendly loading state in
<Suspense>
. Users should see something while heavy components load.
⚠️ Remember: useMemo and useCallback are performance tools, not fixes. If your app is slow, profile first instead of sprinkling memo everywhere.
Recap
- We built an Accordion using compound components (parent + child) so state is shared cleanly.
- We saw how to toggle panels via context and useState. Controlled vs uncontrolled components give flexibility.
- Panel content is conditionally rendered only when open, keeping the DOM light.
- We added an Error Boundary wrapper to catch errors in any panel, showing a fallback instead of crashing the app.
- We used React.lazy and Suspense to code-split heavy panel content and show a loading indicator.
- We organized our files by feature (e.g. Accordion/Accordion.tsx, AccordionItem.tsx) to keep the project scalable.
- Performance optimizations: we talked about React.memo, useMemo, and keys in lists. We should use React DevTools' Profiler (
<Profiler>
) to measure rendering costs. - Always watch out for common pitfalls: immutable state, hook rules, stable keys, and proper use of dependencies.
Now you know how to build a real-world Accordion component with strong architecture and best practices, ready to grow into a robust component library. Happy Coding!