SyntaxStudy
Sign Up
TypeScript Generic Components, Custom Hooks, and Context
TypeScript Beginner 1 min read

Generic Components, Custom Hooks, and Context

Generic React components let you build reusable components that work with different data types while maintaining type safety. A generic component is a function component whose type parameters flow from the props into the render output. Common examples include typed list components, table components, select inputs, and data grids where the type of each item should be inferred from the items prop. Custom hooks encapsulate stateful logic and return typed values. The return type of a custom hook should be an explicit tuple or object type. Returning as const narrows tuple types so that TypeScript understands the exact positions rather than treating the return as a general array. Custom hooks that accept callbacks should use generics to propagate types through the callback. React context is typed through createContext's type parameter. Providing a default context value can be awkward because the full value is often not available until inside the provider. A common pattern is to provide null as the default and write a typed useContext wrapper that asserts the value is non-null and throws a helpful error if the hook is used outside the provider. This combines type safety with a clear runtime error message.
Example
import { createContext, useContext, useState, type ReactNode } from "react";

// Generic List component
interface ListProps<T> {
    items: T[];
    keyExtractor: (item: T) => string | number;
    renderItem: (item: T) => ReactNode;
    emptyMessage?: string;
}

function List<T>({ items, keyExtractor, renderItem, emptyMessage = "No items" }: ListProps<T>) {
    if (items.length === 0) return <p>{emptyMessage}</p>;
    return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}

// Usage — T is inferred as { id: number; name: string }
// <List items={users} keyExtractor={u => u.id} renderItem={u => <span>{u.name}</span>} />

// Custom hook with typed tuple return
function useLocalStorage<T>(key: string, initialValue: T): [T, (val: T) => void] {
    const [stored, setStored] = useState<T>(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? (JSON.parse(item) as T) : initialValue;
        } catch { return initialValue; }
    });

    const setValue = (value: T) => {
        setStored(value);
        localStorage.setItem(key, JSON.stringify(value));
    };

    return [stored, setValue];
}

// Typed Context with null-asserting hook
interface AuthContextValue {
    user: { id: number; name: string } | null;
    login: (email: string, password: string) => Promise<void>;
    logout: () => void;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function useAuth(): AuthContextValue {
    const ctx = useContext(AuthContext);
    if (!ctx) throw new Error("useAuth must be used within <AuthProvider>");
    return ctx;
}

export function AuthProvider({ children }: { children: ReactNode }) {
    const [user, setUser] = useState<AuthContextValue["user"]>(null);
    const login  = async (email: string, _pw: string) => { setUser({ id: 1, name: email }); };
    const logout = () => setUser(null);
    return <AuthContext.Provider value={{ user, login, logout }}>{children}</AuthContext.Provider>;
}

This is the last lesson in this section.

Create a free account to earn a certificate