Creating a deferred promise hook in React
Tags: react, javascript, hooks, typescript
Published: 2022-03-31 02:43:50

Hello fellow readers! In this post I am going to show how to create and use a deferred promise hook in React. Feel free to ask or give your opinion in the comments section.
_It is important that you may have some knowledge about promises to fully understand this article._ If you don't, please read [this great article from MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Let's go!

## Principle

A deferred promise, [defined by the jQuery lib](https://api.jquery.com/category/deferred-object/), is: 

> An object that can register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function.

In simple words, it means that we can store promise's callbacks, such as `resolve` and `reject` to use them later, deferring an action until it's done.

## Use case

Let's imagine the following scenario:

- There is a task list component that has a remove button
- Once the remove button is clicked, a confirm dialog shows up
- Once the user confirms the removal, the task is deleted, otherwise nothing happens

Here is a draft of this idea:

![Scenario draft print screen showing a list with three tasks along with three remove buttons aside](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2crc6uwhpbkt9i2pcpw.png)

We can build the code of this scenario as the following:

- Task list component

```tsx
type ListProps = {
  allowDelete: () => Promise<boolean>;
};

const data = ['Task 1', 'Task 2', 'Task 3'];

const List = ({ allowDelete }: ListProps) => {
  const [tasks, setTasks] = useState(data);

  const handleRemove = async (task: string) => {
    const canDelete = await allowDelete();
    if (!canDelete) return;

    const newTasks = tasks.filter((innerTask) => innerTask !== task);
    setTasks(newTasks);
  };

  return (
    <ul>
      {tasks.map((task) => (
        <li style={{ marginBottom: 10 }}>
          <span>{task}</span>
          <button style={{ marginLeft: 10 }} onClick={() => handleRemove(task)}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
};
```

- Confirm dialog

```tsx
type DialogProps = {
  isOpen: boolean;
  handleConfirm: () => void;
  handleClose: () => void;
};

const Dialog = ({ isOpen, handleConfirm, handleClose }: DialogProps) => {
  return (
    <dialog open={isOpen}>
      <div>Do you really want to remove this task?</div>
      <button onClick={handleConfirm}>Yes</button>
      <button onClick={handleClose}>No</button>
    </dialog>
  );
};
```

- Application

```tsx
const App = () => {
  const [isDialogOpen, setIsDialogOpen] = useState(false);

  const allowDelete = async () => {
    setIsDialogOpen(true);
    return true;
  };

  const handleConfirm = () => {
    setIsDialogOpen(false);
  };

  const handleClose = () => {
    setIsDialogOpen(false);
  };

  return (
    <Fragment>
      <List allowDelete={allowDelete} />
      <Dialog
        isOpen={isDialogOpen}
        handleConfirm={handleConfirm}
        handleClose={handleClose}
      />
    </Fragment>
  );
};
```

Looking at this scenario, it stays clear that the list component **needs to wait for the user intervention** before deciding if a task can or cannot be removed.

But, there is a problem! If we run this code, we will encounter a bug. **As soon as the user clicks the remove button, the task is already deleted before the user's consent.**

![Gif showing the code bug. When the user clicks the remove button of a task, it is immediately removed before the user's consent.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yblgijg92nwao2domum7.gif)

## Deferred promise to the rescue

To fix this bug, **we need to tell our code to wait for the user consent**, and this is possible by creating a deferred promise.
I will show step by step how to create our custom hook.

- First, we will create a type that will hold our **defer object**. This object must have three properties: a `resolve` function, a `reject` function and the `promise` that will be fulfilled. We can note below that the `DeferredPromise` receives a generic type (`DeferType`) that infers the resolve's value type as well as the promise type.
You may skip this step if you are using plain JavaScript instead of TypeScript.

```ts
type DeferredPromise<DeferType> = {
  resolve: (value: DeferType) => void;
  reject: (value: unknown) => void;
  promise: Promise<DeferType>;
};
```

- Next, we are going to start to define the hook's function. This hook begins with a simple ref that will hold our defer object. Note that the hook receives the same generic type defined above.

```ts
export function useDeferredPromise<DeferType>() {
  const deferRef = useRef<DeferredPromise<DeferType>>(null);

  return { deferRef: deferRef.current };
}
```

- So far, so good! Now let's increment our hook with a function that creates the defer object. First, we will build our deferred object.

```ts
// Here is our deferred object that will hold the callbacks and the promise
const deferred = {} as DeferredPromise<DeferType>;

// We then create the main part of our defer object: the promise
// Note that we take the promise's callbacks and inject them into our deferred object
const promise = new Promise<DeferType>((resolve, reject) => {
   deferred.resolve = resolve;
   deferred.reject = reject;
});

// Finally, we inject the whole promise into the deferred object
deferred.promise = promise;
```

- Next, we will update the ref hook with the new deferred object.

```ts
deferRef.current = deferred;
```

- Now we have our complete function and hook! Check it out:

```ts
export function useDeferredPromise<DeferType>() {
  const deferRef = useRef<DeferredPromise<DeferType>>(null);

  const defer = () => {
    const deferred = {} as DeferredPromise<DeferType>;

    const promise = new Promise<DeferType>((resolve, reject) => {
      deferred.resolve = resolve;
      deferred.reject = reject;
    });

    deferred.promise = promise;
    deferRef.current = deferred;
    return deferRef.current;
  };

  return { defer, deferRef: deferRef.current };
}
```

- Alright! Our hook is now complete. Let's now use it to solve the bug we found!

## Using the deferred promise hook

Let's modify the Application component adding the new hook. Note that the `allowDelete` function now returns a deferred promise and the confirm/delete functions resolve this deferred promise.

```tsx
const App = () => {
  const [isDialogOpen, setIsDialogOpen] = useState(false);

  // Here we declare the new hook
  // Note that we will resolve this promise using a boolean value (`true` or `false`). This is the generic type that we defined earlier.
  const { defer, deferRef } = useDeferredPromise<boolean>();

  const allowDelete = async () => {
    setIsDialogOpen(true);
    // Now a deferred promise is being returned
    return defer().promise;
  };

  const handleConfirm = () => {
    setIsDialogOpen(false);
    // If the user consents, the deferred promise is resolved with `true`
    deferRef.resolve(true);
  };

  const handleClose = () => {
    setIsDialogOpen(false);
    // If the user declines, the deferred promise is resolved with `false`
    deferRef.resolve(false);
  };

  return (
    <Fragment>
      <List allowDelete={allowDelete} />
      <Dialog
        isOpen={isDialogOpen}
        handleConfirm={handleConfirm}
        handleClose={handleClose}
      />
    </Fragment>
  );
};
```

Now if we run this code, we will note that the bug is fixed! Our code successfully waits for the user consent before removing a task. If the removal action is declined, nothing happens, as expected.

![Gif showing that the bug is now fixed. The task will be remove if the user gives it's consent, otherwise nothing happens.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rzsyc5h0e4y0gcfgmsfh.gif)
 

## Wrapping up

We successfully created our deferred promise hook from scratch, and it was pretty simple!
I showed just one of the use cases that this hook might become handy, but you can use this whenever you need to wait something to happen before running an action.
Here I also leave the link for every code that was written in this article: [https://stackblitz.com/edit/react-ts-sukfgm?file=index.tsx](https://stackblitz.com/edit/react-ts-sukfgm?file=index.tsx)

Just one important note: **_once you defer a promise, never forget to resolve or reject it, otherwise you might encounter some memory leak problems._**

That's it for now! Please, if you have any doubts, don't hesitate to use the comments section, as I will keep an eye on it!