Skip to main content
โšก Calmops

React Hooks: useState, useEffect, useContext

React Hooks: useState, useEffect, useContext

React Hooks enable state management and side effects in functional components. This article covers essential hooks and patterns.

Introduction

React Hooks provide:

  • State management
  • Side effect handling
  • Context consumption
  • Code reusability
  • Cleaner components

Understanding hooks helps you:

  • Manage component state
  • Handle side effects
  • Share state across components
  • Create reusable logic
  • Write cleaner code

useState Hook

Basic useState

import { useState } from 'react';

// โœ… Good: Basic state
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// โœ… Good: Multiple state variables
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(0);

  return (
    <form>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={age} onChange={(e) => setAge(Number(e.target.value))} />
    </form>
  );
}

// โœ… Good: State with objects
function User() {
  const [user, setUser] = useState({
    name: 'John',
    age: 30,
    email: '[email protected]'
  });

  const updateName = (newName) => {
    setUser({ ...user, name: newName });
  };

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

// โœ… Good: State with arrays
function TodoList() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text }]);
  };

  const removeTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          {todo.text}
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}

Lazy Initialization

// โœ… Good: Lazy initialization
function ExpensiveComponent() {
  const [state, setState] = useState(() => {
    // This runs only once on mount
    const initialState = expensiveCalculation();
    return initialState;
  });

  return <div>{state}</div>;
}

// โœ… Good: Functional updates
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    // Use functional update for dependent updates
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

useEffect Hook

Basic useEffect

import { useEffect, useState } from 'react';

// โœ… Good: Run effect on mount
function Component() {
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  return <div>Component</div>;
}

// โœ… Good: Run effect on dependency change
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

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

// โœ… Good: Cleanup function
function Timer() {
  useEffect(() => {
    const interval = setInterval(() => {
      console.log('Tick');
    }, 1000);

    // Cleanup function
    return () => clearInterval(interval);
  }, []);

  return <div>Timer</div>;
}

// โœ… Good: Multiple effects
function Component() {
  useEffect(() => {
    console.log('Effect 1');
  }, []);

  useEffect(() => {
    console.log('Effect 2');
  }, []);

  return <div>Component</div>;
}

Common Patterns

// โœ… Good: Fetch data on mount
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await fetch('/api/users');
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// โœ… Good: Debounced search
function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      if (query) {
        searchUsers(query).then(setResults);
      }
    }, 500);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search users"
      />
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

// โœ… Good: Event listener
function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>Width: {size.width}, Height: {size.height}</div>;
}

useContext Hook

Basic useContext

import { createContext, useContext } from 'react';

// โœ… Good: Create context
const ThemeContext = createContext();

// โœ… Good: Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// โœ… Good: Use context
function ThemedComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#000' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

// โœ… Good: App setup
function App() {
  return (
    <ThemeProvider>
      <ThemedComponent />
    </ThemeProvider>
  );
}

Multiple Contexts

// โœ… Good: Multiple contexts
const AuthContext = createContext();
const NotificationContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  const addNotification = (message) => {
    setNotifications([...notifications, message]);
  };

  return (
    <NotificationContext.Provider value={{ notifications, addNotification }}>
      {children}
    </NotificationContext.Provider>
  );
}

// โœ… Good: Use multiple contexts
function Dashboard() {
  const { user } = useContext(AuthContext);
  const { notifications } = useContext(NotificationContext);

  return (
    <div>
      <h1>Welcome, {user?.name}</h1>
      <p>Notifications: {notifications.length}</p>
    </div>
  );
}

// โœ… Good: App with multiple providers
function App() {
  return (
    <AuthProvider>
      <NotificationProvider>
        <Dashboard />
      </NotificationProvider>
    </AuthProvider>
  );
}

Custom Hooks

Creating Custom Hooks

// โœ… Good: Custom hook for form handling
function useForm(initialValues, onSubmit) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(values);
  };

  const reset = () => {
    setValues(initialValues);
  };

  return { values, handleChange, handleSubmit, reset };
}

// Usage
function LoginForm() {
  const { values, handleChange, handleSubmit } = useForm(
    { email: '', password: '' },
    (values) => console.log('Submit:', values)
  );

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={values.email}
        onChange={handleChange}
      />
      <input
        name="password"
        type="password"
        value={values.password}
        onChange={handleChange}
      />
      <button type="submit">Login</button>
    </form>
  );
}

// โœ… Good: Custom hook for API calls
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// โœ… Good: Custom hook for local storage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    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);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage
function Preferences() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>Theme: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

Best Practices

  1. Use hooks at top level:

    // โœ… Good
    function Component() {
      const [state, setState] = useState(0);
      return <div>{state}</div>;
    }
    
    // โŒ Bad
    function Component() {
      if (condition) {
        const [state, setState] = useState(0);
      }
    }
    
  2. Use functional updates:

    // โœ… Good
    setCount(prevCount => prevCount + 1);
    
    // โŒ Bad
    setCount(count + 1);
    
  3. Clean up effects:

    // โœ… Good
    useEffect(() => {
      const timer = setInterval(() => {}, 1000);
      return () => clearInterval(timer);
    }, []);
    
    // โŒ Bad
    useEffect(() => {
      setInterval(() => {}, 1000);
    }, []);
    

Summary

React Hooks are essential. Key takeaways:

  • Use useState for state management
  • Use useEffect for side effects
  • Use useContext for global state
  • Create custom hooks for reusable logic
  • Clean up effects properly
  • Use functional updates
  • Follow hook rules

Next Steps

Comments