Share button using React, Web Share API and unsupported fallback

Share button using React, Web Share API and unsupported fallback

The Web Share API provides an easy way to trigger the native share menu.

Before getting started

There're a few things we should know before start coding.

First, browser support, and this is probably its main problem. Web Share API doesn't have the greatest support yet.

Web share API on caniuse.com Web Share API on caniuse.com

However, it's supported on the main mobile browsers and we can add a fallback option to all the unsupported browsers.

Apart from that, some other limitations are:

  • It can only be used on sites served over HTTPS.
  • It must be triggered in response to some user action such as click.

Web Sharing API

To trigger the share menu we are gonna used the promise-based method .share(). But, as I said before and because is not fully supported, let's check if It's supported on the browser first.

const onShareClick = (e) => {
  e.preventDefault()
  if (navigator.share) {
    navigator
      .share({
        title: 'title',
        text: 'Some message',
        url: 'https://yourwebsite.com',
      })
      .catch(console.error)
  }
}

Now, we can attached to the the onClick event of our component and pass the information via props.

const ShareButton = ({ title, text, url }) => {
  const onShareClick = (event) => {
    event.preventDefault()
    if (navigator.share) {
      navigator
        .share({
          title: title,
          text: text,
          url: url,
        })
        .catch(console.error)
    }
  }
 
  return <button onClick={onShareClick}>Share</button>
}

Providing fallback

As I said earlier, let's add a second option for all the other browsers. On this case we'll add options for sharing on facebook, twitter and copy the url to the clipboard. Let's code that.

We can add a state and trigger it whenever navigator.share is unsupported.

const ShareButton = ({ title, text, url }) => {
  const [isShowed, setIsShowed] = useState(false)
 
  const onShareClick = (e) => {
    e.preventDefault()
    if (navigator.share) {
      navigator
        .share({
          title: title,
          text: text,
          url: url,
        })
        .catch(console.error)
    } else {
      setIsShowed((currentIsShowed) => !currentIsShowed)
    }
  }
 
  return <button onClick={onShareClick}>Share</button>
}

Now, let's wrap our button into a div container and add the markup for the fallback.

const ShareButton = ({title, text, url}) => {
  // ...
  return (
    <div>
      <button onClick={onShareClick}>Share</button>
      {isShowed ? (
        <ul>
          <li><button>Share on facebook</button></li>
          <li><button>Share on twitter</button></li>
          <li><button>Copy to the clipboad</button></li>
        </ul>
      ) : null}
    <div>
  )
}

Share on facebook

For facebook there actually a lot of ways to do it, but some options seem a bit overwhelming. You can give it a look if you want to, but in this case I don't want to add the sdk because I don't need to track all the shares from the page, so we are gonna do it by using facebook.com/sharer

const ShareButton = ({title, text, url }) => {
  // ...
  const onFacebookShare = (e) => {
    e.preventDefault()
    window.open(
      `https://www.facebook.com/sharer/sharer.php?u=${url}`,
      'facebook-share-dialog',
      'width=800,height=600'
    )
  }
 
  return (
    <div>
      { /* ... */}
      {isShowed ? (
        <ul>
          <li>
            <button onClick={onFacebookShare}>Share on facebook</button>
          </li>
          { /* ... */}
        </ul>
      ) : null}
    <div>
  )
}

Share on twitter

This one is actually really easy, just need to use an <a> tag and handle it as an external link.

const ShareButton = ({title, text, url }) => {
  // ...
  return (
    <div>
      { /* ... */}
      {isShowed ? (
        <ul>
          { /* ... */}
          <li>
            <a
              target="_blank"
              rel="noopener noreferrer"
              href={`https://twitter.com/intent/tweet?url=${url}&text=${title}`}
            >
              Share on twitter
            </a>
          </li>
        </ul>
      ) : null}
    <div>
  )
}

Copy to the clipboard

And finally, we are gonna use the navigator.clipboard which is supported by most of the modern browsers.

navigator clipboard support on mozilla.com Navigator.Clipboard support on developr.mozilla.org

If you need support on internet explore you might want to explore other alternatives. For now, let's work with navigator.clipboard and add some type of feedback for the users to know when it's been copied.

const ShareButton = ({title, text, url }) => {
  const [isCopied, setIsCopied] = useState(false)
  // ...
 
  const onCopyToClipboard = (e) => {
    e.preventDefault()
    if (navigator.clipboard) {
      navigator.clipboard
        .writeText(url)
        .then(() => setIsCopied(true))
        .catch(console.error)
    }
  }
 
  // Restarts the copy state after a few seconds
  useEffect(() => {
    if (!isCopied) return
 
    const timer = setTimeout(() => {
      setIsCopied(currentIsCopied => !currentIsCopied)
    }, 3000)
 
    return () => clearTimeout(timer)
  }, [isCopied])
 
  return (
    <div>
      { /* ... */}
      {isShowed ? (
        <ul>
          { /* ... */}
          <li>
            <button onClick={onCopyToClipboard}>
              {isCopied ? 'Copied!' : 'Copy to clipboard'}
            </button>
          </li>
        </ul>
      ) :  null}
    <div>
  )
}

Now, you only need to add some styles and that's it.

The full version of our component will look like this.

const ShareButton = ({title, text, url }) => {
  const [isShowed, setIsShowed] = useState(false)
  const [isCopied, setIsCopied] = useState(false)
 
  const onShareClick = (e) => {
    e.preventDefault()
    if (navigator.share) {
      navigator.share({
        title: title,
        text: text,
        url: url,
      })
      .catch(console.error)
    } else {
      setIsShowed(currentIsShowed => !currentIsShowed)
    }
  }
 
  const onFacebookShare = (e) => {
    e.preventDefault()
    window.open(
      `https://www.facebook.com/sharer/sharer.php?u=${url}`,
      'facebook-share-dialog',
      'width=800,height=600'
    )
  }
 
  const onCopyToClipboard = (e) => {
    e.preventDefault()
    if (navigator.clipboard) {
      navigator.clipboard
        .writeText(url)
        .then(() => setIsCopied(true))
        .catch(console.error)
    }
  }
 
  useEffect(() => {
    if (!isCopied) return
 
    const timer = setTimeout(() => {
      setIsCopied(currentIsCopied => !currentIsCopied)
    }, 3000)
 
    return () => clearTimeout(timer)
  }, [isCopied])
 
  return (
    <div>
      <button onClick={onShareClick}>Share</button>
      {isShowed ? (
        <ul>
          <li>
            <button onClick={onFacebookShare}>Share on facebook</button>
          </li>
          <li>
            <a
              target="_blank"
              rel="noopener noreferrer"
              href={`https://twitter.com/intent/tweet?url=${url}&text=${title}`}
            >
              Share on twitter
            </a>
          </li>
          <li>
            <button onClick={onCopyToClipboard}>
              {isCopied ? 'Copied' : 'Copy to clipboard'}
            </button>
          </li>
        </ul>
      ) : null}
    <div>
  )
}

Usage

<ShareButton
  url="https://mywebsite.com/article-slug"
  title="Article Title"
  message="Check this article"
/>

One last thing

You might already notice is gonna be a bit annoying having to write the full url each time we use the component. We can change this by using window.location and changing the url prop's name to path

const ShareButton = ({ title, text, path }) => {
  // Use a ternary for preventing bugs when is running on the server
  const fullURL = window.location ? `${window.location.origin}${path}` : ''
  // ...
  return {
    /* ... */
  }
}

You'll need to replace all the url variables with fullURL

Now, we can use our component and only pass it the path.

// You must use the leading slash for the path
<ShareButton
  path="/article-slug"
  title="Article Title"
  message="Check this article"
/>
OlderSetting up Strapi with Cloudinary and HerokuNewerInfinite marquee animation with React and CSS