FeedFusionDocs
Get started free

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:

types/feedfusion.ts
ts
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.

hooks/useInstagramFeed.ts
ts
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.

components/InstagramFeed.tsx
tsx
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