React Component Patterns You Should Know (But Probably Don’t)

When making React app, most people just write components in random way. They put all logic, state, and UI in one file, and later, when app gets big, everything becomes mess. But there are better ways to do it. React has some patterns that help keep code clean, easy to read, and not so hard to fix when something goes wrong.

Here are some patterns you maybe never heard about but should know.


1. Container and Presentational Components

Most people mix logic and UI together in same component. This makes it hard to reuse and test. Instead, better way is to split them.

  • Container Component → Only takes care of logic, fetching data, and state.
  • Presentational Component → Only takes care of showing UI, taking props, and displaying them.

Example:

// Container Component (handles logic)
function UserContainer() {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    fetch("https://api.example.com/user")
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);

  return <UserProfile user={user} />;
}

// Presentational Component (only UI)
function UserProfile({ user }) {
  if (!user) return <p>Loading...</p>;
  return <h1>Welcome, {user.name}!</h1>;
}        

Now if you want different UI but same logic, you just change UserProfile. No need to rewrite everything.


2. Compound Components

Instead of passing too many props, this pattern lets components work together in smart way. Good example is <select> and <option> in HTML – they work together but still separate.

Example:

function Tabs({ children }) {
  const [active, setActive] = React.useState(0);

  return React.Children.map(children, (child, index) =>
    React.cloneElement(child, { active, index, setActive })
  );
}

function Tab({ index, active, setActive, children }) {
  return (
    <button
      style={{ fontWeight: active === index ? "bold" : "normal" }}
      onClick={() => setActive(index)}
    >
      {children}
    </button>
  );
}

function TabPanel({ index, active, children }) {
  return active === index ? <div>{children}</div> : null;
}

// Usage
<Tabs>
  <Tab>Tab 1</Tab>
  <Tab>Tab 2</Tab>
  <TabPanel>Content for Tab 1</TabPanel>
  <TabPanel>Content for Tab 2</TabPanel>
</Tabs>        

Now Tab and TabPanel know what to do without passing too many props.


3. Render Props

Instead of making a component do one thing, render props lets it be more flexible.

Example:

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

  React.useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handleMove);
    return () => window.removeEventListener("mousemove", handleMove);
  }, []);

  return render(position);
}

// Usage
<MouseTracker render={({ x, y }) => <p>Mouse at {x}, {y}</p>} />        

Now, you can use MouseTracker in different ways without changing the main component.


4. Higher-Order Components (HOC)

HOCs are functions that take a component and return a new one with extra features. Useful for things like authentication or logging.

Example:

function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const user = localStorage.getItem("user");
    if (!user) return <p>Please log in</p>;
    return <Component {...props} user={JSON.parse(user)} />;
  };
}

// Usage
const Dashboard = ({ user }) => <h1>Welcome {user.name}!</h1>;
const ProtectedDashboard = withAuth(Dashboard);        

Instead of adding login checks everywhere, just wrap component in withAuth.


5. Controlled vs Uncontrolled Components

When dealing with forms, people often forget difference between controlled and uncontrolled components.

  • Controlled Component → React controls the value using useState.
  • Uncontrolled Component → Uses ref to get the value when needed.

Example of Controlled:

function ControlledInput() {
  const [value, setValue] = React.useState("");

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

Example of Uncontrolled:

function UncontrolledInput() {
  const inputRef = React.useRef();

  function handleSubmit() {
    alert(inputRef.current.value);
  }

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
}        

Use controlled components when you need React to manage state, and use uncontrolled when you don’t need to re-render on every change.


Final Thoughts

If you only write React components in one way, your app will be hard to manage later. These patterns make code easier to read, reuse, and scale. You don’t need to use all of them, but knowing them will help you become better at React.

Try using Container & Presentational Components to separate logic from UI, Compound Components to make components work together smartly, and Render Props or HOCs to add reusable behavior.

Which pattern do you already use, and which one do you want to try next?

To view or add a comment, sign in

More articles by Yohanes Bogosyan

Others also viewed

Explore content categories