Skip to main content

🧱 Part 2 — TypeScript Fundamentals for React (Props, State, Events, Refs & Unions) ✨

 Welcome! In this part we go slow and explain every concept you’ll touch daily in React + TypeScript. Every snippet has inline comments so you can copy, run, and learn.


0) How TypeScript helps in React

  • Types = contracts between pieces of code (components, hooks, functions).
  • TS is structural: if it looks like the type (has the right shape), it’s acceptable.
  • Prefer inference (let TS figure it out) and add explicit types when needed (nulls, unions, generics).

Tip: Keep your editor’s TypeScript server running; errors are your friend during learning.


1) Typing Props (required, optional, default values, unions)

Props are just an object type that your component accepts.

// src/components/Greeting.tsx
// "type" and "interface" are equivalent for most component props. Pick one and be consistent.
type GreetingProps = {
  name: string;          // required prop
  age?: number;          // optional prop (may be undefined)
  excited?: boolean;     // optional flag
};

export function Greeting({ name, age = 0, excited = false }: GreetingProps) {
  // age and excited now have defaults; their type is "number" and "boolean" respectively.
  return (
    <p>
      Hello, <strong>{name}</strong>
      {age ? ` (Age: ${age})` : ''}
      {excited ? ' 🎉' : ''}
    </p>
  );
}

Usage (intentional errors shown as comments):

<Greeting name="Tushar" />            // ✅ age/excited are optional
<Greeting name="Tushar" age={25} />    // ✅
// <Greeting />                        // ❌ Error: name is required
// <Greeting name={123} />             // ❌ Error: name must be string

Union props (e.g., a badge variant):

// src/components/Badge.tsx
type BadgeProps = {
  text: string;
  variant: 'info' | 'success' | 'error'; // union ensures only these literals are allowed
};

export function Badge({ text, variant }: BadgeProps) {
  const color = variant === 'info' ? '#2563eb' // blue
    : variant === 'success' ? '#16a34a'       // green
    : '#dc2626';                              // red (error)
  return <span style={{ padding: '2px 8px', background: color, color: '#fff' }}>{text}</span>;
}

2) Typing State (inference, nullable state, functional updates)

useState usually infers the type from the initial value. Add explicit types for nullable values or unions.

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0); // inferred as number

  // Functional update keeps correctness when the new state depends on previous state.
  const inc = () => setCount(prev => prev + 1);
  const dec = () => setCount(prev => prev - 1);

  return (
    <div>
      <button onClick={dec}>-</button>
      <span style={{ margin: '0 8px' }}>{count}</span>
      <button onClick={inc}>+</button>
    </div>
  );
}

Nullable state pattern:

// Example: user starts as null before login/Fetch

type User = { id: string; name: string };

export function Welcome() {
  const [user, setUser] = useState<User | null>(null); // explicit union type

  return <div>{user ? `Welcome, ${user.name}` : 'Welcome, guest'}</div>;
}

Avoid storing derived values in state:

// Bad: storing fullName when it can be computed
// Good:
const fullName = `${firstName} ${lastName}`; // derive during render instead of setState

3) Typing Events (mouse, change, submit, keyboard)

React wraps browser events with its own types under React.*Event.

Click (mouse) event:

function ClickMe() {
  // e is a React.MouseEvent for a button element
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    // currentTarget is always the element the handler is bound to
    console.log('clicked', e.currentTarget.name);
  };
  return <button name="primary" onClick={handleClick}>Click Me</button>;
}

Change event (input):

import { useState } from 'react';

function NameInput() {
  const [name, setName] = useState('');
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value); // target is the input element
  };
  return <input placeholder="Your name" value={name} onChange={handleChange} />;
}

Submit event (form):

function LoginForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // prevent page reload
    // read values via refs/state or FormData
  };
  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" />
      <button type="submit">Login</button>
    </form>
  );
}

Keyboard event:

function HandleEnter() {
  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      console.log('Enter pressed');
    }
  };
  return <input onKeyDown={onKeyDown} placeholder="Press Enter" />;
}

Common pitfall: Mixing native Event with React’s synthetic events. Use React.ChangeEvent, React.MouseEvent, etc., in React code.


4) Typing Children (what can be rendered)

Use ReactNode to accept anything React can render.

import type { ReactNode } from 'react';

type CardProps = { children: ReactNode; title?: string };

export function Card({ children, title }: CardProps) {
  return (
    <section style={{ border: '1px solid #e5e7eb', padding: 16, borderRadius: 8 }}>
      {title && <h3 style={{ marginTop: 0 }}>{title}</h3>}
      {children}
    </section>
  );
}

If you frequently accept children, you can also use PropsWithChildren:

import type { PropsWithChildren } from 'react';

type PanelProps = PropsWithChildren<{ kind?: 'info' | 'warning' }>; // merges {children: ReactNode}

export function Panel({ kind = 'info', children }: PanelProps) {
  return <div data-kind={kind}>{children}</div>;
}

5) Typing Refs (DOM elements & values)

Refs let you access DOM nodes or persist values across renders.

DOM ref:

import { useRef } from 'react';

export function FocusInput() {
  // The ref can be null initially (before the input mounts)
  const inputRef = useRef<HTMLInputElement>(null);

  const focus = () => {
    // Optional chaining guards against null during first render
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Click the button to focus me" />
      <button onClick={focus}>Focus</button>
    </div>
  );
}

Value ref (not tied to DOM):

import { useRef } from 'react';

function Stopwatch() {
  // A mutable value that persists without causing re-renders
  const startTimeRef = useRef<number | null>(null);
  // ...assign startTimeRef.current = Date.now() when starting
  return null;
}

Forwarding refs to a child component:

import { forwardRef } from 'react';

type TextInputProps = { label: string };

export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
  ({ label }, ref) => {
    return (
      <label>
        {label}
        <input ref={ref} />
      </label>
    );
  },
);

6) Narrowing with Discriminated Unions (exhaustive checks)

Model UI states with a tagged union and make impossible states impossible.

// One property ("kind") distinguishes each state shape

type LoadState =
  | { kind: 'idle' }
  | { kind: 'loading' }
  | { kind: 'success'; data: string[] }
  | { kind: 'error'; message: string };

export function DataStatus({ state }: { state: LoadState }) {
  switch (state.kind) {
    case 'idle':
      return <p>Idle. Click fetch to start.</p>;
    case 'loading':
      return <p>Loading…</p>;
    case 'success':
      return (
        <ul>
          {state.data.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      );
    case 'error':
      return <p style={{ color: 'crimson' }}>Error: {state.message}</p>;
    default: {
      // If a new case is added to LoadState but not handled here,
      // TypeScript will error below (exhaustiveness check):
      const _never: never = state; // ❌ if a case is missing
      return _never;
    }
  }
}

Pattern: add a default branch with a never variable to catch unhandled cases during compilation.


7) Handy TS tips you’ll use daily

  • Literal unions from arrays:
    const statuses = ['idle', 'loading', 'success', 'error'] as const;
    type Status = typeof statuses[number]; // 'idle' | 'loading' | 'success' | 'error'
    
  • satisfies to validate object shapes without widening:
    const config = {
      env: 'dev',
      retry: 3,
    } as const satisfies { env: 'dev' | 'prod'; retry: number };
    
  • Avoid any: it disables type safety. Prefer precise unions or generics.
  • Avoid React.FC for components (it forces children and messes with generics). Type props yourself.

8) Practice (mini challenges)

  1. Create a Toggle component with a checked: boolean prop and an onChange: (next: boolean) => void prop. Type the event handler and add keyboard support (Space toggles).
  2. Build a Select component typed as HTMLSelectElement for onChange. Show the selected value.
  3. Extend LoadState with a "empty" case and make sure the switch stays exhaustive.

✅ Wrap-Up

You learned how to type props, state, events, children, refs, and how to model UI using discriminated unions with exhaustive checks. These patterns remove entire classes of bugs and make refactors safe.

Next: We’ll build reusable UI components (Button, Input, Modal) and introduce gentle generics to keep components flexible and type-safe. 🚀


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...