Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 48

Custom Hooks: Returning Values and Functions

Recommended resources

Hello! Welcome back to your journey into React.

In our last lesson, we took a significant step by learning how to create the basic structure of a custom hook and adhering to the crucial use naming convention. You successfully extracted the logic for a counter into a useCounter hook, which returned the count value to the component.

That was the "how-to-define" part. Today, we'll focus on making these hooks truly powerful and flexible. A hook that can't communicate back to the component isn't very useful. This lesson is all about that communication.

Our learning outcome is to return values and functions from a custom hook for component use. We'll explore the common patterns for structuring this returned data so that your components can use your custom logic in a clean and intuitive way.

The Purpose of a return Statement in a Hook

Just like any JavaScript function, a hook uses a return statement to send data back to whatever called it. In the useCounter exercise from our previous lesson, you returned a single value: the count.

// A simplified version of your last exercise
function useCounter() {
  const [count, setCount] = useState(0);

  // ... useEffect logic ...

  return count; // Returning a single value
}

function MyComponent() {
  const count = useCounter(); // The component receives the value
  return <h1>Count: {count}</h1>;
}

This is the simplest pattern, and it's perfect for hooks that provide just one piece of information, like the useOnlineStatus hook from the official React documentation which simply returns a boolean.

However, most of the time, we need more than just one piece of data. We usually need the state and the function(s) to change that state. This is where things get interesting.


Pattern 1: Returning an Array

The most common pattern you'll see is returning an array, which mirrors the behavior of React's built-in useState hook. This pattern typically returns two elements: the current state value and a function to update it.

The main advantage of this pattern is the ease of renaming variables when you destructure them in your component.

Let's watch a video that demonstrates this by building a useLocalStorage hook. This hook will need to return both the value stored in local storage and the function to update that value.

Custom Hooks in React - Every React Developer Should Know This

This video from ByteGrad expertly explains why you'd return an array from a custom hook. It also compares it to the alternative of returning an object, which we will discuss next.

Watch the segment from 16:07 to 19:07. Pay close attention to how the hook returns [value, setValue] and how the component can then destructure it into [theme, setTheme] or [items, setItems].

As the video explained, the key points for returning an array are:

  • Flexibility in Naming: You can name the destructured variables anything you like. This is why useState is so versatile.
    // The hook returns [value, setValue]
    const [name, setName] = useLocalStorage('name'); 
    const [cartItems, setCartItems] = useLocalStorage('cart');
    
  • Order is Critical: The main downside is that the order matters. If the hook returns [value, setValue] and you destructure it as [setValue, value], you'll introduce bugs that can be hard to track down.

This pattern is so common that you'll see it everywhere. Here is a simple visual recap.

React Custom Hook Usage Example
This image shows a `useToggle` hook in action. It returns an array `[isToggled, toggle]`, which the component receives and uses. This is a perfect example of the array return pattern.

Pattern 2: Returning an Object

The second common pattern is to return a JavaScript object. This approach has its own set of trade-offs.

The primary benefit of returning an object is that the order of properties does not matter, and the code becomes more "self-documenting" because you use the property names defined in the hook.

Let's explore this by looking at a useFetch hook, which needs to manage several pieces of state: the fetched data, a loading status, and any potential errors.

Full React Tutorial #20 - Making a Custom Hook

This tutorial from Net Ninja builds a useFetch hook and chooses to return an object. This is a great example of when an object is a good choice because the hook returns multiple, distinct pieces of information.

First, watch from 04:43 to 05:39 to see why an object is being returned. Then, watch from 06:39 to 08:08 to see how the component destructures this object and even renames a property.

To summarize the object pattern:

  • Order is Irrelevant: You can destructure properties in any order you want.
    // The hook returns { data, isPending, error }
    const { isPending, data } = useFetch('/api/users');
    
  • Explicit Naming: You must use the property names defined in the hook. This can make the code clearer but requires an extra step if you need to rename a variable.
    // Renaming `data` to `blogs` is more verbose
    const { data: blogs, isPending } = useFetch('/api/blogs');
    

The official React documentation also provides an excellent example with a useFormInput hook that returns an object containing the value and onChange props for an input field.

Reusing Logic with Custom Hooks

Let's read a short section from the React docs that implements a useFormInput hook. It shows a practical use case for returning an object of props.

Please read the section titled Custom Hooks let you share stateful logic, not state itself. Scroll down to the Form component example and find the useFormInput custom hook implementation. Notice how it returns an object named inputProps.


A Hybrid Approach

You don't have to choose strictly between an array and an object. Some developers use a hybrid approach that combines the best of both worlds.

A great example is a useBoolean hook, which manages a true/false state.

React: How to create a Custom Hook

This article by Robin Wieruch demonstrates a very clever pattern for a useBoolean hook. It shows how you can return a mix of values and objects.

First, look at the implementation of the useBoolean hook. Notice its return statement: return [state, { setTrue, setFalse, setToggle }]. Then, see how it's used in the App component. This shows a powerful and flexible pattern.

This pattern, [value, { handler1, handler2 }], gives you the easy renaming of the array pattern for the primary value, while grouping all the related updater functions into a convenient, clearly-named object.


Your Turn: Enhance the useCounter Hook

Now it's time to put this into practice. Let's take the useCounter hook from our last lesson and make it more powerful.

Your task is to modify the useCounter hook so that it returns not only the count but also functions to increment, decrement, and reset the count.

Starter Code:

// in useCounter.js
import { useState, useEffect } from 'react';

// This is the hook from the last lesson.
// It currently only returns the count.
export function useCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return count;
}


// in App.js
import { useCounter } from './useCounter';

export default function Counter() {
  const count = useCounter();

  return (
    <div>
      <h1>Seconds passed: {count}</h1>
      {/* We will add buttons here */}
    </div>
  );
}

Instructions:

  1. Modify useCounter.js:

    • The useEffect that automatically increments the count every second is no longer needed. You can remove it.
    • Create three new functions inside your useCounter hook:
      • increment: Should increase the count by 1.
      • decrement: Should decrease the count by 1.
      • reset: Should set the count back to 0.
    • Modify the return statement to return the count and your three new functions. You can choose to return an array or an object—think about the pros and cons we just discussed.
  2. Update App.js:

    • Destructure the values and functions returned from your useCounter hook.
    • Add three buttons to the JSX: "Increment", "Decrement", and "Reset".
    • Connect the onClick handler of each button to the corresponding function from your hook.

Your final Counter component should look something like this, allowing the user to control the count manually.

(This is a mock-up of the target UI)

Take your time to implement this. This exercise solidifies everything we've covered about what a custom hook provides back to a component.


Conclusion

Excellent work today! You've moved from simply defining a hook to making it a functional, reusable piece of logic that provides an API for your components.

Key Takeaways:

  • A custom hook's return statement is its public API, defining what it provides to components.
  • Hooks can return any JavaScript type, but the most common patterns are returning an array or an object for state and its updaters.
  • Array [value, setValue]: Great for positional data. Allows easy renaming during destructuring. The order is important.
  • Object { value, setValue }: Great for named data. The order doesn't matter, and it's self-documenting. Renaming is more verbose.
  • You can choose the pattern that best fits the specific logic of your hook.

Next Up:

We've now built a self-contained, reusable useCounter hook. The final piece of the puzzle is to understand what "reusable" really means. In our next lesson, we will reuse a custom hook across multiple components and see firsthand how React ensures that the logic is shared, but the state is not.