Skip to content
VoteFirst Docs
Dashboard

Embed

Feedback widget for React

Put a VoteFirst widget inside a React app, as a component that loads the script once, mounts the widget into a ref, and cleans up when the component unmounts. The plain JavaScript quickstart is the three steps this component wraps.

The component

One component handles any widget: load the script if it is not already on the page, call init once, mount the widget into the element the ref points at, and destroy it on cleanup.

VoteFirstRoadmap.jsx
import { useEffect, useRef } from 'react';

let loading;

function loadScript(src) {
  if (window.VoteFirst) return Promise.resolve();
  loading = loading || new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
  return loading;
}

export default function VoteFirstRoadmap({ project, embedKey }) {
  const ref = useRef(null);

  useEffect(() => {
    let handle;
    let cancelled = false;

    loadScript('https://app.votefirst.app/embed/votefirst.js').then(() => {
      if (cancelled) return;
      window.VoteFirst.init({ project, key: embedKey });
      handle = window.VoteFirst.roadmap(ref.current);
    });

    return () => {
      cancelled = true;
      if (handle) handle.destroy();
    };
  }, [project, embedKey]);

  return <div ref={ref} />;
}

Swap VoteFirst.roadmap for any other call on the widgets page, the board, the changelog, the Most Wanted list, a vote button, a badge, or either popup. The component above is the same for all of them; only the method name and the options object change.

Using it

<VoteFirstRoadmap project="your-project-slug" embedKey="your-embed-key" />

The embed key is public by design, the same as in a plain script tag, so passing it as a prop from a server component or an environment variable is safe.

Client side route changes

If the component unmounts when the route changes, the cleanup function above destroys the widget, and mounting it again on the new route runs the effect once more and draws a fresh one. If the component stays mounted across the route change, usually because it lives in a shared layout, nothing has to be done: the widget stays where it is.

What goes wrong

SymptomCause and fix
origin_not_allowed, a 403The page's origin is not on the embed key's allowlist. Add it under Share & Embed in the dashboard, as described in the quickstart. In development that is usually http://localhost:3000, and in production the deployed origin.
Two widgets, or two script tags, on one re-renderReact's Strict Mode runs an effect, cleans it up, and runs it again in development. The component above survives that: loadScript keeps one promise for the whole page, so the second run waits for the same script tag instead of adding another, and the first run's cancelled flag stops it from drawing at all. Dropping the cleanup function is what causes the duplicate.
window is not defined during a build or on the serverThe component only touches window and document inside useEffect, which never runs during server rendering, so this should not happen if the effect is left as the only place those globals are read.

Next

Plans start at $9 a month after a 14 day trial. Start one, see the prices or open the demo.