uservoice
API integration
onClick event
web development
JavaScript

Load uservoice API on Click

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

If you only show a support widget when the user asks for help, loading the UserVoice script lazily is usually better than embedding it at page load. It reduces initial network work, delays third-party code until the user explicitly requests it, and makes failures easier to isolate.

The important implementation detail is that the click handler should not inject the script over and over. Instead, create a one-time loader, wait for the script to finish, and only then call the widget API.

Load the Script Only Once

The safest pattern is to wrap the script injection in a promise and cache that promise for future clicks. That gives you one loading path no matter how many times the button is pressed.

html
1<!doctype html>
2<html lang="en">
3  <head>
4    <meta charset="utf-8" />
5    <title>UserVoice lazy loader</title>
6  </head>
7  <body>
8    <button id="support-button">Contact support</button>
9    <p id="status">Widget has not been loaded yet.</p>
10
11    <script>
12      let userVoiceLoadPromise;
13
14      function loadUserVoice(widgetKey) {
15        if (window.UserVoice) {
16          return Promise.resolve(window.UserVoice);
17        }
18
19        if (userVoiceLoadPromise) {
20          return userVoiceLoadPromise;
21        }
22
23        userVoiceLoadPromise = new Promise((resolve, reject) => {
24          const script = document.createElement("script");
25          script.src = `https://widget.uservoice.com/${widgetKey}.js`;
26          script.async = true;
27
28          script.onload = () => {
29            if (window.UserVoice) {
30              resolve(window.UserVoice);
31            } else {
32              reject(new Error("UserVoice loaded, but no API object was created."));
33            }
34          };
35
36          script.onerror = () => reject(new Error("Failed to load the UserVoice script."));
37          document.head.appendChild(script);
38        });
39
40        return userVoiceLoadPromise;
41      }
42
43      document.getElementById("support-button").addEventListener("click", async () => {
44        const status = document.getElementById("status");
45        status.textContent = "Loading support widget...";
46
47        try {
48          const api = await loadUserVoice("YOUR_WIDGET_KEY");
49          status.textContent = "UserVoice loaded.";
50
51          if (typeof api.push === "function") {
52            api.push(["showLightbox", "classic_widget"]);
53          }
54        } catch (error) {
55          status.textContent = error.message;
56          console.error(error);
57        }
58      });
59    </script>
60  </body>
61</html>

This example is fully runnable as an HTML page. To make the actual UserVoice widget appear, replace YOUR_WIDGET_KEY with your real widget key and confirm that the push command matches the widget version your account uses.

Why the Promise Pattern Matters

Without a cached promise, repeated clicks can create multiple script tags and race each other. That leads to inconsistent behavior:

  • the widget may initialize twice
  • later clicks may run before the first load completed
  • error handling becomes scattered across several branches

With a single loadUserVoice function, every click shares the same loading state. The first click starts the download. Later clicks reuse the same promise and wait for the same result.

That is especially useful on slow connections, where a user might click again because nothing appeared immediately.

Keep the Widget Call Separate From the Loader

Notice that loadUserVoice only loads the script and returns the API object. The UI action, which is opening the widget, happens in the click handler after the promise resolves.

That separation keeps responsibilities clear:

  • the loader manages script lifecycle
  • the click handler manages user interaction
  • error messages stay close to the UI that triggered them

If your UserVoice setup uses a different command than ["showLightbox", "classic_widget"], change only the click-handler block. The loader code stays the same.

Handle Failure and Policy Constraints

Third-party scripts can fail for reasons that have nothing to do with your code:

  • the widget key is wrong
  • a content security policy blocks the script host
  • an ad blocker or privacy extension blocks the request
  • the network is offline

That is why the catch block is important. A failed widget load should not break the rest of the page. Show a fallback message or a regular contact link so support remains reachable.

You should also think about consent and privacy requirements. Loading UserVoice only after a click may align better with your policy than eagerly loading the widget for every visitor.

Common Pitfalls

  • Injecting the script on every click instead of caching the first load attempt.
  • Calling UserVoice.push before the script has finished loading.
  • Hard-coding a widget command that does not match the UserVoice version in use.
  • Ignoring content security policy or browser-extension blocking, which can make the script appear randomly broken.
  • Forgetting a fallback path. Support should still be reachable even if the third-party widget fails.

Summary

  • Lazy loading UserVoice on click improves startup performance and delays third-party code until the user asks for it.
  • Wrap the script injection in a cached promise so the widget loads only once.
  • Wait for the promise to resolve before calling the UserVoice API.
  • Keep the loader generic and the widget-open command in the click handler.
  • Always handle load failures and provide a fallback support path.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.