How to add an Instagram feed widget to a React, Next.js or Vue site

Pasting a widget’s script tag into a React, Next.js or Vue component looks like it should work, and then nothing appears. That is not a bug in your code or in the widget: JavaScript frameworks deliberately do not run script tags written into components. This post explains why, shows which common approaches fail and how, and gives a short, tested component for each framework that adds the Social Feed by grip space embed in the right place and removes it cleanly when the visitor leaves the page.

Updated October 12, 2026

The short answer

  1. Do not write the script tag into JSX or a Vue template, and do not insert it with dangerouslySetInnerHTML, v-html or innerHTML. In those cases the script does not run, or runs in the wrong place.
  2. Create the script element in code after the component has mounted (useEffect in React and Next.js, onMounted in Vue) and append it to an empty container element.
  3. Clean up when the component unmounts by emptying the container. Otherwise you get two feeds after navigating away and back, and two in development.
  4. Do not use the Next.js Script component for this widget. It is built for page-wide scripts and does not put the feed where your component is.

Why in-place widgets need special handling in a framework

On an ordinary HTML page, the browser reads the HTML from top to bottom and runs each script tag when it reaches it. The Social Feed embed takes advantage of that. When the script runs, it reads document.currentScript, which MDN describes as the script element whose script is currently being processed, and inserts the feed directly after that tag, inside the same parent element. If the browser cannot report the current tag, the widget falls back to the last script tag on the page at that moment. Our post on where to place a widget script covers this in more depth.

Single-page apps work differently. The framework builds and updates the page with JavaScript after it loads, and changes pages without a full reload. That has two consequences for a widget like ours:

  1. A script tag only runs if it is added to the page in a way that makes the browser execute it. Several common ways of adding HTML in frameworks do not.
  2. A script tag runs once, when it is inserted. When the visitor navigates away and comes back, the framework may rebuild the component, and the widget has to be added again, without leaving the previous feed behind.

The widget itself needs nothing special. It needs its script tag to be inserted into the right container, at the right time, once per mount.

What does not work, and why

We tested each approach in headless Chrome 155, with React 19.1 rendering on the client and a test script that uses the same placement logic as the widget. Here is what happened:

ApproachWhat happens
A script tag written in JSX, without asyncReact rendered the tag into the page, but the script did not run. No feed.
A script tag in JSX with asyncThe React documentation says React moves script components with src and async to the document head and de-duplicates them. In our test the tag moved to the head, ran there, and the feed was inserted into the head, which browsers never display. No visible feed.
dangerouslySetInnerHTML with the embed codeReact sets the element’s innerHTML. MDN notes that innerHTML prevents script elements from executing. No feed.
A script tag in a Vue templateVue’s template compiler removes script and style tags from templates (and warns about it in development). The tag never reaches the page.
v-html with the embed codeThe Vue documentation says v-html updates the element’s innerHTML, so the script does not run.
The Next.js Script componentCovered below: the feed appears at the end of the page instead of in your component, and it is not added again after client-side navigation.
Pasting the tag into index.html or the root layoutThe feed is drawn wherever that tag is in the base HTML, usually outside your app’s layout, and only once per full page load.

The React documentation also says that React may leave a script in the page after the component that rendered it has unmounted, and that this has no effect because scripts execute only once when inserted. That is fine for libraries, but it confirms that a script rendered by React will not re-run to redraw a feed when the component comes back.

The pattern that works

The reliable approach is the same in every framework:

  1. Render an empty container element, such as a div, where the feed should appear.
  2. After the component has mounted, create a new script element in code with document.createElement, set its src to your embed address, and append it to the container.
  3. The browser downloads and runs the script. While it runs, document.currentScript points to the element you created, so the widget inserts the feed right after it, inside your container.
  4. When the component unmounts, empty the container. This removes both the script tag and the feed.

In our tests this placed the feed inside the container every time, and the widget found its own tag through document.currentScript rather than the fallback. After the container was emptied and the component mounted again, exactly one feed appeared.

Scripts created this way load asynchronously by default, so they never hold up the rest of your app. Setting async explicitly in the samples below just makes that visible. The embed address is the src value from the embed code in your Social Feed dashboard; every widget has its own.

Where to get the embed address

In your Social Feed dashboard, open the widget and copy its embed code. It is a single tag like this:

<script type="text/javascript" src="https://n246.grip.space/api/v1/instagram/widget/xxxxxxxx"></script>

The components below only need the value of the src attribute, the address between the quotes. Every widget has its own address, so if you create a second widget with different settings, copy its address separately.

You do not need to change your code when you change the widget’s settings. The script is generated for each request with the current layout, filters and colors, so new settings show up the next time the component mounts. The same goes for switching a widget to Unpublished in the dashboard: the script then draws nothing, and your component simply stays empty.

React (Vite, Create React App and similar)

Create a small component that takes the embed address as a prop:

import { useEffect, useRef } from 'react';

export default function InstagramFeed({ src }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const container = containerRef.current;
    if (!container || !src) return;

    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    container.appendChild(script);

    return () => {
      // Removes the script tag and the feed the widget drew after it
      container.replaceChildren();
    };
  }, [src]);

  // Keep this div empty in JSX: the widget fills it, React does not
  return <div ref={containerRef} className="instagram-feed" />;
}

Then use it wherever the feed should appear:

<section>
  <h2>Follow us on Instagram</h2>
  <InstagramFeed src="https://n246.grip.space/api/v1/instagram/widget/xxxxxxxx" />
</section>

A few details in this component matter:

  1. The container is empty in JSX. The React documentation advises against changing DOM nodes that React manages, but says it is safe to add or remove elements inside a div that is always empty in JSX, because React has no reason to touch its children. Do not put other JSX inside the container.
  2. src is a dependency of the effect. If you switch the component to a different widget, React runs the cleanup (removing the old feed) and then the effect again with the new address.
  3. The cleanup empties the container. The widget inserts the feed directly after its script tag, inside the same parent, so emptying the parent removes both. replaceChildren() with no arguments is supported in all current browsers; container.innerHTML = '' does the same thing.

In TypeScript, type the ref as useRef<HTMLDivElement>(null) and the props as { src: string }, as in the Next.js version below.

Next.js (App Router)

In the App Router, components are Server Components by default, and effects only run in Client Components. Put the widget component in its own file with the "use client" directive at the top:

// components/InstagramFeed.tsx
'use client';

import { useEffect, useRef } from 'react';

export default function InstagramFeed({ src }: { src: string }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = containerRef.current;
    if (!container || !src) return;

    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    container.appendChild(script);

    return () => {
      container.replaceChildren();
    };
  }, [src]);

  return <div ref={containerRef} className="instagram-feed" />;
}

You can then import it into any page or layout, including Server Components:

// app/page.tsx (a Server Component is fine here)
import InstagramFeed from '../components/InstagramFeed';

export default function Page() {
  return (
    <main>
      {/* ...your page content... */}
      <section>
        <h2>Follow us on Instagram</h2>
        <InstagramFeed src="https://n246.grip.space/api/v1/instagram/widget/xxxxxxxx" />
      </section>
    </main>
  );
}

The server sends an empty container, and the feed appears in it once the page has loaded in the browser and the effect has run. On client-side navigation, the component unmounts and mounts like any other, and the cleanup keeps it to one feed. The same component works unchanged in the Pages Router; the "use client" line is simply ignored there.

Why not the Next.js Script component?

next/script is the recommended way to load third-party scripts in Next.js, so it is the natural first try. It is designed for scripts that do not care where they sit in the page, and two of its documented behaviors work against an in-place widget:

  1. The script is loaded only once. The Next.js guide says Next.js will ensure the script will only load once, even if a user navigates between multiple pages. Our widget draws one feed each time its script runs, so after client-side navigation away and back, the feed would not be drawn again.
  2. The script is not placed where your component is. The documentation says beforeInteractive scripts are always injected into the head. For the default afterInteractive and for lazyOnload, the docs describe the script as injected client-side but do not say where. In the Next.js source (version 16.2), the script element is appended to the end of the body. Either way, the feed would appear in the head (invisible) or at the very bottom of the page, below your footer.

next/script works well for analytics and other page-wide tags. For the feed, use the component above. And do not add the embed code to a root layout or a tag manager; see why an Instagram feed shows twice for what happens when copies pile up.

Vue 3 (and Nuxt)

The same idea in a Vue single-file component, using the Composition API:

<!-- InstagramFeed.vue -->
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';

const props = defineProps({ src: { type: String, required: true } });
const container = ref(null);

function mountFeed() {
  const el = container.value;
  if (!el) return;
  el.replaceChildren(); // remove any previous feed first

  const script = document.createElement('script');
  script.src = props.src;
  script.async = true;
  el.appendChild(script);
}

onMounted(mountFeed);
watch(() => props.src, mountFeed);
onBeforeUnmount(() => {
  container.value?.replaceChildren();
});
</script>

<template>
  <!-- Keep this div empty: the widget fills it -->
  <div ref="container" class="instagram-feed"></div>
</template>

Use it like any other component:

<InstagramFeed src="https://n246.grip.space/api/v1/instagram/widget/xxxxxxxx" />

The Vue documentation describes onMounted as running after the component’s own DOM tree has been created and inserted, which is the point where the container exists. It also says onMounted, onBeforeUnmount and onUnmounted are not called during server-side rendering, so the component does not try to touch the DOM on the server. That makes it safe in Nuxt and other server-rendered Vue setups without extra checks. onBeforeUnmount runs while the component is still fully functional, so the container ref is still available for the cleanup.

The watch re-mounts the feed if you pass a different src. mountFeed empties the container first, so switching widgets never leaves two feeds. With the Options API, the same code goes in mounted() and beforeUnmount().

Other frameworks

The pattern carries over to Svelte, Angular, Solid and others. Look for two hooks: one that runs after the component’s DOM exists, and one that runs before it is removed. Create and append the script in the first, and empty the container in the second. Avoid any feature that inserts the embed code as an HTML string, since that uses innerHTML and the script will not run.

Caveats and how to handle them

Development mode runs the script twice

In development, React’s Strict Mode mounts every component, unmounts it and mounts it again, to check that effects clean up properly. The React documentation describes this as expected behavior and says it only happens in development.

We tested the component above under Strict Mode in a React 19 development build. The script ran twice, but only one feed appeared. The first script element had already been removed by the cleanup when it finished downloading; the widget found its own tag, saw that it was no longer in the page, and drew nothing. The second script drew the feed. In a production build the script ran once.

One side effect: the first, removed copy still sends a view. Views from your development machine may therefore be counted twice. On the Free plan, with 3,000 views a month, heavy local testing against a live widget can add up, so consider a separate widget for development or test with a production build.

Without cleanup, feeds pile up

We also tested the same component without the cleanup function. Under Strict Mode it drew two feeds in the same container, and in an app the same thing happens each time a visitor navigates back to the page without a full reload. If you see a second feed after navigating around your site, check that the cleanup empties the container. Our post on an Instagram feed showing twice covers the other causes of duplicates.

Every mount is a new view and a new request

The widget script is generated fresh for each request so it reflects your latest posts and settings, and browsers are told not to cache it. Each mount downloads it again and counts one view, just like a page view on a regular site. That is expected, but avoid patterns that remount the component unnecessarily, such as changing its key on every render or toggling it on and off with state. Your dashboard shows views per widget, and the plans and their monthly allowances are in the pricing section.

The lightbox lives outside your component

When a visitor clicks a post (with the lightbox click setting), the widget attaches the lightbox to the end of the page body so it can cover the whole screen. Your cleanup does not remove it. If a visitor presses the browser’s Back button while a post is open, the lightbox can stay on screen over the next page until they click it. Most visitors close the lightbox before navigating, but if this matters for your site, you can set the click action to open the post on Instagram or to do nothing.

A few listeners stay behind

The widget adds listeners to the window so the grid can adjust its number of columns when the screen size changes. It does not offer a function to remove them, so after the feed is removed they remain until the next full page load. They are small and do nothing visible once the feed is gone, but it is worth knowing if you profile memory in a long-running app.

Layout shift

The feed is drawn in the browser after your app has rendered, so content below it moves down when it appears. Placing the feed below your main content keeps that movement away from what visitors are reading. Our post on social media widgets and layout shift has more options.

Content Security Policy

If your app sends a Content Security Policy, it must allow the widget script and the images from Instagram’s servers, whether the script is in your HTML or created in code. See Content Security Policy for the Instagram widget for the directives involved.

A testing checklist

  1. Build your app for production and open a page with the feed in a private window. The feed appears inside your container, not at the top or bottom of the page.
  2. Navigate to another page and back several times without reloading. There is always exactly one feed.
  3. In the browser’s developer tools, search the Elements panel for instagram/widget. There is one script tag per feed, inside your container, followed by a div with the class grip-ig-widget.
  4. Check the Console for errors, including Content Security Policy messages.
  5. Check on a phone. The grid reduces its number of columns on narrow screens.

Troubleshooting

Nothing appears, and there is no widget script in the page. The tag was written in JSX, a Vue template, or inserted as HTML, and never ran or was removed. Use the component pattern above.

The script tag is in the head. It was rendered with async in React, loaded through next/script with beforeInteractive, or placed in head settings. Move it into the component.

The feed appears at the bottom of the page, below the footer. It is probably loaded with next/script, a tag manager, or from the root HTML file. Remove that copy and use the component.

Two feeds appear after navigating back to the page. The cleanup is missing or does not empty the container the script was appended to.

The feed works in development but not in production. Check your production Content Security Policy, and that the embed address in your environment configuration is correct.

The feed still does not show, even with the component. See Instagram feed not showing on your website for causes on the widget side, such as an unpublished widget or a view limit.

Frequently asked questions

Is there an npm package or a React component from Social Feed? No. The embed is a single script tag, and the component in this post is all you need. It has no dependencies beyond your framework.

Can I render the feed on the server? No. The feed is drawn by the script in the visitor’s browser, so server-rendered HTML contains only the empty container.

Can I show two widgets on one page? Yes. Use the component twice with two different embed addresses. Each feed appears in its own container. See multiple Instagram feeds on one website.

Does this work with a static export of a Next.js site or a static Vue build? Yes. The component runs entirely in the browser, so it does not depend on a server.

My site is not a single-page app. Do I need any of this? No. On a regular site, paste the embed code where the feed should appear. Our HTML guide and the platform guides for WordPress, Shopify and Webflow show where.

New to Social Feed? Create a free account to get your embed address, then drop it into the component for your framework.

Sources

  1. React documentation, “<script>”: https://react.dev/reference/react-dom/components/script
  2. React documentation, “Common components” (dangerouslySetInnerHTML): https://react.dev/reference/react-dom/components/common
  3. React documentation, “Synchronizing with Effects”: https://react.dev/learn/synchronizing-with-effects
  4. React documentation, “Manipulating the DOM with Refs”: https://react.dev/learn/manipulating-the-dom-with-refs
  5. Next.js documentation, “Script Component”: https://nextjs.org/docs/app/api-reference/components/script
  6. Next.js documentation, “How to load and optimize scripts”: https://nextjs.org/docs/app/guides/scripts
  7. Vue.js documentation, “Template Syntax” (v-html): https://vuejs.org/guide/essentials/template-syntax.html
  8. Vue.js documentation, “Built-in Directives” (v-html): https://vuejs.org/api/built-in-directives.html
  9. Vue.js documentation, “Composition API: Lifecycle Hooks”: https://vuejs.org/api/composition-api-lifecycle.html
  10. Vue.js compiler source, “ignoreSideEffectTags”: https://coverage.vuejs.org/compiler-dom/src/transforms/ignoresideeffecttags.ts
  11. MDN Web Docs, “Document: currentScript property”: https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript
  12. MDN Web Docs, “Element: innerHTML property”: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML

Get your embed code in a few minutes. The Free plan needs no credit card.

Sign up free