Show an Instagram feed in React (no npm package needed)
Do you need an npm package to show an Instagram feed in React? No. FeedFusion serves your posts as JSON from Meta's official Instagram API, so the integration is one fetch in a small hook. This guide gives you the hook, a grid component and the TypeScript types.
You don't need an npm package for this. FeedFusion gives you one JSON endpoint, so the whole integration is a fetch inside a small hook, plus a component that renders the posts. Everything below is plain React and TypeScript, with no dependencies.
Setup
Your dashboard shows two values for each feed: the Instagram username and a publicKey (a 32-character hex string). The publicKey is public by design, so it's fine in browser code. Browser requests only work from the sites you add to the feed's allowed sites. Anywhere else, the API answers 403 with domain_not_allowed.
TypeScript types
The response shape, in one shared file. These are the same types the Next.js guide uses:
export type MediaType = 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM'
export type Post = {
id: string
timestamp: string // ISO 8601
permalink: string // Instagram URL
mediaType: MediaType
mediaUrl: string
thumbnailUrl: string | null // null for IMAGE, populated for VIDEO/CAROUSEL
optimizedMediaUrl: string | null // FeedFusion-hosted copy; use first
caption: string | null
}
export type FeedMeta = {
tier: 'free' | 'pro' | 'agency'
showBadge: boolean // show the "Powered by FeedFusion" badge
overLimit: boolean // this month's feed loads passed the plan limit (feed still works)
total: number // total posts available before slice
limit: number // posts returned in this response
offset: number // starting index of this slice
cacheTtl: number // suggested cache time for your side, in seconds (no edge cache)
}
export type FeedResponse = {
username: string
meta: FeedMeta
posts: Post[]
}The hook
useInstagramFeed fetches one page of posts and tells you whether it's loading, failed or ready. It cancels the request if the component unmounts or the inputs change, so a slow response never overwrites a newer one.
import { useEffect, useState } from 'react'
import type { FeedResponse } from '@/types/feedfusion'
export type FeedState =
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'ready'; data: FeedResponse }
type Options = { limit?: number; offset?: number }
export function useInstagramFeed(
username: string,
publicKey: string,
{ limit = 12, offset = 0 }: Options = {},
): FeedState {
const key = `${username}|${publicKey}|${limit}|${offset}`
const [result, setResult] = useState<{ key: string; state: FeedState } | null>(null)
useEffect(() => {
const controller = new AbortController()
const url =
`https://www.feed-fusion.com/api/feeds/${encodeURIComponent(username)}` +
`?publicKey=${encodeURIComponent(publicKey)}&limit=${limit}&offset=${offset}`
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`FeedFusion fetch failed: ${res.status}`)
return res.json() as Promise<FeedResponse>
})
.then((data) => setResult({ key, state: { status: 'ready', data } }))
.catch((error: unknown) => {
if (controller.signal.aborted) return
const err = error instanceof Error ? error : new Error(String(error))
setResult({ key, state: { status: 'error', error: err } })
})
return () => controller.abort()
}, [key, username, publicKey, limit, offset])
// A new username, key or page shows "loading" until its own response arrives.
return result?.key === key ? result.state : { status: 'loading' }
}The component
A simple grid that links each post to Instagram. It uses optimizedMediaUrl first, the copy FeedFusion hosts, because Instagram's own image links expire. It also shows the badge when meta.showBadge is true, the same rule the widget follows.
import { useInstagramFeed } from './useInstagramFeed'
type Props = { username: string; publicKey: string }
export function InstagramFeed({ username, publicKey }: Props) {
const feed = useInstagramFeed(username, publicKey, { limit: 12 })
if (feed.status === 'loading') return <p>Loading posts…</p>
// If the feed can't load, keep the page clean and check the console.
if (feed.status === 'error') return null
const { posts, meta } = feed.data
return (
<div>
<ul style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
{posts.map((post) => (
<li key={post.id}>
<a href={post.permalink} target="_blank" rel="noopener noreferrer">
<img
src={post.optimizedMediaUrl ?? post.thumbnailUrl ?? post.mediaUrl}
alt={post.caption?.slice(0, 120) ?? 'Instagram post'}
loading="lazy"
style={{ width: '100%', aspectRatio: '1', objectFit: 'cover' }}
/>
</a>
</li>
))}
</ul>
{meta.showBadge && (
<a href="https://www.feed-fusion.com" target="_blank" rel="noopener noreferrer">
Powered by FeedFusion
</a>
)}
</div>
)
}Then drop it wherever the feed should appear: <InstagramFeed username="your-instagram-handle" publicKey="your-32-char-public-key" />.
Pagination
Pass offset to step through the stored posts: useInstagramFeed(username, publicKey, { limit: 12, offset: 12 }) returns the second page. limit accepts 1 to 100 and defaults to 24 on the API (the hook asks for 12). meta.total tells you how many posts there are, so you know when to stop showing a "Load more" button. See the API Reference for every parameter.
Rendering on the server
Using Next.js? Fetch on the server instead and let ISR cache the result: each refetch counts as one feed load, not each visitor. The Next.js guide shows the pattern.
Questions? hello@feed-fusion.com
Continue reading