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.
The API has 2 methods navigator.canShare()
and navigator.share()
these can be used to check sharing support and call the systems share method.
In the React app create a folder called Share
and add index.ts
and Share.tsx
files.
// index.ts
export { default as Share } from "./Share"
Exposing the Share
functionality like this is optional but my preferred approach for constructing components.
// 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.
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.
// ShareController.tsx
import { type FC } from "react";
const ShareController: FC<Props> = ({
children,
shareData,
onInteraction,
onSuccess,
onError,
onNonNativeShare,
disabled,
}) => {
const handleOnClick = async () => {
onInteraction?.();
if (navigator?.share) {
try {
await navigator.share(shareData);
onSuccess?.();
} catch (err) {
onError?.(err);
}
} else {
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.
...
const handleOnClick = async () => {
onInteraction?.();
if (navigator?.share) {
try {
await navigator.share(shareData);
onSuccess?.();
} catch (err) {
onError?.(err);
}
} else {
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.
Create a new file SharePopup.tsx
and add the following:
// 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:
...
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.
Going back to Share.tsx if we now update to include our newly added components.
// Share.tsx
import { type FC, useState } from "react";
import ShareController from "./ShareController";
import SharePopup from "./SharePopup";
const Share: FC<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:
// 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>
...
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 my phone:
and on Chrome browser:
onInteraction
, onSuccess
and onError
functions can be used to report sharing behaviour and issues if required.