Using the Web Share API in React
Tags: javascript, typescript, react, web
Published: 2022-08-11 07:30:25

A guide to adding a Share button to a React app using the Web Share API.

The Web Share API allows a site to share text, links, files, and other content to user-selected share targets, utilizing the sharing mechanisms of the underlying operating system. Source: https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API

You can use the functionality to add a rich share experience to a site which accesses a device's Native Share, if supported.

## Key Points

Web Share API doesn't have 100% coverage, as of Chrome 104 this only has partial support, so an approach is needed to ensure this is covered.

The Web Share API requires a https secure context.
The API has 2 methods `navigator.canShare()` and `navigator.share()` these can be used to check sharing support and call the systems share method.

## Prerequisites

An already setup React app, this can be a Remix, Next.JS, Create React App or any other Framework as approach agnostic.

## Step 1 - Adding Share Wrapper


In the React app create a folder called Share and add `index.ts` and `Share.tsx` files.

```typescript
// index.ts

export { default as Share } from "./Share"
```

Exposing the Share functionality like this is optional but my preferred approach for constructing components.

```typescript
// Share.tsx

import { type FC, useState } from "react";


const Share: FC<Props> = ({
  children,
  shareData,
  onInteraction,
  onSuccess,
  onError,
  disabled
}) => {
  const [openPopup, setOpenPopup] = useState(false);

  const handleNonNativeShare = () => {
    setOpenPopup(true);
  };

  return (
    <>
        {children}
    </>
  );
};

interface Props {
  children: React.ReactNode;
  shareData: ShareData;
  onSuccess?: () => void;
  onError?: (error?: unknown) => void;
  onInteraction?: () => void;
  disabled?: boolean;
}

export default Share;
```

The above starts to bootstrap the components required, and map out the component's props which we are exposing for use.

Worth noting in the Props `ShareData` is a Typescript language type hence not defining here.

The structure for following components is; a button which will call the navigator.share() functionality and enable the custom Share experience if the Web Share API isn't available.

## Step 2 - Adding Share Controller

Add a file called `ShareController.tsx` this will be our button, however we are going to leave the content of the button open to the consumer to define via children prop in the `Share.tsx` file.

```typescript
// ShareController.tsx

import { type FC } from "react";

const ShareController: FC<Props> = ({
  children,
  shareData,
  onInteraction,
  onSuccess,
  onError,
  onNonNativeShare,
  disabled,
}) => {
  const handleOnClick = async () => {
    onInteraction && onInteraction();
    if (navigator.share) {
      try {
        await navigator.share(shareData);
        onSuccess && onSuccess();
      } catch (err) {
        onError && onError(err);
      }
    } else {
      onNonNativeShare && onNonNativeShare();
    }
  };
  
  return (
    <button
      onClick={handleOnClick}
      type="button"
      disabled={disabled}
    >
      {children}
    </button>
  );
};

interface Props {
  children: React.ReactNode;
  shareData: ShareData;
  onSuccess?: () => void;
  onError?: (error?: unknown) => void;
  onNonNativeShare?: () => void;
  onInteraction?: () => void;
  disabled?: boolean;
}

export default ShareController;
```

The core part of this component is the handleOnClick function. It's worth noting this is an async method as the `navigator.share` method resolves with a Promise.

```typescript
...
const handleOnClick = async () => {
    onInteraction && onInteraction();
    if (navigator.share) {
      try {
        await navigator.share(shareData);
        onSuccess && onSuccess();
      } catch (err) {
        onError && onError(err);
      }
    } else {
      onNonNativeShare && onNonNativeShare();
    }
  };
  
 ...
```
Just exploring the above in a bit more detail:

`onInteraction` - allows a consumer provided function to be called when the share button is clicked.

`onSuccess` - allows a consumer provided function to be called when successful.

`onError` - resolve any errors from Web Share API, note a user cancelling the native share dialog will call this method.

`onNonNativeShare` - handle the cases when a Native share experience isn't available.

I've included a prop to disable the button if required, but some considerations should be taken as to whether this is appropriate for your use case, in particularly regarding accessibility concerns.

## Step 3 - Adding Custom Share Popup

Create a new file `SharePopup.tsx` and add the following:

```typescript
// SharePopup.tsx

import { type FC, useState } from "react";

const SharePopup: FC<Props> = ({
  shareData,
  onClose,
  onError
}) => {
  const [state, setState] = useState<ShareState>("pending");

  const copyClicked = async () => {
    try {
      await navigator.clipboard.writeText(shareData?.url || "");
      setState("success");
    } catch (err) {
      onError && onError(err);
      setState("error");
    }
  };

  const getButtonText = (state: ShareState) => {
    switch (state) {
      case "success":
        return "Link copied";
      case "pending":
      default:
        return "Copy link";
    }
  };

  return (
    <div>
      <div>
        <div>
          <div>
            <div>
              <div>
                <h3>
                  {shareData.title}
                </h3>
                <button onClick={onClose}>
                  <span>Close Share</span>
                  <div aria-hidden="true">
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="24"
                      height="24"
                      viewBox="0 0 24 24"
                    >
                      <g id="close">
                        <path
                          id="x"
                          d="M18.717 6.697l-1.414-1.414-5.303 5.303-5.303-5.303-1.414 1.414 5.303 5.303-5.303 5.303 1.414 1.414 5.303-5.303 5.303 5.303 1.414-1.414-5.303-5.303z"
                        />
                      </g>
                    </svg>
                  </div>
                </button>
              </div>
              <div>
                {state === "error" ? (
                  <div>
                      <p>
                        Unable to copy to clipboard, please manually copy the
                        url to share.
                      </p>
                  </div>
                ) : null}
                <input
                  value={shareData.url}
                  readOnly
                />
                <button
                  onClick={copyClicked}
                >
                  {getButtonText(state)}
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

type ShareState = "pending" | "success" | "error";

interface Props {
  shareData: ShareData;
  onClose: () => void;
  onError?: (error?: unknown) => void;
}

export default SharePopup;
```

I've removed all styling from above and how you choose to implement the above is dependent upon your approach.

The core function is the copyClicked:

```typescript
...
const copyClicked = async () => {
    try {
      await navigator.clipboard.writeText(shareData?.url || "");
      setState("success");
    } catch (err) {
      onError && onError(err);
      setState("error");
    }
  };
...
```

Worth noting again this is another async method. This is to handle to case when native support isn't available, to copy the shareData's url prop calling the `navigator.clipboard.writeText()` method. If this errors, perhaps due to permissions for example a message will appear to indicate a user should manually copy the text.

## Step 4 - Combining the Components

Going back to `Share.tsx` if we now update to include our newly added components.

```typescript
// Share.tsx

import { type FC, useState } from "react";

import ShareController from "./ShareController";
import SharePopup from "./SharePopup";

const Share: FC<WithColors<Props>> = ({
  children,
  shareData,
  onInteraction,
  onSuccess,
  onError,
  disabled
}) => {
  const [openPopup, setOpenPopup] = useState(false);

  const handleNonNativeShare = () => {
    setOpenPopup(true);
  };

  return (
    <>
      <ShareController
        shareData={shareData}
        onInteraction={onInteraction}
        onSuccess={onSuccess}
        onError={onError}
        onNonNativeShare={handleNonNativeShare}
        disabled={disabled}
      >
        {children}
      </ShareController>
      {openPopup ? (
        <SharePopup
          shareData={shareData}
          onClose={() => setOpenPopup(false)}
        />
      ) : null}
    </>
  );
};

interface Props {
  children: React.ReactNode;
  shareData: ShareData;
  onSuccess?: () => void;
  onError?: (error?: unknown) => void;
  onInteraction?: () => void;
  disabled?: boolean;
}

export default Share;
```
We forward a number of functions to the `ShareController.tsx` so now we can use the Share component by providing the appropriate ShareData prop.

Our consumer may look something similar to below:

```typescript
// ShareConsumer.tsx
...
import Share from "./Share";
...
const shareData = {
  title: "Share",
  text: "Share message",
  url: "https://www.brannen.dev"
}
...
    <Share shareData={shareData}>
        <span>Share</span>
    </Share>
...
```

## Conclusion

The `Share.tsx` provides a wrapper for the `ShareController.tsx` and `SharePopup.tsx` in order to access the Web Share API. This enables the Share Component to be imported into consumers and provided with shareData which triggers the end users Native Share experience or a custom Share Popup to copy the provided url.

On phone:

![Image of native share on android phone](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t2m8buqqdnewfocdm1nh.jpg)

on Chrome browser

![Image of custom share popup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ci4z26pdmvxd3izjnl4h.png)


## Further considerations

The Web Share API also enables sharing of Files see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share#parameters
The additional onInteraction, onSuccess and onError functions can be used to report sharing behaviour and issues if required.