Menu

React & Next.js Topic

React State Management with Hooks

1. useState - Managing Component State

Basic Usage

jsx

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: '', age: 0 });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      
      <input 
        value={user.name}
        onChange={(e) => setUser({...user, name: e.target.value})}
        placeholder="Name"
      />
    </div>
  );
}

Functional Updates

jsx

function Counter() {
  const [count, setCount] = useState(0);

  // Use functional update when new state depends on previous state
  const increment = () => setCount(prevCount => prevCount + 1);
  const incrementBy = (amount) => setCount(prevCount => prevCount + amount);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={() => incrementBy(5)}>+5</button>
    </div>
  );
}

2. useEffect - Handling Side Effects

Basic Effects

jsx

import React, { useState, useEffect } from 'react';

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

  // Effect runs after every render
  useEffect(() => {
    document.title = user ? `${user.name}'s Profile` : 'Loading...';
  });

  // Effect runs only when userId changes
  useEffect(() => {
    if (!userId) return;

    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Dependency array

  // Cleanup effect (runs on unmount)
  useEffect(() => {
    const handleResize = () => console.log('Window resized');
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty dependency array = runs once on mount

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

3. useMemo - Memoizing Expensive Calculations

jsx

import React, { useState, useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  // This expensive calculation only re-runs when items or filter change
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]); // Recompute only when dependencies change

  // Without useMemo, this would run on every render
  const sortedItems = useMemo(() => {
    return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
  }, [filteredItems]);

  return (
    <div>
      {sortedItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

4. useCallback - Memoizing Functions

jsx

import React, { useState, useCallback, memo } from 'react';

const ChildComponent = memo(({ onButtonClick, data }) => {
  console.log('Child rendered'); // This won't log unnecessarily
  return <button onClick={onButtonClick}>{data}</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Without useCallback, this function would be recreated on every render
  // causing ChildComponent to re-render unnecessarily
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1);
  }, []); // Same function reference unless dependencies change

  // Function that depends on state
  const handleTextUpdate = useCallback((newText) => {
    setText(newText);
    console.log(`Count is: ${count}`); // This would use stale closure
  }, [count]); // Include count as dependency

  return (
    <div>
      <p>Count: {count}</p>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)}
      />
      <ChildComponent 
        onButtonClick={handleClick} 
        data={`Count: ${count}`}
      />
    </div>
  );
}

5. useRef - Persistent References

jsx

import React, { useState, useRef, useEffect } from 'react';

function RefExamples() {
  const [count, setCount] = useState(0);
  const inputRef = useRef(null);
  const previousCountRef = useRef();
  const renderCountRef = useRef(0);

  // Store previous value
  useEffect(() => {
    previousCountRef.current = count;
  }, [count]);

  // Count renders (without causing re-renders)
  renderCountRef.current += 1;

  const focusInput = () => {
    inputRef.current.focus();
    inputRef.current.select();
  };

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {previousCountRef.current}</p>
      <p>Renders: {renderCountRef.current}</p>
      
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

6. Understanding Re-renders

When Components Re-render

jsx

import React, { useState, memo } from 'react';

// Child components re-render when:
// 1. Their own state changes
// 2. Their props change
// 3. Parent component re-renders

const ExpensiveChild = memo(({ user, onUpdate }) => {
  console.log('ExpensiveChild rendered');
  return (
    <div>
      <p>{user.name}</p>
      <button onClick={() => onUpdate(user.id)}>Update</button>
    </div>
  );
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ id: 1, name: 'John' });
  const [text, setText] = useState('');

  // This would cause ExpensiveChild to re-render every time
  // because a new function is created on each render
  const badHandleUpdate = (userId) => {
    console.log('Update user:', userId);
  };

  // This prevents unnecessary re-renders
  const goodHandleUpdate = React.useCallback((userId) => {
    console.log('Update user:', userId);
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Re-render Parent ({count})
      </button>
      
      <input 
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="This triggers re-renders"
      />

      {/* With bad function - re-renders every time */}
      <ExpensiveChild user={user} onUpdate={badHandleUpdate} />
      
      {/* With good function - only re-renders when user changes */}
      <ExpensiveChild user={user} onUpdate={goodHandleUpdate} />
    </div>
  );
}

Performance Optimization Patterns

jsx

import React, { useState, useMemo, useCallback, memo } from 'react';

// Optimized component
const UserList = memo(({ users, onUserSelect, filter }) => {
  const filteredUsers = useMemo(() => {
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);

  return (
    <div>
      {filteredUsers.map(user => (
        <UserItem 
          key={user.id} 
          user={user} 
          onSelect={onUserSelect}
        />
      ))}
    </div>
  );
});

const UserItem = memo(({ user, onSelect }) => {
  return (
    <div onClick={() => onSelect(user.id)}>
      {user.name}
    </div>
  );
});

function OptimizedParent() {
  const [users, setUsers] = useState([]);
  const [selectedUserId, setSelectedUserId] = useState(null);
  const [filter, setFilter] = useState('');

  const handleUserSelect = useCallback((userId) => {
    setSelectedUserId(userId);
  }, []);

  const addUser = useCallback((name) => {
    setUsers(prev => [...prev, { id: Date.now(), name }]);
  }, []);

  return (
    <div>
      <input 
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter users..."
      />
      
      <UserList 
        users={users}
        filter={filter}
        onUserSelect={handleUserSelect}
      />
    </div>
  );
}

Key Principles for State Management

1. State Structure

jsx

// Avoid: Separate related state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [age, setAge] = useState(0);

// Better: Group related state
const [user, setUser] = useState({
  firstName: '',
  lastName: '', 
  age: 0
});

// Update nested state properly
setUser(prev => ({
  ...prev,
  firstName: 'John'
}));

2. Effect Dependencies

jsx

function Component({ id, fetchData }) {
  const [data, setData] = useState(null);

  // Missing dependency
  useEffect(() => {
    fetchData(id).then(setData);
  }, [id]); // Missing fetchData

  // Correct dependencies
  useEffect(() => {
    fetchData(id).then(setData);
  }, [id, fetchData]); // Include all dependencies

  // Using useCallback for functions
  const stableFetchData = useCallback((id) => {
    // fetch implementation
  }, []); // No dependencies needed if function doesn't use props/state
}

3. When to Use Each Hook

  • useState: Component-level state

  • useEffect: Side effects, subscriptions, API calls

  • useMemo: Expensive calculations, derived data

  • useCallback: Function props to optimized children

  • useRef: DOM access, persistent values without re-renders

This comprehensive approach to React hooks will help you build efficient, performant applications with proper state management and minimal unnecessary re-renders.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?