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 };
}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.