Skip to content
Muhammet Şafak
tr
Interface 4 min read

How React Hooks Changed the Way We Write Components

React 16.8 introduced the Hooks API — here's how it shifted component authoring from class-based to functional, what the advantages are, and what to watch out for.


React 16.8 shipped in February 2019, and hooks became stable. I had been following the announcement since Dan Abramov’s talk at React Conf in October 2018 — the problems he described were painfully familiar: logic scattered across lifecycle methods, bloated class components, the endless confusion around this. I have been using hooks in real projects for a few weeks now and wanted to share my thoughts.

What Was Exhausting About Class Components?

The most tedious part of writing a class component was the way related logic ended up split across different lifecycle methods. Take a subscription, for example: you start it in componentDidMount, tear it down in componentWillUnmount, and restart it in componentDidUpdate whenever the id changes. Three separate methods in three separate places, yet all of them belong to the same “subscription management” concern.

class UserProfile extends React.Component {
  componentDidMount() {
    this.subscription = userService.subscribe(this.props.userId, this.handleUpdate);
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.subscription.unsubscribe();
      this.subscription = userService.subscribe(this.props.userId, this.handleUpdate);
    }
  }

  componentWillUnmount() {
    this.subscription.unsubscribe();
  }

  handleUpdate = (data) => {
    this.setState({ user: data });
  };

  render() {
    return <div>{this.state.user?.name}</div>;
  }
}

The same logic with hooks:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const subscription = userService.subscribe(userId, setUser);
    return () => subscription.unsubscribe();
  }, [userId]);

  return <div>{user?.name}</div>;
}

Everything lives together. The function returned from useEffect acts as the cleanup. The effect re-runs whenever userId changes. This is the exact equivalent of what the class version was doing — but without the fragmentation.

useState and useEffect: Basic, but Sufficient

Two hooks cover a surprisingly wide surface area. useState is straightforward: it returns a value and a function to update it. useEffect handles side effects — data fetching, subscriptions, timers, and direct DOM manipulation.

The dependency array, useEffect’s second argument, is a critical concept. Leave it empty and the effect runs only on the initial render. Provide values and the effect re-runs whenever those values change. Omit it entirely and the effect runs on every render — which is rarely what you want.

// Only on initial render:
useEffect(() => {
  fetchInitialData();
}, []);

// On every render (use with caution):
useEffect(() => {
  document.title = `${count} items`;
});

// Whenever count changes:
useEffect(() => {
  saveToStorage(count);
}, [count]);

Custom Hooks: Sharing Logic the Right Way

The most valuable aspect of hooks is the ability to write custom hooks. Any function whose name starts with use can be a hook; inside it you can call other hooks. This is far cleaner than the mixin or Higher-Order Component (HOC) alternatives.

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);

  return width;
}

// Usable in any component:
function Header() {
  const width = useWindowWidth();
  return <header>{width > 768 ? <DesktopNav /> : <MobileNav />}</header>;
}

In the class-based world I would have reached for an HOC or a render prop to share this kind of logic. Both work, but both create wrapper hell.

Things I Have Had to Watch Out For

A few pitfalls caught me when I first started using hooks:

Stale closure problem. If you access props or state inside useEffect and forget to include them in the dependency array, you can end up reading stale values. ESLint’s exhaustive-deps rule catches most of these cases — I strongly recommend keeping it enabled.

Not reaching for useEffect for every side effect. You do not need useEffect to fetch data inside a click handler — you can call it directly from the event handler. useEffect is for side effects that are a genuine consequence of rendering.

Not rushing to rewrite class components. The React team itself says it: class components are not going away. Rather than spending time migrating existing, working class components to hooks, it makes more sense to write all new code with hooks going forward.

In Summary

Hooks move React toward a more functional place. They allow logic to be grouped by related concept rather than by lifecycle method. I see this change as an improvement, not a revolution. Does it solve every problem that class components introduced? No — but it meaningfully addresses the most common pain points. After a few weeks I have no desire to go back.

That said, when I introduce hooks to a new team member, I still make a point of explaining what class components were solving first. The conveniences hooks bring only make sense when you can see the problem behind them. Using a tool without knowing what it replaced is using it at half capacity — and in a fast-moving ecosystem like React, that gap becomes consequential within a few years.

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind