Guide

Using Simpleembed with React

Three ways to put your feed in a React or Next.js app, from zero-effort to fully custom. Grab your feed ID from the Embed code dialog on the Feeds page.

1. The widget as a React component

The standard embed.jssnippet doesn't work as-is in JSX — React never executes <script> tags it renders. Inject the script in an effect instead; the widget handles layout, lightbox styling, and auto-resizing exactly like the plain-HTML embed:

"use client"; // Next.js App Router only — plain React apps can drop this line

import { useEffect, useRef } from "react";

export function InstagramFeed({ feedId }: { feedId: string }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = ref.current;
    if (!container) return;

    // Script tags in JSX don't execute — inject it imperatively instead.
    const script = document.createElement("script");
    script.src = "https://simpleembed.com/embed.js";
    script.async = true;
    script.dataset.feedId = feedId;
    container.appendChild(script);

    return () => {
      container.replaceChildren(); // remove iframe + script on unmount
    };
  }, [feedId]);

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

Then anywhere:

<InstagramFeed feedId="YOUR_FEED_ID" />

2. Plain iframe (no script)

If you'd rather avoid injecting scripts, render the embed URL directly. You give up automatic height-matching, so set a height that fits your layout:

export function InstagramFeed({ feedId }: { feedId: string }) {
  return (
    <iframe
      src={`https://simpleembed.com/embed/${feedId}`}
      title="Instagram feed"
      loading="lazy"
      style={{ width: "100%", height: 600, border: 0 }}
    />
  );
}

3. Fully custom UI with the JSON API

For complete control over markup and styling, skip the widget and consume the JSON API. The response shape:

type FeedResponse = {
  feed: { id: string; name: string; source: string };
  posts: Array<{
    id: string;
    mediaType: "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM";
    caption: string | null;
    permalink: string;
    imageUrl: string;
    timestamp: string;
    childCount: number;
  }>;
};

In Next.js, fetch on the server:

// app/components/instagram-grid.tsx — Next.js Server Component

export async function InstagramGrid({ feedId }: { feedId: string }) {
  const res = await fetch(`https://simpleembed.com/api/feeds/${feedId}`, {
    next: { revalidate: 300 }, // the API is CDN-cached ~5 min anyway
  });

  if (!res.ok) return null;
  const { posts }: FeedResponse = await res.json();

  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8 }}>
      {posts.map((post) => (
        <a key={post.id} href={post.permalink} target="_blank" rel="noreferrer">
          <img
            src={post.imageUrl}
            alt={post.caption ?? "Instagram post"}
            loading="lazy"
            style={{ width: "100%", aspectRatio: "1", objectFit: "cover" }}
          />
        </a>
      ))}
    </div>
  );
}

In a client-only React app, a small hook does the same:

// Plain React (Vite, CRA, …) — client-side fetch
import { useEffect, useState } from "react";

export function useInstagramFeed(feedId: string) {
  const [data, setData] = useState<FeedResponse | null>(null);
  const [error, setError] = useState<string | null>(null);

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

    fetch(`https://simpleembed.com/api/feeds/${feedId}`)
      .then((res) => {
        if (!res.ok) throw new Error("Feed not found");
        return res.json();
      })
      .then((json) => !cancelled && setData(json))
      .catch((cause) => !cancelled && setError(String(cause)));

    return () => {
      cancelled = true;
    };
  }, [feedId]);

  return { data, error, loading: !data && !error };
}
Always render images through the imageUrl the API gives you — it proxies Instagram's CDN so images never expire. Hotlinking Instagram URLs directly breaks after a day or two. The API is public and CORS-enabled, so client-side fetching works from any origin.