Mastering JavaScript for React (TypeScript Edition) in 30 Practical Examples

Moving from JavaScript to React can feel confusing, especially when familiar JS features start behaving in nuanced ways inside React components. Fear not! This tutorial is written in a conversational, developer-friendly tone to guide you through 30 practical examples that will help you master the JavaScript (and TypeScript) techniques most often used in React development. We’ll cover everything from modern ES6+ syntax to asynchronous patterns and TypeScript typings, using real-life React examples (with both functional hooks and class components where relevant) so you can quickly apply these concepts with confidence. Let’s dive in and make your transition to React in 2025 a smooth and empowering journey! 1. Arrow Functions for Concise Callbacks Arrow functions provide a concise syntax for writing functions and automatically bind the lexical this context (they don't have their own this context). In React, arrow functions are commonly used for inline event handlers and to avoid explicitly binding methods in class components. This leads to cleaner code and fewer pitfalls with the this keyword. Why it matters in React: Arrow functions let us write event handlers directly in JSX or define class methods without worrying about losing the component context. This means no more this.someMethod = this.someMethod.bind(this) in constructors if you use arrow syntax. Example (Functional & Class): In a functional component, you can define an event handler inline. In a class component, using an arrow function as a method ensures this refers to the class instance. // Functional Component: Arrow function in an onClick handler const CounterButton: React.FC = () => { const [count, setCount] = React.useState(0); return ( setCount(prev => prev + 1)}> Increment Count (current: {count}) ); }; // Class Component: Arrow function method to auto-bind `this` class CounterButtonClass extends React.Component { state = { count: 0 }; // Define handleClick as an arrow property to bind `this` handleClick = () => { this.setState(prev => ({ count: prev.count + 1 })); }; render() { return ( Increment Count (current: {this.state.count}) ); } } In the functional version, the inline arrow function updates state without any extra ceremony. In the class version, handleClick is an arrow function property, so it inherits the class this context automatically. No explicit binding is required. 2. Template Literals for Dynamic Strings Template literals (using ` backticks) allow you to embed expressions into strings easily. This is handy in React for constructing class names, inline styles, or any dynamic text in your JSX. Why it matters in React: You often need to build strings based on props or state (for example, CSS class names or IDs). Template literals make this more readable than string concatenation. Example: Using a template literal to set a dynamic CSS class on a based on a prop value: interface AlertProps { type: 'success' | 'error'; message: string; } const Alert: React.FC = ({ type, message }) => { const className = `alert ${type === 'error' ? 'alert-error' : 'alert-success'}`; return {message}; }; Here we use a template literal `alert ${...}` to include either "alert-error" or "alert-success" depending on the type prop. This is much cleaner than doing a string concat like "alert " + (condition ? ...). Template literals can also span multiple lines and include any JS expression, which comes in handy for formatting text in JSX without ugly concatenations. 3. Object Destructuring (Props and State) Object destructuring lets you extract properties from an object into variables in a single, concise statement. In React, this is commonly used to pull values from props or state for easier use. Why it matters in React: Destructuring props and state makes your component code cleaner by avoiding repetitive this.props.x or props.x references. It also clearly documents which props or state fields are used. Example: Destructuring props in both functional and class components: interface UserBadgeProps { name: string; age: number; } // Functional Component: destructure in the parameter list const UserBadge: React.FC = ({ name, age }) => ( {name} is {age} years old. ); // Class Component: destructure inside render class UserBadgeClass extends React.Component { render() { const { name, age } = this.props; return {name} is {age} years old.; } } In the functional version, we destructure name and age directly in the function signature. In the class version, we destructure inside the render() method. Either way, we can use name and age directly, which is shorter and clearer than writing this.props.name or similar each time. This technique also works for state (e.g., const { something } = this.state in a class, or extracting multiple values from a useState object). 4. Array Destructuring (e.g. Hook

May 12, 2025 - 09:29
 0
Mastering JavaScript for React (TypeScript Edition) in 30 Practical Examples

Moving from JavaScript to React can feel confusing, especially when familiar JS features start behaving in nuanced ways inside React components. Fear not! This tutorial is written in a conversational, developer-friendly tone to guide you through 30 practical examples that will help you master the JavaScript (and TypeScript) techniques most often used in React development. We’ll cover everything from modern ES6+ syntax to asynchronous patterns and TypeScript typings, using real-life React examples (with both functional hooks and class components where relevant) so you can quickly apply these concepts with confidence. Let’s dive in and make your transition to React in 2025 a smooth and empowering journey!

1. Arrow Functions for Concise Callbacks

Arrow functions provide a concise syntax for writing functions and automatically bind the lexical this context (they don't have their own this context). In React, arrow functions are commonly used for inline event handlers and to avoid explicitly binding methods in class components. This leads to cleaner code and fewer pitfalls with the this keyword.

Why it matters in React: Arrow functions let us write event handlers directly in JSX or define class methods without worrying about losing the component context. This means no more this.someMethod = this.someMethod.bind(this) in constructors if you use arrow syntax.

Example (Functional & Class): In a functional component, you can define an event handler inline. In a class component, using an arrow function as a method ensures this refers to the class instance.

// Functional Component: Arrow function in an onClick handler
const CounterButton: React.FC = () => {
  const [count, setCount] = React.useState(0);
  return (
    <button onClick={() => setCount(prev => prev + 1)}>
      Increment Count (current: {count})
    button>
  );
};

// Class Component: Arrow function method to auto-bind `this`
class CounterButtonClass extends React.Component<{}, { count: number }> {
  state = { count: 0 };
  // Define handleClick as an arrow property to bind `this`
  handleClick = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
  };
  render() {
    return (
      <button onClick={this.handleClick}>
        Increment Count (current: {this.state.count})
      button>
    );
  }
}

In the functional version, the inline arrow function updates state without any extra ceremony. In the class version, handleClick is an arrow function property, so it inherits the class this context automatically. No explicit binding is required.

2. Template Literals for Dynamic Strings

Template literals (using ` backticks) allow you to embed expressions into strings easily. This is handy in React for constructing class names, inline styles, or any dynamic text in your JSX.

Why it matters in React: You often need to build strings based on props or state (for example, CSS class names or IDs). Template literals make this more readable than string concatenation.

Example: Using a template literal to set a dynamic CSS class on a

based on a prop value:
interface AlertProps { type: 'success' | 'error'; message: string; }

const Alert: React.FC<AlertProps> = ({ type, message }) => {
  const className = `alert ${type === 'error' ? 'alert-error' : 'alert-success'}`;
  return <div className={className}>{message}div>;
};

Here we use a template literal `alert ${...}` to include either "alert-error" or "alert-success" depending on the type prop. This is much cleaner than doing a string concat like "alert " + (condition ? ...).

Template literals can also span multiple lines and include any JS expression, which comes in handy for formatting text in JSX without ugly concatenations.

3. Object Destructuring (Props and State)

Object destructuring lets you extract properties from an object into variables in a single, concise statement. In React, this is commonly used to pull values from props or state for easier use.

Why it matters in React: Destructuring props and state makes your component code cleaner by avoiding repetitive this.props.x or props.x references. It also clearly documents which props or state fields are used.

Example: Destructuring props in both functional and class components:

interface UserBadgeProps { name: string; age: number; }

// Functional Component: destructure in the parameter list
const UserBadge: React.FC<UserBadgeProps> = ({ name, age }) => (
  <p>{name} is {age} years old.p>
);

// Class Component: destructure inside render
class UserBadgeClass extends React.Component<UserBadgeProps> {
  render() {
    const { name, age } = this.props;
    return <p>{name} is {age} years old.p>;
  }
}

In the functional version, we destructure name and age directly in the function signature. In the class version, we destructure inside the render() method. Either way, we can use name and age directly, which is shorter and clearer than writing this.props.name or similar each time.

This technique also works for state (e.g., const { something } = this.state in a class, or extracting multiple values from a useState object).

4. Array Destructuring (e.g. Hook Results)

Array destructuring is similar to object destructuring, but for array elements. A very common use case in React is with React Hooks like useState or useReducer, which return arrays.

Why it matters in React: Hooks often return a tuple (array) of values. Destructuring lets you name those values clearly. For example, useState returns [state, setState]. By destructuring, you assign meaningful names in one step.

Example: Using array destructuring with the React Hook useState:

const [count, setCount] = React.useState<number>(0);
<button onClick={() => setCount(count + 1)}>Increment: {count}button>;

When calling useState(0), we get back an array [value, updater]. The destructuring const [count, setCount] = ... assigns the first element to count and the second to setCount. This pattern is used for all hooks that return arrays (like [state, dispatch] = useReducer(...), or [value, refSetter] = useState() itself).

Another example: if you had a function that returns an array, you could destructure it similarly. In React, you'll mainly see this with Hooks.

5. Spread Operator for Props and State Updates

The spread operator (...) allows you to expand (or spread) iterable elements or object properties. In React, it's widely used for copying state objects/arrays (to avoid mutations) and for passing groups of props to a component.

Why it matters in React: Immutability is key in React state updates – you often create new objects/arrays from old ones. Spread syntax makes it easy to copy an array or object with updated values. Also, when you have a set of props to forward to a child, the spread operator helps pass them through.

Example 1 – Copying and updating state: Suppose you have an array in state and want to add an item:

const [items, setItems] = React.useState<string[]>(['a', 'b']);
const addItem = (item: string) => {
  setItems(prevItems => [...prevItems, item]); // create new array with old items + new item
};

Here [...] creates a new array containing all of prevItems plus the new item at the end. We use the spread operator to avoid mutating the original array.

Example 2 – Spreading props: You can forward props to a child component:

interface BaseButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
}
const BaseButton: React.FC<BaseButtonProps> = ({ label, ...rest }) => (
  <button {...rest}>{label}button>
);

// Usage:
<BaseButton label="Click" onClick={() => console.log('clicked')} disabled={isLoading} />

In BaseButton, we destructure label and then collect the remaining props in ...rest. The }. If user is truthy (a non-null username), it shows the welcome message. Otherwise, it shows a "Login" button. This one-liner inside JSX is very readable for simple either/or conditions.

Without a ternary, you'd have to prepare a variable before the return or use logical && which only handles the "show or nothing" case. Ternary shines when you have two distinct elements to toggle between.

Make sure the expressions on both sides of : produce JSX (or one could be null to render nothing). This keeps your render logic declarative and succinct.

14. Short-Circuit Logical && for Conditional Rendering

Short-circuit evaluation using the logical AND (&&) operator is a common pattern in React to conditionally render something or nothing. In JavaScript, expr1 && expr2 returns expr2 if expr1 is truthy, otherwise it returns expr1 (which would be falsy). We leverage that in JSX: condition && will either render or nothing (if condition is false).

Why it matters in React: This provides a neat way to say "if this condition is true, then show this part of the UI; if not, show nothing." It avoids an else case entirely.

Example: Only render a loading spinner if a loading flag is true:

interface LoaderProps { loading: boolean; }
const Loader: React.FC<LoaderProps> = ({ loading }) => {
  return (
    <div>
      {loading && <span className="spinner">Loading...span>}
    div>
  );
};

If loading is true, the with "Loading..." will be rendered. If loading is false, the expression {loading && ...} will short-circuit and result in false, which React treats as nothing to render (it won’t render a boolean). So the disappears entirely.

This pattern is great for optional sub-components, like modal dialogs (isOpen && ) or sections of a page that should only show under certain conditions.

One caveat: if the left side can sometimes be a number or string 0 or "", be careful because those are falsy and could unintentionally short-circuit. For purely boolean flags or truthy checks, && is perfect. (For values where 0 is meaningful, you might use a ternary or check explicitly for != null.)

15. Optional Chaining (?.) for Safe Property Access

Optional chaining (?.) is a JavaScript operator that lets you safely access nested object properties even if an intermediate value might be null or undefined. Instead of throwing an error when encountering a null, it short-circuits and returns undefined for the whole expression.

Why it matters in React: You often receive props or state that could be missing certain nested data (especially when dealing with APIs). Optional chaining allows you to guard against accessing properties of undefined. In JSX, this prevents runtime errors and can simplify conditional rendering of deeply nested values.

Example: Accessing a deeply nested prop safely:

interface Profile {
  name: string;
  location?: { city: string; country: string };
}
const UserInfo: React.FC<{ profile: Profile | null }> = ({ profile }) => {
  return (
    <div>
      <p>Name: {profile?.name}p>
      <p>City: {profile?.location?.city ?? "Unknown"}p>
    div>
  );
};

In the first

, profile?.name will safely yield undefined if profile is null (so nothing is rendered after "Name:"). In the second

, profile?.location?.city drills into location and then city only if each part is defined. We also used ?? "Unknown" (nullish coalescing, see next section) to display "Unknown" if city isn’t available.

Without optional chaining, we’d need to do a lot of checks: profile && profile.location && profile.location.city. The ?. operator streamlines this. It’s extremely useful for avoiding errors like "Cannot read property 'city' of undefined" in your React app when data might not be fully present yet (e.g., before an API call resolves).

16. Nullish Coalescing (??) for Default Values

Nullish coalescing (??) is an operator that returns the right-hand operand when the left-hand operand is null or undefined (nullish), otherwise it returns the left-hand operand. It's like a safer default value operator than || because it won't mistakenly default on falsy non-nullish values like 0 or "".

Why it matters in React: When rendering, you might want to show a default text or value if a prop/state is missing (null/undefined). ?? lets you do that without treating valid falsy values as missing. This is particularly useful in cases where 0 is a valid value that you don't want to override.

Example: Providing a default username if none is provided:

interface HelloProps { username?: string | null; }
const HelloUser: React.FC<HelloProps> = ({ username }) => {
  return <p>Hello, {username ?? "Guest"}!p>;
};

If username is null or undefined, the expression username ?? "Guest" evaluates to "Guest". If username is any other value (including an empty string or 0, if that were possible here), it would use that value. By contrast, if we had used username || "Guest", an empty string "" would incorrectly trigger the default to "Guest". With ??, only nullish values trigger the fallback.

Another example: value = props.value ?? 0; ensures value is a number – if props.value is null/undefined, you get 0, but if props.value were 0 explicitly, you keep 0 (instead of defaulting).

In summary, use ?? when you want to provide a default for missing data but want to allow falsy legitimate values. It makes your component output more predictable for edge cases.

17. Promises for Asynchronous Tasks

JavaScript promises are used to handle asynchronous operations. In a React app, you frequently deal with promises when fetching data or performing any async task (like interacting with an API). Understanding promises lets you coordinate these operations and update your component once the promise settles.

Why it matters in React: React by itself doesn’t change how promises work, but you often use promises within effects or event handlers. Knowing how to chain .then() and handle results or errors is key to fetching data and updating state accordingly.

Example: Fetching data with fetch (which returns a promise) in a React component:

interface User { name: string; }
const UserLoader: React.FC = () => {
  const [user, setUser] = React.useState<User | null>(null);

  React.useEffect(() => {
    // simulate data fetch
    fetch('/api/user/123')
      .then(response => response.json())
      .then(data => {
        setUser(data);
      })
      .catch(error => {
        console.error("Failed to load user", error);
      });
  }, []); // empty dependency array -> run once on mount

  if (!user) return <p>Loading...p>;
  return <p>Hello, {user.name}p>;
};

In this example, we call fetch, which returns a promise for a Response. We use .then to wait for the response and parse JSON, then another .then to get the data and update state with setUser(data). We also attach a .catch to handle any errors.

We placed this in a useEffect so it runs when the component mounts. Until the promise resolves, user is null and we show a loading message. Once the promise fulfills and we call setUser, React re-renders and displays the user name.

Promises are often used with the newer async/await syntax (coming next), but it's important to understand the underlying .then/.catch as well, since you may see both styles in codebases.

18. Async/Await for Cleaner Async Code

Async/await is syntactic sugar over promises that allows you to write asynchronous code in a synchronous style. By marking a function async, you can use the await keyword inside it to pause execution until a promise resolves, instead of using .then chains.

Why it matters in React: Async/await can make your data fetching or other async operations easier to read and maintain. In React, you'd typically use async/await inside useEffect or event handlers for clarity.

Example: The previous fetch example rewritten with async/await:

const UserLoaderAsync: React.FC = () => {
  const [user, setUser] = React.useState<User | null>(null);

  React.useEffect(() => {
    const loadUser = async () => {
      try {
        const response = await fetch('/api/user/123');
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error("Failed to load user", error);
      }
    };
    loadUser();
  }, []);

  if (!user) return <p>Loading...p>;
  return <p>Hello, {user.name}p>;
};

We define an inner async function loadUser and call it, because useEffect itself cannot directly take an async function (it would return a promise which React would treat as a cleanup function). Inside loadUser, we await the fetch and then await the .json() parsing. This linear style is often easier to follow than nested .then calls. We wrap it in a try/catch to handle errors (equivalent to .catch).

Async/await makes the flow of asynchronous logic more intuitive. Just remember that any function using await must be marked async, and that async functions return a promise. In event handlers (like an onClick), you can directly mark the handler as async and use await inside it, which is very convenient for sequences of actions (for example, submitting a form then showing a success message after a save completes).

19. Closures in React (Stale State and Variables)

A closure is when a function "remembers" the variables from the place where it was defined, even if it's executed later, potentially in a different scope. In React, closures come into play especially with state and hooks: a function defined in a component render can capture state variables, leading to what’s called “stale state” if not handled correctly.

Why it matters in React: If you use functions inside useEffect or setTimeout, or rely on state values inside callbacks, you may unintentionally use an outdated value of a variable due to how closures work. Understanding closures helps you avoid bugs like a stale state value in an asynchronous callback.

Example – Stale closure with a timeout:

const DelayedAlert: React.FC = () => {
  const [count, setCount] = React.useState(0);

  const showAlert = () => {
    setTimeout(() => {
      alert("Count is " + count);
    }, 3000);
  };

  return (
    <>
      <p>{count}p>
      <button onClick={() => setCount(count + 1)}>Incrementbutton>
      <button onClick={showAlert}>Show Alert in 3sbutton>
    
  );
};

Try this: click "Increment" a few times, then click "Show Alert". The alert will appear 3 seconds later. You might expect it to show the latest count, but often it will show an older value. Why? Because the function passed to setTimeout closed over the value of count at the time showAlert was called. Even if count changes later, that inner function doesn't know – it has its own preserved copy from that render.

This is a closure issue. To fix it, you might use the functional state update or always read latest value via a ref. For example, using a functional update in setTimeout:

setTimeout(() => {
  alert("Count is " + document.getElementById('countVal')?.textContent);
}, 3000);

(not a recommended pattern to access DOM, but illustrates retrieving fresh info at call time), or better, use a ref to always have the latest count.

The key is: any function defined in a render will hold onto the variables from that render. If those variables change later, the old function doesn't magically update them. React's hooks like useEffect dependencies and useCallback exist to manage when closures should refresh. Being aware of closures helps you use these tools correctly.

In summary, closures are powerful (they allow inner functions to access outer scope), but in React they mean you should be mindful that a callback might not “see” updated state unless you account for it.

20. Timers (setTimeout) and useEffect Cleanup

Using setTimeout or setInterval in React requires care to avoid memory leaks or unwanted behavior. If a component sets a timer, that timer might still fire even after the component is unmounted, unless you clear it. React’s useEffect cleanup function is the place to clear timers.

Why it matters in React: Timers are a common JavaScript tool to delay or repeat actions. In a React component, if you start a timer, you should clear it if the component unmounts (or if the effect re-runs and you want to reset the timer). Otherwise, you might attempt to update state on an unmounted component or just waste resources.

Example: Setting up a timer to auto-hide a message:

const AutoHideMessage: React.FC<{ duration: number }> = ({ duration }) => {
  const [visible, setVisible] = React.useState(true);

  React.useEffect(() => {
    const timerId = setTimeout(() => setVisible(false), duration);
    return () => {
      clearTimeout(timerId); // cleanup if component unmounts before timeout
    };
  }, [duration]);

  return <>{visible && <div>This message will disappear after {duration}msdiv>};
};

When this component mounts or when duration changes, we set a timeout to update state after duration milliseconds. The effect returns a cleanup function that clears the timeout. If the component unmounts early, the timeout is cleared and setVisible won't be called on an unmounted component.

If we didn't clear the timeout, and the component was removed (unmounted) quickly, the timeout would still fire and attempt setVisible(false). In development, React might warn "Can't perform a React state update on an unmounted component". Clearing timers prevents that.

The same principle applies to setInterval (clear it with clearInterval) or other subscriptions (like WebSocket events or event listeners – always clean up in useEffect). By handling cleanup, your component remains well-behaved and doesn't introduce memory leaks or console warnings.

21. Immutability: Avoiding Direct State Mutation

In React, state should be treated as immutable – you do not want to modify state variables (objects or arrays) in place. Instead, always make a copy and then set state to the new copy. This is because React relies on detecting changes (usually via reference equality) to know when to re-render. If you mutate state directly, you might not trigger a re-render and could cause bugs.

Why it matters in React: Direct mutations (e.g., this.state.foo.push(...) or modifying an object property in state) can lead to React not updating the UI, since setState/useState was never called or React thinks nothing changed. Also, state mutations can make debugging harder. Keeping state updates immutable ensures predictable UI updates and enables potential performance optimizations.

Example – Incorrect vs correct state update (class):

// Incorrect: mutating state directly
this.state.items.push(newItem);
this.setState({ items: this.state.items }); // This mutation can cause issues

// Correct: create a new array
this.setState(prev => ({ items: [...prev.items, newItem] }));

In the incorrect example, we directly push into this.state.items. While we do call setState after, we passed the same array object (now modified) to setState. React might still re-render in this case, but it's not a good practice because if you ever skip calling setState, the mutation would be lost or cause inconsistencies. The correct example uses a new array via spread (...prev.items) so we're sure we have a fresh object.

Example – Functional component with object state:

const [user, setUser] = React.useState({ name: "Alice", points: 0 });

// Incorrect: directly mutate the object
user.points = 100; 
setUser(user); // React may not re-render because object reference is same

// Correct: copy the object
setUser(prev => ({ ...prev, points: 100 }));

By copying prev with {...prev} and then changing points, we create a new object. React sees the state value has changed (new reference) and triggers a re-render.

The rule of thumb: treat state as read-only. If you need to change it, produce a new value (object/array) and use that in your state setter. This ensures you don't accidentally override data or miss updates.

22. Class vs Functional Components (Differences)

React gives two ways to create components: class components and functional components. Understanding their differences is key as you transition to modern React (which favors functional components with Hooks).

Why it matters: Class components use an older API (with render() method, lifecycle methods like componentDidMount, and this.state/this.setState). Functional components are simpler JavaScript functions that use hooks (like useState, useEffect) for state and lifecycle. Knowing both helps in reading older code and writing new code effectively.

Example – Similar component in class and functional form:

// Class Component
interface CounterProps { start?: number; }
class CounterClass extends React.Component<CounterProps, { count: number }> {
  state = { count: this.props.start ?? 0 };
  componentDidMount() {
    console.log("CounterClass mounted");
  }
  increment = () => this.setState(prev => ({ count: prev.count + 1 }));
  render() {
    return <button onClick={this.increment}>{this.state.count}button>;
  }
}

// Functional Component
const CounterFunc: React.FC<CounterProps> = ({ start = 0 }) => {
  const [count, setCount] = React.useState(start);
  React.useEffect(() => {
    console.log("CounterFunc mounted");
  }, []);  // run once on mount
  return <button onClick={() => setCount(c => c + 1)}>{count}button>;
};

Both components do the same thing: start at a given count (default 0) and increment on button click, logging when mounted. The class uses this.state and this.setState, plus the lifecycle method componentDidMount. The functional uses useState and useEffect. The functional version is more concise and avoids this altogether.

Key differences:

  • State: Class uses an initial state property and this.setState (which merges state partially), functional uses useState (you replace state completely or handle merging manually for objects).
  • Lifecycle: Class has methods like componentDidMount, componentDidUpdate, componentWillUnmount. Functional uses useEffect for all these (the effect's dependencies determine when it runs).
  • this: Class components require dealing with this (and binding if passing methods around). Functional components have no this — they close over variables directly.
  • Hooks: Only functional components can use Hooks (state, context, etc. via Hooks API). Classes cannot use Hooks; they use older patterns like render props or HOCs for similar functionality.

In 2025, most new code is written with functional components and hooks, but class components still work and exist in many codebases. It's useful to recognize both patterns.

23. Typing Component Props with TypeScript

TypeScript allows us to define the shape of props a component should receive. This makes our components safer and self-documenting. You can use interface or type aliases to describe props, and then apply that to your React component.

Why it matters: With proper prop types, TypeScript will catch when a parent component passes wrong or missing props. It also provides IntelliSense for consumers of your component. This reduces bugs and misunderstandings about what props are expected.

Example – Functional and Class component prop typing:

// Define an interface for props
interface GreetingProps { name: string; age?: number; }

// Functional Component with typed props
const Greeting: React.FC<GreetingProps> = ({ name, age }) => (
  <div>
    <p>Hello, {name}!p>
    {age !== undefined && <p>You are {age} years old.p>}
  div>
);

// Class Component with typed props
class GreetingClass extends React.Component<GreetingProps> {
  render() {
    const { name, age } = this.props;
    return (
      <div>
        <p>Hello, {name}!p>
        {age !== undefined && <p>You are {age} years old.p>}
      div>
    );
  }
}

We declared GreetingProps requiring a name: string and an optional age: number. In the functional component, we annotate React.FC (FC stands for Function Component) which tells TypeScript this component should be called with name and optionally age. In the class component, we extend React.Component to accomplish the same.

Now if someone uses without a name, or with a name that's not a string, TypeScript will error. Similarly, it will autocomplete prop names and types for you.

TypeScript also supports using type aliases instead of interfaces for props (e.g. type GreetingProps = { ... }), which works similarly. The key part is attaching that type to the component (either via React.FC or as a generic to React.Component for classes).

24. Typing State in Class Components (and useState)

Just like props, you can type the component’s state. For class components, state typing is done by providing a second generic argument to React.Component. For functional components, you don't explicitly type "state", but you type the value passed to useState or let TypeScript infer it from the initial value.

Why it matters: Typing state ensures that when you call this.setState or use state values, you respect the shape of the state. It prevents you from accidentally storing the wrong data type in state or accessing a non-existent state property.

Example – Class component state typing:

interface CounterState { count: number; }
class Counter extends React.Component<{}, CounterState> {
  state: CounterState = { count: 0 };
  increment = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
  };
  render() {
    return <button onClick={this.increment}>{this.state.count}button>;
  }
}

Here we declared an interface CounterState and told the class that its state conforms to that. We initialized state accordingly. Now this.state.count is known to be a number, and this.setState will only allow updates that match the state shape (e.g., if we tried this.setState({ count: "hello" }), TypeScript would error out because "hello" is not a number).

Example – useState typing (functional):

const [count, setCount] = React.useState<number>(0);
setCount(prev => prev + 1);

In this functional example, we explicitly provided the generic to useState to indicate the state type. This is often optional because TS can infer from the initial value 0 that it's a number. But if your initial state is ambiguous (like null or an empty array), you might supply the type.

For instance:

// state is an array of strings, initial empty
const [items, setItems] = React.useState<string[]>([]);

Typing state in functional components is mostly about the useState generic or initial value. Typing class state is done via the class generics. Both approaches ensure you treat state as the right type throughout the component.

25. Generics in Components and Hooks

Generics allow components or functions to be type-parametrized. This is useful for creating reusable components or hooks that work with different data types. You might not use generics in every component, but they shine for things like list or form components that can handle various types.

Why it matters: With generics, you can write one component that is flexible with types while still getting full type safety. For example, a list component can accept an array of any type T and a render function for T, and the compiler will enforce consistency.

Example – Generic List component:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>): JSX.Element {
  return <ul>{items.map((item, idx) => <li key={idx}>{renderItem(item)}li>)}ul>;
}

// Usage:
<List 
  items={['Alice', 'Bob', 'Charlie']} 
  renderItem={(name) => <span>{name.toUpperCase()}span>}
/>

The List component above is generic (function List). It takes items of type T[] and a renderItem function that knows how to render an item: T. When we use with an array of strings, TypeScript infers T as string. Inside the renderItem, name is correctly typed as string, so toUpperCase() is allowed. If we tried to use renderItem={(name) => name * 2} TS would error, because name is a string, not a number.

You could similarly create a generic hook. For example, a hook to filter an array:

function useFilter<T>(items: T[], predicate: (item: T) => boolean): T[] {
  return React.useMemo(() => items.filter(predicate), [items, predicate]);
}

// Usage:
const evenNumbers = useFilter([1,2,3,4], num => num % 2 === 0);

Here useFilter works for any type T, and TS ensures the predicate receives that same T. In usage, T becomes number because we passed an array of numbers.

Generics add a bit of complexity, but they empower you to write highly reusable and type-safe components and hooks in a React+TS codebase.

26. Union and Literal Types for Flexible Props

Union types allow a variable (or prop) to be one of several types. A common pattern is a discriminated union, where a prop can have multiple shapes distinguished by a literal field. This is useful in React for making a component that has slightly different props in different modes or variants.

Why it matters: Union types let you model variations in props. Instead of making a prop optional and doing runtime checks, you can use union types to have the compiler enforce that "if prop type is X, then these other props must be present". This leads to more robust components and fewer runtime errors.

Example – Discriminated union for an Alert component:

type AlertProps = 
  | { type: 'success'; message: string } 
  | { type: 'error'; error: Error };

const Alert: React.FC<AlertProps> = (props) => {
  if (props.type === 'success') {
    return <div className="alert-success">{props.message}div>;
  } else {
    return <div className="alert-error">{props.error.message}div>;
  }
};

// Usage:
<Alert type="success" message="Operation completed." />
<Alert type="error" error={new Error("Something went wrong")} />

Here AlertProps is a union of two object types. If type is 'success', then a message: string is expected. If type is 'error', an error: Error is expected. Inside the component, we discriminate with if (props.type === 'success'). TypeScript then knows within that block that props is the first variant (with message available), and in the else it knows props is the error variant (with an error object). This is called type narrowing.

If someone using Alert tries to pass inconsistent props (like type="success" but provide an error prop, or missing message), TypeScript will error. This makes the API of Alert very clear and strict.

Even simpler unions are useful, e.g., a prop that can be one of a few string literals:

type Size = 'small' | 'medium' | 'large';
interface ButtonProps { size: Size; label: string; }

Now size can only be "small", "medium", or "large". If someone passes