Skip to content
intermediate Phase 4 · TypeScript with React

Typed Hooks

Type useState, useRef, useCallback, and custom hooks with TypeScript.

1h
0 problems
Topic Progress 0%

useState Typing

useState Typing

Basic useState

import { useState } from 'react';

// Type inferred automatically
const [count, setCount] = useState(0);        // number
const [name, setName] = useState('Alice');     // string
const [active, setActive] = useState(false);   // boolean

// Explicit type for initial value
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');

// Union type with explicit generic
const [user, setUser] = useState<User | null>(null);

Complex State

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

// Array state
const [todos, setTodos] = useState<Todo[]>([]);

// Object state
interface FormState {
  name: string;
  email: string;
  errors: Record<string, string>;
}

const [form, setForm] = useState<FormState>({
  name: '',
  email: '',
  errors: {}
});

// Functional update
setForm(prev => ({ ...prev, name: 'Bob' }));

Reducer Pattern

import { useReducer } from 'react';

interface State {
  count: number;
  step: number;
}

type Action = 
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setStep'; payload: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

useEffect and Lifecycle

useEffect and Lifecycle Hooks

Basic useEffect

import { useEffect, useState } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    // Effect with cleanup
    const controller = new AbortController();

    fetchUser(userId, controller.signal)
      .then(setUser)
      .catch(console.error);

    return () => controller.abort();
  }, [userId]); // Dependency array typed automatically

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

Custom Hook with useEffect

function useFetch<T>(url: string): {
  data: T | null;
  loading: boolean;
  error: string | null;
} {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then((json: T) => {
        setData(json);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
const { data: users, loading, error } = useFetch<User[]>('/api/users');

useCallback and useMemo

useCallback and useMemo

useCallback Typing

import { useCallback, useState } from 'react';

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

function TodoList({ todos }: { todos: TodoItem[] }) {
  const [filter, setFilter] = useState<string>('all');

  // Return type inferred as (todo: TodoItem) => boolean
  const filteredTodos = useCallback(
    (todo: TodoItem): boolean => {
      if (filter === 'completed') return todo.completed;
      if (filter === 'active') return !todo.completed;
      return true;
    },
    [filter]
  );

  const handleToggle = useCallback(
    (id: number) => {
      // Use functional update for complex state
      setTodos(prev =>
        prev.map(todo =>
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        )
      );
    },
    []
  );

  return (
    <ul>
      {todos.filter(filteredTodos).map(todo => (
        <li key={todo.id} onClick={() => handleToggle(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

useMemo Typing

function expensiveCalculation(data: number[]): number {
  return data.reduce((acc, val) => acc + val * Math.random(), 0);
}

function Dashboard({ numbers }: { numbers: number[] }) {
  // Return type inferred as number
  const result = useMemo(() => expensiveCalculation(numbers), [numbers]);

  return <div>Result: {result}</div>;
}

useRef and useImperativeHandle

useRef and useImperativeHandle

useRef Typing

import { useRef } from 'react';

function VideoPlayer() {
  // DOM element ref
  const videoRef = useRef<HTMLVideoElement>(null);

  // Mutable ref for values
  const intervalRef = useRef<number | null>(null);

  const play = () => {
    videoRef.current?.play();
  };

  const pause = () => {
    videoRef.current?.pause();
  };

  return (
    <>
      <video ref={videoRef} src="video.mp4" />
      <button onClick={play}>Play</button>
      <button onClick={pause}>Pause</button>
    </>
  );
}

useImperativeHandle

import { useImperativeHandle, useRef } from 'react';

interface InputHandle {
  focus: () => void;
  clear: () => void;
  getValue: () => string;
}

const FancyInput = forwardRef<InputHandle, {}>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => { if (inputRef.current) inputRef.current.value = ''; },
    getValue: () => inputRef.current?.value ?? ''
  }));

  return <input ref={inputRef} type="text" />;
});

Context Typing

interface AuthContextType {
  user: User | null;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}