My Technical Blog

Insights on software engineering, AI/ML, and web development

Advanced React Patterns for Clean Component Design
3 min read
ReactJavaScriptFrontend

Advanced React Patterns for Clean Component Design

When building React applications, the way you structure your components can make a significant difference in how maintainable and reusable your code will be. In this post, I'll cover some advanced patterns I've found extremely useful.

1. The Compound Component Pattern

Compound components are a pattern where components are used together to form a cohesive unit of functionality, but while maintaining a separation of concerns.

// Example implementation of compound component pattern
const Tabs = ({ children, defaultActiveTab }) => {
  const [activeTab, setActiveTab] = useState(defaultActiveTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
};

Tabs.TabList = ({ children }) => <div className="tab-list">{children}</div>;
Tabs.Tab = ({ id, children }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      className={activeTab === id ? "active-tab" : "tab"}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
};
Tabs.TabPanels = ({ children }) => <div className="tab-panels">{children}</div>;
Tabs.TabPanel = ({ id, children }) => {
  const { activeTab } = useContext(TabsContext);
  return activeTab === id ? <div className="tab-panel">{children}</div> : null;
};

Usage:

<Tabs defaultActiveTab="tab1">
  <Tabs.TabList>
    <Tabs.Tab id="tab1">Tab 1</Tabs.Tab>
    <Tabs.Tab id="tab2">Tab 2</Tabs.Tab>
  </Tabs.TabList>
  <Tabs.TabPanels>
    <Tabs.TabPanel id="tab1">Tab 1 content</Tabs.TabPanel>
    <Tabs.TabPanel id="tab2">Tab 2 content</Tabs.TabPanel>
  </Tabs.TabPanels>
</Tabs>

2. The Render Props Pattern

Render props are a technique where a component receives a function as a prop, which it then calls with its internal state data.

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({
        x: event.clientX,
        y: event.clientY,
      });
    };

    window.addEventListener("mousemove", handleMouseMove);
    return () => window.removeEventListener("mousemove", handleMouseMove);
  }, []);

  return render(position);
}

Usage:

<MouseTracker
  render={({ x, y }) => (
    <div>
      The current mouse position is ({x}, {y})
    </div>
  )}
/>

3. Custom Hooks for Logic Reuse

Custom hooks are the preferred way to share logic between components in React's ecosystem.

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === "undefined") {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      if (typeof window !== "undefined") {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

Usage:

function App() {
  const [name, setName] = useLocalStorage("name", "Guest");

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
}

Performance Optimization

Beyond patterns, there are several techniques for optimizing performance in React applications:

1. Memoization with useMemo and useCallback

// Without memoization
const filteredUsers = users.filter((user) => user.active);

// With memoization
const filteredUsers = useMemo(
  () => users.filter((user) => user.active),
  [users]
);

2. Using React.memo for Component Memoization

const UserProfile = React.memo(function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
});

Conclusion

By adopting these patterns, you can create React applications that are not only performant but also easier to maintain and extend. The key is to understand when each pattern is appropriate and to use them judiciously rather than trying to force them everywhere.

Remember that the best pattern is the one that makes your code more readable and maintainable in your specific use case. Don't apply patterns just for the sake of using patterns!

What advanced React patterns have you found most useful in your projects? I'd love to hear your thoughts in the comments below.

About the Author

Olanrewaju Olaboye

Olanrewaju Olaboye(BoyePanthera)

Software, Ai/ML Engineer specializing in building scalable Ai powered applications with Node.js and React. Passionate about teaching and sharing knowledge through technical writing.