React + TypeScript

SE II Ref
Theme
↑
useState
// Inferred
const [count, setCount] = useState(0);

// Explicit generic
const [user, setUser] = useState<User | null>(null);

// Object state
const [form, setForm] = useState<FormState>({
  name: '', email: ''
});
// Merge partial updates
setForm(prev => ({ ...prev, name: 'Eric' }));
useEffect
// On mount
useEffect(() => { fetchData(); }, []);

// On dep change
useEffect(() => {
  fetchUser(userId);
}, [userId]);

// Cleanup
useEffect(() => {
  const sub = subscribe();
  return () => sub.unsubscribe();
}, [dep]);
âš  Async inside: define an inner async fn, don't make the callback async.
useRef
// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
return <input ref={inputRef} />;
inputRef.current?.focus();

// Mutable value (no re-render)
const timerRef = useRef<ReturnType<typeof setTimeout>>();

// Store prev value
const prev = useRef<number>();
useEffect(() => { prev.current = value; });
useReducer
type Action =
  | { type: 'inc' }
  | { type: 'set'; payload: number };

function reducer(state: number, action: Action) {
  switch (action.type) {
    case 'inc': return state + 1;
    case 'set': return action.payload;
    default: return state;
  }
}
const [n, dispatch] = useReducer(reducer, 0);
useContext
interface ThemeCtx { dark: boolean; }

const ThemeContext = createContext<ThemeCtx>(
  { dark: false }
);

// Provider
<ThemeContext.Provider value={{ dark }}>
  {children}
</ThemeContext.Provider>

// Consumer
const { dark } = useContext(ThemeContext);
Custom Hook Pattern
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetch(url)
      .then(r => r.json() as T)
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}
Functional Component
// Preferred: explicit return type
interface Props {
  name: string;
  count?: number;       // optional
  onClick: () => void;
  children?: React.ReactNode;
}

function MyComp({ name, count = 0, onClick }: Props): JSX.Element {
  return <div onClick={onClick}>{name}: {count}</div>;
}

// React.FC (adds children implicitly in older TS)
const MyComp: React.FC<Props> = ({ name }) => <div>{name}</div>;
Generic Component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
forwardRef + displayName
const Input = React.forwardRef<
  HTMLInputElement,
  React.InputHTMLAttributes<HTMLInputElement>
>((props, ref) => (
  <input {...props} ref={ref} />
));

Input.displayName = 'Input';

// useImperativeHandle
React.useImperativeHandle(ref, () => ({
  focus: () => inputRef.current?.focus()
}));
Children Patterns
// Typed children
children: React.ReactNode   // most permissive
children: React.ReactElement // single element
children: JSX.Element        // single JSX
children: string             // text only

// Render prop
interface P {
  render: (val: string) => React.ReactNode;
}
const x = ({ render }: P) => render('hello');

// Children as function
children: (val: number) => React.ReactNode;
Component Composition
// Extend HTML element props
interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'ghost';
}

function Button({ variant = 'primary', ...rest }: ButtonProps) {
  return <button className={variant} {...rest} />;
}

// Polymorphic 'as' prop (simplified)
type AsProp<C extends React.ElementType> = {
  as?: C;
};
Error Boundary
class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    return this.state.hasError
      ? <h2>Error!</h2> : this.props.children;
  }
}
Event Handler Types
EventHandler Type
onClickReact.MouseEvent<HTMLButtonElement>
onChange (input)React.ChangeEvent<HTMLInputElement>
onChange (select)React.ChangeEvent<HTMLSelectElement>
onSubmitReact.FormEvent<HTMLFormElement>
onKeyDownReact.KeyboardEvent<HTMLInputElement>
onFocus/onBlurReact.FocusEvent<HTMLInputElement>
onDragReact.DragEvent<HTMLDivElement>
handler propReact.MouseEventHandler<HTMLButtonElement>
Common Patterns
// Input change
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

// Form submit + prevent default
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  doSomething();
};

// Keyboard shortcut
const handleKey = (e: React.KeyboardEvent) => {
  if (e.key === 'Enter' && e.metaKey) submit();
};

// Get value from event target
const val = (e.target as HTMLInputElement).value;
Synthetic vs Native
// React wraps native events
// Access native: e.nativeEvent
const onClick = (e: React.MouseEvent) => {
  e.stopPropagation();  // synthetic
  e.preventDefault();  // synthetic
  e.nativeEvent;         // underlying DOM event
  e.currentTarget;       // always the bound element
  e.target;              // element that fired
};
💡 React 17+: events attach to root, not document. stopPropagation() stops React tree, not DOM tree.
Inline vs Handler
// Inline (avoid complex logic)
<button onClick={() => setOpen(true)}>Open</button>

// Curried handler (passes extra args)
const handleClick = (id: string) =>
  (e: React.MouseEvent) => { ... };

<button onClick={handleClick(item.id)}>...</button>

// Typed prop handler
interface P {
  onSelect: React.MouseEventHandler<HTMLLIElement>;
}
Compound Components
const TabContext = createContext<{ active: string }>({ active: '' });

function Tabs({ children, defaultTab }: TabsProps) {
  const [active, setActive] = useState(defaultTab);
  return <TabContext.Provider value={{ active }}>{children}</...>;
}

Tabs.Tab = function Tab({ id, children }: ...) {
  const { active } = useContext(TabContext);
  return <div aria-selected={active === id}>{children}</div>;
};

// Usage: <Tabs><Tabs.Tab id="a">A</Tabs.Tab></Tabs>
HOC Pattern
function withAuth<P extends object>(
  Component: React.ComponentType<P>
) {
  return function WithAuth(props: P) {
    const { isAuthed } = useAuth();
    if (!isAuthed) return <Redirect to="/login" />;
    return <Component {...props} />;
  };
}

const ProtectedPage = withAuth(Dashboard);
// ProtectedPage.displayName = 'withAuth(Dashboard)'
Controlled vs Uncontrolled
// Controlled (React owns value)
const [val, setVal] = useState('');
<input value={val} onChange={e => setVal(e.target.value)} />

// Uncontrolled (DOM owns value)
const ref = useRef<HTMLInputElement>(null);
<input defaultValue="initial" ref={ref} />;
ref.current?.value; // read on demand

// Controlled component pattern
interface P { value: string; onChange(v: string): void; }
Lifting State Up
// Parent owns shared state
function Parent() {
  const [shared, setShared] = useState('');
  return (
    <>
      <ChildA value={shared} onChange={setShared} />
      <ChildB value={shared} />
    </>
  );
}

// Both children stay in sync
// ChildA writes, ChildB reads
Portal + Suspense
// Portal (render outside DOM hierarchy)
import { createPortal } from 'react-dom';
createPortal(<Modal />, document.getElementById('modal-root')!);

// Suspense + lazy
const Dashboard = React.lazy(() => import('./Dashboard'));

<Suspense fallback={<Spinner />}>
  <Dashboard />
</Suspense>
State Machines (enum)
type Status = 'idle' | 'loading' | 'success' | 'error';

const [status, setStatus] = useState<Status>('idle');

// Discriminated union state
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: Data }
  | { status: 'error'; error: Error };
// Narrows automatically in if/switch
TypeScript Utility Types
TypeEffectExample
Partial<T>All props optionalPartial<User>
Required<T>All props requiredRequired<Config>
Readonly<T>ImmutableReadonly<State>
Pick<T, K>Keep keys KPick<User, 'id'|'name'>
Omit<T, K>Remove keys KOmit<User, 'password'>
Record<K,V>Map typeRecord<string, number>
ReturnType<T>Fn return typeReturnType<typeof fn>
Parameters<T>Fn param tupleParameters<typeof fn>
NonNullable<T>Remove null/undefNonNullable<string|null>
Extract<T,U>IntersectionExtract<A|B, B|C>
Exclude<T,U>DifferenceExclude<A|B, B>
React-Specific Types
TypeUse for
React.ReactNodeAny renderable (string, elem, null…)
React.ReactElementJSX element specifically
React.FC<P>Function component (adds children)
React.ComponentType<P>FC or class, useful for HOCs
React.ElementTypeString or component ('div', Button)
React.CSSPropertiesstyle={{ }} props
React.Dispatch<A>useReducer dispatch
React.SetStateAction<T>useState setter arg
React.Ref<T>ref prop
React.MutableRefObject<T>useRef (non-null)
Advanced Patterns
// Discriminated union (narrows w/ switch)
type Result<T> = { ok: true; data: T } | { ok: false; error: string };

// Template literal types
type EventName = `on${Capitalize<string>}`;

// Conditional types
type IsArray<T> = T extends any[] ? true : false;

// Mapped types
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Infer keyword
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
React.memo
// Skip re-render if props unchanged
const ExpensiveChild = React.memo(
  function Child({ data }: { data: Data }) {
    return <div>{data.value}</div>;
  }
);

// Custom comparator
React.memo(Child, (prev, next) =>
  prev.id === next.id
);

// âš  Only helps if parent re-renders often
// âš  Inline objects/fns bypass memo!
useCallback
// Stable fn reference across renders
const handleClick = useCallback(
  (id: string) => { deleteItem(id); },
  [deleteItem] // deps
);

// Pair with React.memo to prevent child re-render
const fetchPage = useCallback(
  async (page: number) => {
    const data = await api(page);
    setItems(data);
  },
  [] // api is stable
);
💡 useMemo memoizes a value; useCallback memoizes a function.
useMemo
// Memoize expensive computation
const sorted = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// Stable object reference (stops child re-render)
const config = useMemo(
  () => ({ timeout: 3000, retries }),
  [retries]
);

// ⚠ Don't overuse — has its own cost
// Use when: expensive calc OR stable ref needed
Reconciliation & Keys
// Key tells React element identity
items.map(item => <Row key={item.id} />); // ✅
items.map((item, i) => <Row key={i} />);  // âš  avoid

// Force re-mount with key change
<Form key={userId} /> // new key = fresh mount

// Fragment shorthand
<>...</>  // no DOM node
<React.Fragment key={id}>...</React.Fragment> // keyed
Batching & Transitions
// React 18: automatic batching everywhere
setTimeout(() => {
  setA(1); setB(2); // single re-render in React 18
}, 0);

// Opt-out of batching
import { flushSync } from 'react-dom';
flushSync(() => setState(x));

// useTransition: mark low-priority update
const [isPending, startTransition] = useTransition();
startTransition(() => setFilter(val)); // deferrable
Common Pitfalls
PitfallFix
Stale closure in effectAdd dep or use ref
Infinite effect loopCheck deps, stable refs
New obj/arr each renderuseMemo or move outside
Missing cleanupReturn fn from useEffect
State mutationAlways return new object/array
Index as keyUse stable ID
Over-render with contextSplit contexts or memoize