TypeScript  

How to Pass Functions & Params in React + TypeScript

The Situation

See if this sounds familiar: you’re passing functions around like candy in your React app, but when TypeScript throws a tantrum, you start hacking with any or begging Google for help. Let’s fix that. You’ll learn how to type function parameters like someone who knows what they’re doing and not someone duct-taping their code.

This is our agenda for today,

  1. Typing basic functions in TypeScript: Return types, parameters, and the bare minimum.
  2. Typing event handlers in React properly: No more any, no guessing. Use React.MouseEvent.
  3. Passing parameters to event handlers (the right way): Fixing the classic onClick={handleClick(5)} mistake.
  4. Combining custom params with event objects: (e) => handleClick(e, id) and how to type it cleanly.
  5. Passing data from parent to child: Props with both object and destructuring styles.
  6. Child components calling parent callbacks: onSelect, lifting state, and typing the callback.
  7. Handling lists with dynamic params and callbacks: Avoiding inline functions that wreck performance.
  8. Passing multiple parameters in callback props: Positional args vs grouping into objects.
  9. Defining reusable function types across files: No more repeating inline types. One type, reuse everywhere.
  10. Using useCallback properly (only when you should): No overkill. Just the real use case.
  11. Passing parameters into custom hooks: How to write hooks like useUser(id) and why simple is better.
  12. Lifting handlers across multiple layers: Flexible design: passing just the data, not hardcoded logic.

1. Before You Write Another Callback

Let’s kick it off with the classic:

function add(a: number, b: number): number{
    return a + b;
}

console.log(add(5,5)); // Output: 10

Yeah, that’s basic. You already know that. But now you’re in React, and suddenly:

export default function functions() {
    return (
        <button onClick={handleClick}> Click me</button>
    );
}

// Error: Cannot find name 'handleClick'.ts

And you’re like, how do I type handleClick?

2. Typing Event Handlers in React

TypeScript is picky with events, and it should be. Don’t guess the type. Use the built-in ones.

export default function SubmitButton() {

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    console.log(e.currentTarget);
  }

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

See, now it works.

 Typing Event Handlers in React

People try to pass data through components or event handlers and end up with spaghetti logic. You need to pass values to functions. You need to do it cleanly. And TypeScript needs to be happy. That’s it.

Let’s walk through how to pass parameters in every common React situation.

3. Passing Parameters to Event Handlers

Rookie Mistake, calling the function immediately during render.

  • handleClick(5) runs right away as React renders the component.
  • It returns undefined (because handleClick doesn't return anything).
function handleClick(id: number) {
    console.log("Clicked id: ", id);
}

<button onClick={handleClick(5)}>Click me</button>

// Error: Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'

// So what you’re effectively doing is:
<button onClick={undefined}>Click me</button>

So, TS is saying, you gave me something that runs immediately (and returns nothing, void), but I was expecting a function to call later when the button is clicked.

Let's fix that by wrapping in an Arrow Function.

  • Now it creates a new function that, when called, will call handleClick(5).
  • So you're giving React a proper function to call later when the button is actually clicked.
  • This creates a new function every render, which is fine unless you’re building an airline dashboard with 10,000 rows.
<button onClick={() => handleClick(5)}>Click me</button>

function handleClick(id: number) {
    console.log("Clicked id: ", id);
}

Arrow Function

4. Event Parameter + Custom Data

<button onClick={(e) => handleClick(e, 5)}>Click me</button>

function handleClick(e: React.MouseEvent, id: number) {
    console.log(e.currentTarget);
    console.log("Clicked id: ", id);
}

You want access to the event and your custom data? Just pass both. TypeScript will figure out the event type if your handler is explicit.

Event Parameter + Custom Data

5. Passing Data to Child Components

You want to pass a parameter (like id) from the parent to the child, and use it in the child (e.g., in a button click).

Parent component

//Parent.tsx

import Child from "./Child";

export default function Parent(){
    return (
        <Child id={1} />
    );
}

Child component

1. Accessing parameters using props: keeps the full object and accesses its properties later

//Child.tsx

type childProps = {
    id: number
}

//With Props
export default function Child(props: childProps){

    function handleClick(){
        console.log("Clicked ID:", props.id);
    }

    return(
        <button onClick={ () => handleClick() }>Click me</button>
    );
}
  • The parent passes a prop: id=1.
  • The child receives that prop and uses it:
    • Inside the click handler.

2. Using Destructuring: Extracts values directly from an object:

//Child.tsx

type childProps = {
    id: number
}

//With Destructuring
export default function Child({id}: childProps){

    function handleClick(){
        console.log("Clicked ID:", id);
    }

    return(
        <button onClick={ () => handleClick() }>Click me</button>
    );
}

Using Destructuring

6. Handling Child Events in the Parent

When building reusable components, it's common for a child component to notify its parent about something that happened, like a button click. This is called “lifting state up” or event delegation.

Child component

  • Child now receives new prop: onSelect: a function provided by the parent to call when the button is clicked.
  • Inside the onClick, we call onSelect(id), which tells the parent which child was clicked.
//Child.tsx

type childProps = {
    id: number
    onSelect: (id: number) => void;
}


export default function Child({id, onSelect}: childProps){

    return(
        <button onClick={ () => onSelect(id)}>Click me</button>
    );
}

Parent component

  • Parent passes a function handleItemClick to Child as the onSelect prop.
  • When the Child button is clicked, it calls onSelect(id), which triggers handleItemClick(id) in the parent.
  • This way, the child triggers an event, and the parent handles it.
//Parent.tsx

import Child from "./Child";

export default function Parent(){

function handleItemClick(itemId: number) {
    console.log("Child clicked:", itemId);
  }

    return (
        <Child id = {1} onSelect={handleItemClick} />
    );
}

7. List Rendering with Dynamic Params (Parent-Child)

When rendering a list of components dynamically, pass relevant data (like id and name) as props:

  • Unique key: Always provide a unique, stable key for each item, typically item.id. Keys help React efficiently update and reorder list elements.
  • onSelect is the same function instance across renders for better performance.
items.map((item) => (
          <Item key={item.id} 
                id={item.id} 
                name={item.name} 
                onSelect={handliClick} /> ))

Full version of Parent component

  • We have a list of items (id + name)
  • Defines a function handliClick(id, name) that logs the selected item
  • Loops through the items array and renders one <Item /> per entry
  • Passes id, name, and the handliClick function to each child
//Parent component

import Item from "./Item"

type Item = {
    id: number;
    name: string;
}

const items: Item[] = [
    { id: 1, name: "Buy Car" },
    { id: 2, name: "Buy House" }
]

export default function Parent() {
    function handliClick(id : number, name : string) {
        console.log(`Selected Id, ${id}, item: ${name}`);
    }

    return (
        <div>
            {
                items.map((item) => (<Item key={item.id} id={item.id} name={item.name} onSelect={handliClick} />))
            }
        </div>
    );
}

Child component

  • Receives id, name, and onSelect as props
  • Renders a <button> that shows the item name
  • When the button is clicked, it calls onSelect(id, name)
//Item.tsx : Child component

interface ItemProps {
    id: number;
    name: string;
    onSelect: (id: number, name: string) => void;
}

export default function Item({id, name, onSelect} : ItemProps) {
    return (
        <button onClick={() => onSelect(id, name)}> {name} </button>
    );
}

Mistakes to avoid when working with a list

Avoid using arrow functions for events while working with a list.

<Item ... 
       onSelect={() => handliClick(item.id)} />

Every time this component renders, a new anonymous function (() => handliClick(item.id)) is created for every item.

This causes

  • Unnecessary re-renders, especially if ItemRow is a memoized component (React.memo()).
  • React sees each handliClick prop as "changed", even if item.id hasn’t.
  • Over time, this leads to performance issues, especially in long lists.

Instead, pass id as a prop and move the logic inside the child, like we did above.

Let's see the output

  • The Parent renders a list of <Item /> buttons.
  • Each Item gets its own data (id and name) and the click handler.
  • When a user clicks a button, the child calls onSelect(id, name).
  • That triggers handliClick() in the parent.
  • The parent logs something like:
    Parent logo

8. Multiple Parameters in Callback Props

As we saw above as well, sometimes your callback needs to accept multiple pieces of information. Here's another example where the callback receives two parameters: an ID and a status.

interface Props {
  onAction: (id: number, status: "Active" | "Inactive") => string;
}

const MultiArgs = ({ onAction }: Props) => {
  return (
    <button onClick={() => onAction(123, "Active")}>
            Action
    </button>
  );
};

export default MultiArgs;

8.1 Grouping Parameters into a Single Object

As your parameters grow, passing multiple values can get messy. Instead, it’s better to group related parameters into a single object with a clear type.

interface ActionData {
    id: number; 
    status: "Active" | "Inactive";
}

interface Props {
    onAction: ( actionArgs : ActionData) => string;
}

const MultiArgs = ({ onAction }: Props) => {
    return (
        <button onClick={() => onAction( { id: 123, status: "Active" } )}>
            Action
        </button>
    );
};

export default MultiArgs;

Now we're able to pass arguments as an object, and this is easy to scale.

9. Define Reusable Function Types for Callback Props

Problem: When passing functions (callbacks) as props to components in TypeScript, it's common to inline the function types like this:

interface Props{
    onSubmit : (data: { id: number, email: string }) => void;
}

This works, but over time:

  • The inline type gets repeated everywhere.
  • It becomes hard to maintain if the shape changes.
  • Reusability suffers across components, tests, mocks, and more

The Solution: Use a Named Function Type:

Instead of repeating the inline shape every time, define a named function type once and reuse it:

1. Define type

type submitHandler = (args: { id: number, email: string }) => void;

2. Then plug it into your props

interface Props{
    onSubmit : submitHandler
}

Full Example

// types.ts
export type SubmitHandler = (data: { id: number; email: string }) => void;


// Child: ReuseFuncTypes .tsx
import type { SubmitHandler } from './types';

interface Props{
    onSubmit : SubmitHandler
}

export default function ReuseFuncTypes({ onSubmit }: Props) {
  const handleClick = () => {
    onSubmit({ id: 1, email: "[email protected]" });
  };

  return <button onClick={handleClick}>Submit</button>;
}

//Parent
import ReuseFuncTypes from "./ReuseFuncTypes"
import type { SubmitHandler } from './types';

export default function Parent() {

    const handleSubmit: SubmitHandler = (data) => {
        console.log("Form submitted:", data);
    };

    return (
        <div>
            {
                <ReuseFuncTypes onSubmit={handleSubmit}></ReuseFuncTypes>
            }
        </div>
    );
}

Output

Output

10. useCallback, When You Actually Need It

const handleSelect = useCallback((id: string) => {
  console.log('Selected:', id);
}, []);

Use it when

  • You’re passing a function to a deeply memoized child.
  • Your function has dependencies and causes re-renders.

11. Parameters in Custom Hooks

function useUser(id: string) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then((res) => res.json())
      .then(setUser);
  }, [id]);

  return user;
}

Pass ID directly. No need to be clever with objects or destructuring unless you're batching.

12. Lifting Handlers Across Components

You don’t need to pass a full handler with baked-in data.

<Parent onAction={handleGlobalAction} />

const Child = ({ id, onAction }: { id: string; onAction: (id: string) => void }) => {
  return <button onClick={() => onAction(id)}>Go</button>;
};

You get flexible components. And your parent doesn’t care how deep the call goes.

Wrap Up

If you're serious about building React apps with TypeScript, you can’t afford to wing function parameters. Whether it's handling clicks, lifting callbacks, or passing data cleanly, it all comes down to writing functions that are clear, typed, and reusable. Stop guessing, stop hacking, and start typing like you mean it.