All skills
asyrafhussin avatar

/typescript-react-patterns

@e60e907

TypeScript best practices for React development. Use when writing typed React components, hooks, events, refs, or generic components. Triggers on tasks involving TypeScript errors, type definitions, props typing, or type-safe React patterns.

Use this Skill: https://skilld.dev/gh/asyrafhussin/agent-skills/typescript-react-patterns

This session only. Nothing lands on disk.

ruleshook-use-reducer.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

useReducer Typing

Impact: CRITICAL (enables exhaustive action handling and type-safe state transitions)

Properly typing the useReducer hook with actions, state, and discriminated unions. Discriminated union action types enable exhaustive switch statements and catch missing cases at compile time.

Incorrect

// ❌ Bad
// Untyped reducer - no type safety
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      return state;
  }
};

// Action type as string - no autocomplete or validation
interface Action {
  type: string;
  payload?: any;
}

// Missing exhaustiveness checking
function badReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'add':
      return { ...state, items: [...state.items, action.payload] };
    // Forgot to handle 'remove' action - no error!
  }
  return state;
}

Problems:

  • Untyped reducers have implicit any for state and action
  • String-typed action types provide no autocomplete or validation
  • Without exhaustiveness checking, missing action cases are silently ignored
  • any payload types allow invalid data to pass through

Correct

// ✅ Good
import { useReducer, Reducer } from 'react';

// Define state interface
interface CounterState {
  count: number;
  step: number;
}

// Define action types using discriminated union
type CounterAction =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setStep'; payload: number }
  | { type: 'incrementBy'; payload: number };

const initialState: CounterState = {
  count: 0,
  step: 1,
};

// Typed reducer with exhaustiveness checking
function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'reset':
      return initialState;
    case 'setStep':
      return { ...state, step: action.payload };
    case 'incrementBy':
      return { ...state, count: state.count + action.payload };
    default:
      // Exhaustiveness check - will error if a case is missing
      const _exhaustive: never = action;
      return state;
  }
}

// Usage in component
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'incrementBy', payload: 10 })}>+10</button>
    </div>
  );
}

// Complex state with nested objects
interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
  isLoading: boolean;
  error: string | null;
}

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

type TodoAction =
  | { type: 'ADD_TODO'; payload: { text: string } }
  | { type: 'TOGGLE_TODO'; payload: { id: string } }
  | { type: 'DELETE_TODO'; payload: { id: string } }
  | { type: 'SET_FILTER'; payload: TodoState['filter'] }
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: Todo[] }
  | { type: 'FETCH_ERROR'; payload: string };

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: crypto.randomUUID(),
            text: action.payload.text,
            completed: false,
          },
        ],
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload.id),
      };

    case 'SET_FILTER':
      return { ...state, filter: action.payload };

    case 'FETCH_START':
      return { ...state, isLoading: true, error: null };

    case 'FETCH_SUCCESS':
      return { ...state, isLoading: false, todos: action.payload };

    case 'FETCH_ERROR':
      return { ...state, isLoading: false, error: action.payload };

    default:
      const _exhaustive: never = action;
      return state;
  }
}

// Lazy initialization with typed init function
interface FormState {
  values: Record<string, string>;
  touched: Record<string, boolean>;
  errors: Record<string, string>;
}

type FormAction =
  | { type: 'SET_VALUE'; field: string; value: string }
  | { type: 'SET_TOUCHED'; field: string }
  | { type: 'SET_ERROR'; field: string; error: string }
  | { type: 'RESET' };

function createInitialState(fields: string[]): FormState {
  return {
    values: Object.fromEntries(fields.map((f) => [f, ''])),
    touched: Object.fromEntries(fields.map((f) => [f, false])),
    errors: {},
  };
}

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SET_VALUE':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
      };
    case 'SET_TOUCHED':
      return {
        ...state,
        touched: { ...state.touched, [action.field]: true },
      };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.field]: action.error },
      };
    case 'RESET':
      return createInitialState(Object.keys(state.values));
    default:
      const _exhaustive: never = action;
      return state;
  }
}

// Usage with lazy init
function Form({ fields }: { fields: string[] }) {
  const [state, dispatch] = useReducer(formReducer, fields, createInitialState);

  // dispatch is fully typed
  dispatch({ type: 'SET_VALUE', field: 'email', value: 'test@example.com' });

  return <form>{/* form fields */}</form>;
}

// Action creators for better DX
const todoActions = {
  addTodo: (text: string): TodoAction => ({ type: 'ADD_TODO', payload: { text } }),
  toggleTodo: (id: string): TodoAction => ({ type: 'TOGGLE_TODO', payload: { id } }),
  deleteTodo: (id: string): TodoAction => ({ type: 'DELETE_TODO', payload: { id } }),
  setFilter: (filter: TodoState['filter']): TodoAction => ({ type: 'SET_FILTER', payload: filter }),
};

// Usage with action creators
dispatch(todoActions.addTodo('Buy groceries'));
dispatch(todoActions.toggleTodo('123'));

Benefits:

  • Discriminated unions enable exhaustive switch statements
  • Compile-time errors when action cases are missing
  • Each action has its own strongly-typed payload
  • Type system helps ensure proper immutable state updates
  • Lazy initialization third argument properly types the init function
  • Action creator factory functions provide better developer experience

Reference: React TypeScript Cheatsheet

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill contains educational reference material and code documentation for TypeScript React development patterns. No security risks or malicious vectors were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    30 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at e60e907. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated 7 months ago
metadata
{
  "author": "agent-skills",
  "version": "2.0.0"
}

README badge

README badge for asyrafhussin/agent-skills/typescript-react-patterns