DEV Community

Cover image for Mastering JavaScript for React (TypeScript Edition) in 30 Practical Examples
Tihomir Ivanov
Tihomir Ivanov

Posted on

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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 <div> 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>;
};
Enter fullscreen mode Exit fullscreen mode

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>;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

In BaseButton, we destructure label and then collect the remaining props in ...rest. The <button {...rest}> spreads those remaining props onto the real <button> element. This way, BaseButton can accept any standard button props (like onClick, disabled, etc.) and pass them through automatically. The spread operator makes this pattern straightforward.

6. Rest Parameters and Props

Rest syntax is the sibling of spread. While spread expands iterable/objects, rest collects multiple elements into an array or object. In functions, rest parameters gather all remaining arguments into an array. In object destructuring, rest can gather remaining properties into a new object.

Why it matters in React: Rest parameters can make your functions more flexible by handling an arbitrary number of arguments. In React components, rest props (as shown above) allow your component to take "extra" props without explicitly listing them, which is great for wrapper components.

Example 1 – Rest parameters in a function: Suppose we want a utility to log a message with varying values:

function logValues(message: string, ...values: number[]) {
  console.log(message, values.join(', '));
}

logValues("Scores:", 10, 20, 30);
// console output: "Scores: 10, 20, 30"
Enter fullscreen mode Exit fullscreen mode

The ...values in the function definition captures any number of arguments after message into an array named values. This is standard JavaScript, but it's useful in React if you write functions that need to handle multiple items (though in React specifically, you often pass arrays instead).

Example 2 – Rest in object destructuring (props): In the BaseButton example above, we used { label, ...rest } = props. That syntax collected all prop fields except label into the rest object. This is how you implement components that accept "any other props" to forward to a child.

The rest operator thus helps in writing flexible components and functions by capturing "the rest" of the data easily.

7. Default Parameters and Default Props

JavaScript allows function parameters to have default values. If an argument isn’t provided (or is undefined), the default kicks in. In React, you might want default values for props. With functional components, you can use default parameter syntax or default values in destructuring. For class components, you can use default props or similar patterns.

Why it matters in React: Setting default prop values helps your component have sensible behavior even if a parent doesn't pass all props. Using JS default parameters (or defaultProps) prevents undefined errors and makes the component more robust.

Example – Functional component default prop:

interface GreetProps { name?: string; }
const Greet: React.FC<GreetProps> = ({ name = "Guest" }) => (
  <h1>Welcome, {name}!</h1>
);
Enter fullscreen mode Exit fullscreen mode

Here, if name is not provided by the parent, it defaults to "Guest". We achieved this by using name = "Guest" in the function's parameter list. This is the recommended way to handle default props in modern React/TypeScript, and it keeps the default close to where the prop is used.

Example – Class component default prop:

interface GreetProps { name?: string; }
class GreetClass extends React.Component<GreetProps> {
  static defaultProps = { name: "Guest" };
  render() {
    const { name } = this.props;
    return <h1>Welcome, {name}!</h1>;
  }
}
Enter fullscreen mode Exit fullscreen mode

In a class, one way is using the defaultProps static property. Here we declare defaultProps on the class so that if name isn’t provided, it defaults to "Guest". TypeScript understands this pattern, but note that the React community is moving away from defaultProps on function components (it may be deprecated) in favor of default parameters.

Using default parameters or defaultProps ensures your component doesn't receive undefined for missing props, making your code safer and more predictable.

8. The this Keyword in React Components

In JavaScript, this refers to the context in which a function is called. In React class components, this typically refers to the component instance, which holds props, state, and methods. Functional components, on the other hand, don’t use this at all.

Why it matters in React: If you're writing class components, understanding this is crucial to access this.props or this.state. Losing track of this (for example, in an unbound callback) can cause errors. In functional components, you avoid this altogether, which is one reason many prefer them.

Example: Comparing this usage in class vs functional:

// Class Component using `this`
class Welcome extends React.Component<{ name: string }> {
  render() {
    return <p>Hello, {this.props.name} (from class)!</p>;
  }
}

// Functional Component (no `this`)
const WelcomeFunc: React.FC<{ name: string }> = ({ name }) => {
  return <p>Hello, {name} (from function)!</p>;
};
Enter fullscreen mode Exit fullscreen mode

In Welcome (class), we access the name via this.props.name. In WelcomeFunc, we just use the name variable (no this needed).

If you try to use this inside a functional component, it won’t be what you expect – there is no component instance. All data for functional components comes through props or hooks, not this.

Understanding this difference helps avoid mistakes like trying to do this.setState in a functional component or forgetting to bind methods in classes (which leads to this being undefined in the method).

9. Binding this vs Arrow Functions in Classes

When using class components, event handler methods that you pass to JSX need the correct this context. By default, in JavaScript class methods are not bound to the class instance. There are two common solutions: manually binding them in the constructor, or using arrow functions (as class properties) which automatically use the class this.

Why it matters in React: If you forget to bind a class handler, this will be undefined when the method is called (because it’s called as a callback without context). This is a classic source of bugs in React classes. Arrow function class properties save you from this by auto-binding.

Example – Manual bind vs arrow property:

class Clicker extends React.Component<{}, { clicks: number }> {
  state = { clicks: 0 };

  // Method that is not bound by default
  handleClickBad() {
    this.setState({ clicks: this.state.clicks + 1 });
  }

  // Arrow function method (auto-bound)
  handleClickGood = () => {
    this.setState(prev => ({ clicks: prev.clicks + 1 }));
  };

  constructor(props: {}) {
    super(props);
    // manual binding of handleClickBad
    this.handleClickBad = this.handleClickBad.bind(this);
  }

  render() {
    return (
      <>
        <button onClick={this.handleClickBad}>Bad Click (manual bind)</button>
        <button onClick={this.handleClickGood}>Good Click (arrow)</button>
        <p>Total clicks: {this.state.clicks}</p>
      </>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In this class, handleClickBad is a normal method. We bind it in the constructor: this.handleClickBad.bind(this). If we didn't, clicking the "Bad Click" button would throw an error because this would be undefined inside handleClickBad.

By contrast, handleClickGood is defined as an arrow function property. Arrow functions don't have their own this, they use the surrounding this (which is the component instance). So it's effectively bound automatically. No constructor code needed. Clicking "Good Click" works without additional binding.

Today, using arrow function class properties is the common pattern to avoid manual binding and keep code cleaner.

10. Using Array.map to Render Lists

The Array.prototype.map method takes an array and transforms it into a new array by applying a function to each element. In React, map is indispensable for rendering lists of elements or components from arrays of data.

Why it matters in React: If you have an array of items (say, a list of users or products), you can .map over it to return an array of JSX elements. This is the idiomatic way to generate repeated elements dynamically. Each item should have a unique key prop when rendered in a list to help React optimize updates.

Example: Rendering a list of strings as list items:

const fruits = ['Apple', 'Banana', 'Cherry'];

const FruitList: React.FC = () => {
  return (
    <ul>
      {fruits.map(fruit => (
        <li key={fruit}>{fruit}</li>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here, fruits.map(...) takes each fruit name and returns an <li> element. The result is an array of <li> elements which React renders inside the <ul>. We use key={fruit} in this simple case (each fruit name is unique). In real life, you might use a unique ID from the data as the key.

You can also map an array of objects to an array of components:

interface User { id: number; name: string; }
const users: User[] = [ {id: 1, name: 'Alice'}, {id: 2, name: 'Bob'} ];

<div>
  {users.map(user => (
    <div key={user.id}>Hello, {user.name}</div>
  ))}
</div>
Enter fullscreen mode Exit fullscreen mode

This will produce two <div> greetings. The map method is a powerful tool to translate data into UI elements in React.

11. Using Array.filter for Conditional Lists

The Array.prototype.filter method creates a new array with only the elements that pass a given condition. In React, filter is useful when you want to display a subset of data based on some condition (perhaps a search query or a toggle).

Why it matters in React: Sometimes you want to render only items that meet certain criteria. By filtering your data array before mapping to JSX, you can conditionally include or exclude elements in the render.

Example: Filtering a list of products to show only those on sale:

interface Product { id: number; name: string; onSale: boolean; }
const products: Product[] = [
  { id: 1, name: "Phone", onSale: true },
  { id: 2, name: "Laptop", onSale: false },
  { id: 3, name: "Tablet", onSale: true }
];

const SaleProducts: React.FC = () => {
  const saleItems = products.filter(p => p.onSale);
  return (
    <ul>
      {saleItems.map(item => (
        <li key={item.id}>{item.name} (On Sale!)</li>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

First we use filter to get only the products where onSale is true. Then we map those to <li> elements. The result is that only "Phone" and "Tablet" from the array will be rendered (each with the "(On Sale!)" note).

This pattern can be combined inline too: {products.filter(...).map(...)} in JSX is valid, but splitting into a variable like saleItems can improve readability.

12. Using Array.reduce for Derived Data

The Array.prototype.reduce method boils an array down to a single value (or a new array/object) by cumulatively combining elements through a reducer function. In React, you might use reduce to calculate a summary or aggregate to display, or to transform an array into a different structure needed for rendering.

Why it matters in React: If you need to derive a value from a list – such as computing the total price from an array of items, or grouping items by category for separate renders – reduce can do it elegantly without writing manual loops.

Example: Calculating and displaying the sum of an array of numbers:

const numbers = [10, 20, 5];

const Total: React.FC = () => {
  const total = numbers.reduce((sum, num) => sum + num, 0);
  return <p>Total: {total}</p>;
};
Enter fullscreen mode Exit fullscreen mode

Here, reduce takes an initial sum of 0 and adds each number in the array to it, resulting in the total (35). The component then displays the total.

Another example – transforming an array to an object map (though this is a bit more advanced use):

const people = [
  { id: 1, name: "Alice", city: "NY" },
  { id: 2, name: "Bob", city: "SF" }
];
const peopleById = people.reduce((acc, person) => {
  acc[person.id] = person;
  return acc;
}, {} as Record<number, {id: number; name: string; city: string}>);
Enter fullscreen mode Exit fullscreen mode

Here we reduced an array of people into an object where you can lookup by id. In a React context, you might do this kind of transformation outside the JSX, then use the resulting object for quick access to people by ID.

In summary, use reduce when you need to compute a single result (number, object, etc.) from a list – for example, summing scores, merging arrays, or preparing data for rendering.

13. Ternary Operator for Conditional Rendering

The ternary operator (condition ? expr1 : expr2) is a concise way to choose one of two values based on a boolean condition. In JSX, you can use it to conditionally render one element or another inline.

Why it matters in React: It’s common to show either one UI element or a different one based on state or props. A ternary in JSX allows this without an if/else block (which can’t directly be used inside JSX).

Example: Displaying a login button if the user is not logged in, or a greeting if they are:

interface HeaderProps { user: string | null; }
const Header: React.FC<HeaderProps> = ({ user }) => {
  return (
    <header>
      {user ? <p>Welcome, {user}!</p> : <button>Login</button>}
    </header>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here we use { user ? <p>Welcome...> : <button>Login</button> }. 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 && <SomeJSX /> will either render <SomeJSX /> 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

This pattern is great for optional sub-components, like modal dialogs (isOpen && <Modal />) 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

In the first <p>, profile?.name will safely yield undefined if profile is null (so nothing is rendered after "Name:"). In the second <p>, 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>;
};
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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)}>Increment</button>
      <button onClick={showAlert}>Show Alert in 3s</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

(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}ms</div>}</>;
};
Enter fullscreen mode Exit fullscreen mode

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] }));
Enter fullscreen mode Exit fullscreen mode

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 }));
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We declared GreetingProps requiring a name: string and an optional age: number. In the functional component, we annotate React.FC<GreetingProps> (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<GreetingProps> to accomplish the same.

Now if someone uses <Greeting /> 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<Props> or as a generic to React.Component<Props, State> 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<Props, State>. 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>;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

In this functional example, we explicitly provided the generic <number> 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[]>([]);
Enter fullscreen mode Exit fullscreen mode

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>}
/>
Enter fullscreen mode Exit fullscreen mode

The List component above is generic (function List<T>). It takes items of type T[] and a renderItem function that knows how to render an item: T. When we use <List> 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);
Enter fullscreen mode Exit fullscreen mode

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")} />
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

Now size can only be "small", "medium", or "large". If someone passes <Button size="huge" />, TS will catch it.

Union and literal types thus help create flexible yet type-safe component interfaces in React.

27. Typed Event Handlers in React

In TypeScript, DOM events in React have specific types. For example, a form input change event is React.ChangeEvent<HTMLInputElement>, a click event on a button is React.MouseEvent<HTMLButtonElement>, and so on. Using these types for event handler parameters ensures you access the right properties without errors.

Why it matters: By typing your event handlers, you get autocomplete for event properties (like target and its value) and you ensure you're handling the correct event type (for instance, differentiating an onChange of an <input> vs an onSubmit of a <form>). It also communicates to other developers what kind of event to expect.

Example – Typing an onChange handler for an input:

const NameInput: React.FC = () => {
  const [name, setName] = React.useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };

  return <input value={name} onChange={handleChange} />;
};
Enter fullscreen mode Exit fullscreen mode

Here, handleChange is explicitly typed as a function that takes a React.ChangeEvent<HTMLInputElement>. That tells us e.target is an HTML input element, so it has a value property (which is a string for text inputs). Inside, we do e.target.value confidently. If we mistyped something (say e.tARGET.value or accessed a nonexistent property), TS would error.

For a button click:

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault();
  // ... 
};
<button onClick={handleClick}>Click me</button>
Enter fullscreen mode Exit fullscreen mode

A submit event for a form would be React.FormEvent<HTMLFormElement>.

You often don't need to annotate these by hand because TS can infer the type from the JSX attribute (if you define handler inline). But if you separate the handler (like in our example), adding the type is a good practice. It documents what element it expects and allows you to use the event object with full type support.

In summary, use the React.*Event types from @types/react for the specific element you are dealing with. Common ones include ChangeEvent, MouseEvent, KeyboardEvent, FormEvent, etc., each parameterized by the HTML element type.

shows an example of defining an onChange handler with the appropriate ChangeEvent type for an input.

28. Using Refs in React with TypeScript

Refs provide a way to access DOM nodes or React elements directly. In TypeScript, you should specify what type of element a ref refers to. This avoids the need for casting and ensures you access valid properties on that ref.

Why it matters: If you want to focus an input or measure a div’s size, you'll use refs. Typing them makes sure, for example, that your ref to an <input> is not mistakenly used on a <canvas> or something. It also helps TS know that ref.current might be HTMLInputElement | null, for instance.

Example – Using useRef for a DOM element:

const FocusableInput: React.FC = () => {
  const inputRef = React.useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

We initialize useRef<HTMLInputElement>(null) meaning this ref will eventually point to an HTMLInputElement (or be null initially). Later, we can call inputRef.current?.focus(). TypeScript knows current is an HTMLInputElement | null, so it permits .focus() (which exists on HTMLInputElement) and we use ?. in case it's null. If we tried to call a method that isn't on HTMLInputElement, TS would error.

For class components, you'd use React.createRef<HTMLDivElement>() similarly. Example:

class Scroller extends React.Component {
  private listRef = React.createRef<HTMLUListElement>();

  scrollToBottom = () => {
    const ul = this.listRef.current;
    ul?.lastElementChild?.scrollIntoView();
  };

  render() {
    return (
      <div>
        <ul ref={this.listRef}>{/* ...list items... */}</ul>
        <button onClick={this.scrollToBottom}>Scroll to bottom</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Typing refs is especially useful when dealing with non-HTML things, like a ref to another React component (you'd use React.RefObject<YourComponent>), or when using useRef to hold a mutable value (then you type it as that value type).

But for common DOM refs, providing the DOM type (HTMLInputElement, HTMLDivElement, etc.) gives you safe access to its properties and methods.

29. Functional State Updates with Previous State

When updating state based on the previous state, it's best to use the functional update form of setState (in classes, pass a function to this.setState; in hooks, pass a function to setState). This ensures you always get the latest state value, especially when state updates might be batched or an update is triggered in quick succession.

Why it matters: If you rely on state variables directly when setting new state, you might close over a stale value (similar to the closure topic earlier). The functional update form provides the current state as an argument, guaranteeing you use the most up-to-date value. It's crucial in cases where you do multiple updates in one go or in event handlers that might run before state is updated.

Example – incrementing state multiple times:

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

  const incrementTwice = () => {
    // WRONG: this would only increment once because `count` is stale for second call
    // setCount(count + 1);
    // setCount(count + 1);

    // RIGHT: use functional updates to ensure each uses latest result
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={incrementTwice}>Increment twice</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

If we incorrectly did setCount(count + 1); setCount(count + 1);, the second call would still see the old count (because state doesn't update until after the function execution). The result would be count increased by 1 instead of 2. By using prev => prev + 1 each time, the first call sets it to old+1, the second call uses that updated value (prev) and adds 1 again. The end result is count+2 as expected.

In class components, a similar pattern:

this.setState(prevState => ({ volume: prevState.volume + 1 }));
Enter fullscreen mode Exit fullscreen mode

If you call that twice (in the same tick), each gets the updated prevState.

React will batch updates, but apply each functional updater in order with the latest state. This pattern avoids the bug of stale state usage.

Use functional updates whenever your new state depends on the old state. Common cases: counters (as above), toggling booleans (setFlag(prev => !prev)), pushing to arrays (setList(prev => [...prev, newItem])), etc. This ensures correctness even if multiple updates happen or other state updates intervene.

30. ES Module Imports/Exports in React Projects

Modern React code uses ES modules for organizing code into files. Understanding how to export components or utilities and import them elsewhere is fundamental. There are two kinds of exports: named exports and default exports.

Why it matters: In a React project, you'll split components into separate files and use imports/exports to use them. Knowing the syntax helps avoid confusion (for example, when to use curly braces in imports). Also, consistent usage of default vs named exports can improve clarity.

Example – Default export vs named export:

// File: components/MyButton.tsx
import React from 'react';

interface MyButtonProps { label: string; }
const MyButton: React.FC<MyButtonProps> = ({ label }) => (
  <button>{label}</button>
);

export default MyButton;
Enter fullscreen mode Exit fullscreen mode

Here, MyButton is exported as the default from its file. This means when we import it elsewhere, we can choose any name:

// File: App.tsx
import MyButton from './components/MyButton'; // default import, name matches exported component for clarity
// ...
<MyButton label="Click me" />
Enter fullscreen mode Exit fullscreen mode

If we had a named export instead:

// File: utils/math.ts
export function add(a: number, b: number) { return a + b; }
export const PI = 3.14;
Enter fullscreen mode Exit fullscreen mode

We exported two named members. To import them:

import { add, PI } from './utils/math';
console.log(add(2,2), PI);
Enter fullscreen mode Exit fullscreen mode

Notice we use { } and the names must match (unless we do aliasing like import { add as sum }). With default exports, no curly braces and we choose the name (though typically use the original name for sanity).

In summary, default export: one per file, import without braces. Named exports: many per file, import with braces. You can mix, but it's often a style choice to either have a default (often for the main component in a file) or only named exports. React components are frequently default-exported (one component per file), but utility modules often have multiple named exports.

Understanding modules ensures you can organize your React code and use components across files. The ES module system has been standard in React apps since the move to build tools like webpack/CRA, and knowing it is essential for any modern JS development.

summarizes: a module can have multiple named exports, but only one default export.


By mastering these 30 JavaScript and TypeScript techniques in the context of React, you should feel more confident in writing clean, efficient, and error-free React code. Each concept, from basic syntax like arrow functions and destructuring to more advanced TypeScript patterns, plays a role in everyday React development. Keep this guide as a quick reference, and happy coding as you build awesome React applications in 2025 and beyond!

Top comments (0)