Why hooks appeared and what did they replace?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React #Hooks

Brief Answer

Hooks appeared in React to solve fundamental problems with class components, such as difficulty reusing logic, confusing lifecycle, and troubles with understanding this. They replaced class components, providing a simpler and declarative way to manage state and side effects in functional components.

Main reasons for hooks appearance:

  • Logic reuse — complexity with HOC and render-props
  • Class complexity — problems with this and lifecycle
  • Component simplification — more readable and compact code
  • Better optimization — opportunities for improvements in React

Full Answer

Hooks were introduced in React 16.8 as a solution to key problems developers faced when working with class components. They radically changed the approach to writing React components.

Problems with Class Components

1. Logic Reuse Complexity

Class components didn’t allow easy logic reuse:

// With HOC and render-props code became cumbersome
const EnhancedComponent = withSubscription(
  withMouseTracker(
    withTheme(
      withAuth(Component)
    )
  )
);

2. Confusing Lifecycle

Lifecycle methods often contained unrelated logic:

// Logic scattered across different lifecycle methods
componentDidMount() {
  // Data subscription
  // Interval setup
  // State initialization
}
 
componentDidUpdate() {
  // Subscription updates
  // Prop changes checking
}
 
componentWillUnmount() {
  // Subscription cleanup
  // Interval stopping
}

What Hooks Replaced

1. Class Components → Function Components

Hooks allowed using state and effects in functional components:

// Instead of class - function with hooks
function Component() {
  const [state, setState] = useState(initialValue);
  useEffect(() => {
    // Effects
  }, []);
  
  return <div>{/* JSX */}</div>;
}

2. HOC and Render Props → Custom Hooks

Custom hooks replaced higher-order patterns and render-props:

// Instead of HOC - reusable hook
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    // Initialization logic
  });
  
  return [value, setValue];
}

Practical Examples

Migration Example

// Class component
class Counter extends Component {
  state = { count: 0 };
  
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };
  
  render() {
    return (
      <button onClick={this.increment}>
        {this.state.count}
      </button>
    );
  }
}
 
// Functional component with hooks
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  
  return (
    <button onClick={increment}>
      {count}
    </button>
  );
}

Custom Hook Example

// Reusable logic as a hook
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(!value), [value]);
  
  return [value, toggle];
}

Common Mistakes

1. Direct Replacement Without Understanding

// ❌ Mechanical replacement without understanding concepts
function BadComponent() {
  const [state1, setState1] = useState();
  const [state2, setState2] = useState();
  const [state3, setState3] = useState();
  // ... many useState calls in a row
}
 
// ✅ Grouping related state
function GoodComponent() {
  const [formState, setFormState] = useState({
    name: '',
    email: '',
    age: ''
  });
}

2. Ignoring Hook Rules

// ❌ Violating hook rules
function Component({ condition }) {
  if (condition) {
    const [state, setState] = useState(); // Error!
  }
}
 
// ✅ Following hook rules
function Component({ condition }) {
  const [state, setState] = useState();
  if (condition) {
    // Logic inside condition
  }
}

Best Practices

  1. Use custom hooks — for logic reuse
  2. Group related state — one useState for related data
  3. Follow hook rules — call hooks at the top level
  4. Separate logic by hooks — one hook - one responsibility
  5. Optimize with useCallback/useMemo — when actually needed

Compatibility

Hooks are fully compatible with existing code and can be used together with class components.

Key Hook Benefits

  1. Simplicity — more readable and compact code
  2. Reusability — easy logic sharing between components
  3. Better composition — hooks can be combined
  4. Less boilerplate — less template code
  5. Better optimization — opportunities for React improvements

Hooks radically simplified React development, making code more readable, reusable, and maintainable compared to class components.


Knowledge Check Task

Task

What problems do hooks solve compared to this class component?

class UserProfile extends Component {
  state = {
    user: null,
    loading: true,
    error: null
  };
 
  componentDidMount() {
    this.fetchUser();
  }
 
  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser();
    }
  }
 
  componentWillUnmount() {
    // Cleanup if needed
  }
 
  fetchUser = async () => {
    try {
      this.setState({ loading: true, error: null });
      const user = await fetchUser(this.props.userId);
      this.setState({ user, loading: false });
    } catch (error) {
      this.setState({ error, loading: false });
    }
  };
 
  render() {
    const { user, loading, error } = this.state;
    
    if (loading) return <Loading />;
    if (error) return <Error message={error.message} />;
    if (!user) return <NotFound />;
    
    return <UserDetails user={user} />;
  }
}
View answer

Answer: Hooks solve several key problems with this class component:

Class component problems:

  1. Scattered logic — data loading code is scattered across different lifecycle methods
  2. this complexity — need for arrow functions and bind
  3. Excessive state — all states combined in one object
  4. Testing complexity — tight coupling of methods and state
  5. Verbosity — lots of boilerplate code

Hook solution:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);
        const userData = await fetchUser(userId);
        setUser(userData);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };
 
    fetchUser();
  }, [userId]); // Automatic subscription to userId changes
 
  if (loading) return <Loading />;
  if (error) return <Error message={error.message} />;
  if (!user) return <NotFound />;
  
  return <UserDetails user={user} />;
}

Benefits of hook solution:

  1. Grouped logic — all data loading logic in one useEffect
  2. No this — no need to worry about context
  3. Separated state — each state separately
  4. Easy testing — easier to mock individual hooks
  5. Compactness — less boilerplate code
  6. Automatic subscription — effect dependencies explicitly specified

Hooks make code more declarative and understandable, eliminating the need to remember complex lifecycle method hierarchies.


Want more articles to prepare for interviews? Subscribe to EasyAdvice, bookmark the site and improve yourself every day 💪