Eliaslog.pw

This is how to create a CodePen embed that is GDPR compliant. We’re gonna use the useScript-hook by the fine people of usehooks.com.

For starters, we gonna head over to the CodePen Blog post where we can get the basic template for an embed.

The baseline

I’ve distilled it down to the very minimal:

<p class="codepen" data-slug-hash="XWJPxpZ"></p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

In short, we need a target DOM-Element that has a .codepen class and the data-slug-hash attribute that is necessary for the external CodePen script.

Adapting it to React

Lets refactor that template into an React component.

Here we need to load that script via the useScript - hook, so we can make sure that it’s available when we need it.

// CodePen.js

import React, { useEffect } from "react"
import useScript from "./useScript.js"

const CodePen = ({ url }) => {
  const hash = url.split("/").reverse()[0]

  const CodePenScript = useScript(
    "https://static.codepen.io/assets/embed/ei.js"
  )

  useEffect(() => {
    if (CodePenScript === "ready" && typeof window !== `undefined`) {
      window.__CPEmbed()
    }
  }, [CodePenScript])

  return <p className="codepen" data-slug-hash={hash}></p>
}

export default CodePen

First up we introduce the URL parameter that takes in an ordinary CodePen URL (like https://codepen.io/EliasGuen/pen/abpaaoN).

In the constant hash we then extract the needed hash, that is placed into data-slug-hash.

The loaded script checks for an DOM element with the .codepen class and injects the iframe. Due to the nature of JSX, there can be situations (like a hard reload) where the script runs faster than the target DOM element is present.

This is why we use useEffect to set up a function that runs when the status of CodePenScript changes. CodePenScript registers an __CPEmbed function to the window object wich we are now calling. For all the Gatsby users we’re throwing in a check for typeof window !== "undefined".

We now have a functional CodePen embed for React.

GDPR precautions

Sadly CodePen sets a bunch of cookies in your browser and according to their Privacy Policy, they are tracking you. According to GDPR we now need to ask the user if he wants that.

// CodePen.js
import React, { useEffect, useState } from "react"
import useScript from "./useScript.js"

const CodePen = ({ url }) => {
  const hash = url.split("/").reverse()[0]

  const [consent, setConsent] = useState(false)

  const CodePenScript = useScript(
    "https://static.codepen.io/assets/embed/ei.js"
  )

  useEffect(() => {
    if (consent && CodePenScript === "ready" && typeof window !== `undefined`) {
      window.__CPEmbed()
    }
  }, [CodePenScript, consent])

  return consent ? (
    <p className="codepen" data-slug-hash={hash}></p>
  ) : (
    <div className="placeholder">
      <p>If you wanna see this CodePen embed, click below.</p>
      <p>CodePen will set some cookies to your browser.</p>
      <a
        href="https://blog.codepen.io/documentation/privacy-policy/"
        target="_blank"
        referrerPolicy="no-referrer noopener"
      >
        CodePen.io Privacy Policy
      </a>
      <button
        onClick={() => {
          setConsent(true)
        }}
      >
        Show Codepen
      </button>
    </div>
  )
}

export default CodePen

First we’re using useState to keep track of the users decision. Then we implement a conditional return statement where we check for consent to be true.

If its false, we show a .placeholder informing the user about his options. The button set’s the consent to true and the embed gets rendered.

Again, we have to make sure the __CPEmbed is run after the target DOM has been put on the screen, so we add the consent state to the useEffect.

It’s a bit of an irony that I can’t use this embed to show you a React related topic such as this, so I guess, next up is a CodeSandbox embed 😂. Have a good one!

Full Code

Working example in CodeSandbox: https://codesandbox.io/s/nice-feynman-7li31?file=/src/CodePen.js

Bonus: Example with react-cookie to remember the choice of the user: https://gist.github.com/HooK2000/08438095c84b9698d7a26d9fd9513c28

// useScript.js
// Source: https://usehooks.com/useScript/

import { useState, useEffect } from "react"

// Hook
function useScript(src) {
  // Keep track of script status ("idle", "loading", "ready", "error")
  const [status, setStatus] = useState(src ? "loading" : "idle")
  useEffect(
    () => {
      // Allow falsy src value if waiting on other data needed for
      // constructing the script URL passed to this hook.
      if (!src) {
        setStatus("idle")
        return
      }
      // Fetch existing script element by src
      // It may have been added by another intance of this hook
      let script = document.querySelector(`script[src="${src}"]`)
      if (!script) {
        // Create script
        script = document.createElement("script")
        script.src = src
        script.async = true
        script.setAttribute("data-status", "loading")
        // Add script to document body
        document.body.appendChild(script)
        // Store status in attribute on script
        // This can be read by other instances of this hook
        const setAttributeFromEvent = event => {
          script.setAttribute(
            "data-status",
            event.type === "load" ? "ready" : "error"
          )
        }
        script.addEventListener("load", setAttributeFromEvent)
        script.addEventListener("error", setAttributeFromEvent)
      } else {
        // Grab existing script status from attribute and set to state.
        setStatus(script.getAttribute("data-status"))
      }
      // Script event handler to update status in state
      // Note: Even if the script already exists we still need to add
      // event handlers to update the state for *this* hook instance.
      const setStateFromEvent = event => {
        setStatus(event.type === "load" ? "ready" : "error")
      }
      // Add event listeners
      script.addEventListener("load", setStateFromEvent)
      script.addEventListener("error", setStateFromEvent)
      // Remove event listeners on cleanup
      return () => {
        if (script) {
          script.removeEventListener("load", setStateFromEvent)
          script.removeEventListener("error", setStateFromEvent)
        }
      }
    },
    [src] // Only re-run effect if script src changes
  )
  return status
}
// CodePen.js
import React, { useEffect, useState } from "react";
import useScript from "./useScript.js";

const CodePen = ({url}) => {

  const hash = url.split("/").reverse()[0];

  const [consent, setConsent] = useState(false);

  const CodePenScript = useScript(
    "https://static.codepen.io/assets/embed/ei.js"
  );

  useEffect(() => {
    if (consent
        && CodePenScript === "ready"
        && typeof window !== `undefined`) {
      window.__CPEmbed();
    }
  }, [CodePenScript, consent]);

  return consent ? (
    <p className="codepen" data-slug-hash={hash}"></p>
  ) : (
    <div className="placeholder">
      <p>If you wanna see this CodePen embed, click below.</p>
      <p>CodePen will set some cookies to your browser.</p>
			<a
        href="https://blog.codepen.io/documentation/privacy-policy/"
        target="_blank"
        referrerPolicy="no-referrer noopener"
      >
        CodePen.io Privacy Policy
      </a>
      <button
        onClick={() => {
          setConsent(true);
        }}
      >
        Show Codepen
      </button>
    </div>
  );
};

export default CodePen;
The creation of this post was made possible by coffee.
Buy me a coffee