React Instagram Feed Tutorial
Build Instagram feeds in React with Feedframer's API. Complete tutorial with hooks, components, and error handling. Works with Next.js and other frameworks.
Overview
This guide shows you how to integrate Feedframer's Instagram API into a React application. We'll build a reusable Instagram feed component using modern React patterns with hooks.
Prerequisites
- React 16.8+ (hooks support)
- Feedframer account with connected Instagram account
- API key from Feedframer dashboard
Quick Start
1. Install Dependencies
No special dependencies needed! Use native fetch API or your preferred HTTP client.
npm install
# or
yarn install
2. Basic Component
Create a simple Instagram feed component:
import { useState, useEffect } from 'react';
function InstagramFeed() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch(
'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&page[size]=12'
);
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
const data = await response.json();
setPosts(data.posts);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="grid grid-cols-3 gap-4">
{posts.map((post) => (
<div key={post.id} className="relative aspect-square">
<img
src={post.mediaUrl}
alt={post.caption || 'Instagram post'}
className="w-full h-full object-cover rounded"
/>
</div>
))}
</div>
);
}
export default InstagramFeed;
Custom Hook Pattern
Create a reusable hook for Instagram data:
import { useState, useEffect } from 'react';
function useInstagramPosts(apiKey, options = {}) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({
api_key: apiKey,
'page[size]': options.pageSize || 12,
sort: options.sort || '-publishedAt',
...options.filters && Object.keys(options.filters).reduce((acc, key) => {
acc[`filter[${key}]`] = options.filters[key];
return acc;
}, {}),
});
const response = await fetch(
`https://feedframer.com/api/v1/me?${params}`
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.errors[0]?.title || 'Failed to fetch posts');
}
const data = await response.json();
setPosts(data.posts);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, [apiKey, JSON.stringify(options)]);
return { posts, loading, error };
}
export default useInstagramPosts;
Usage
import useInstagramPosts from './hooks/useInstagramPosts';
function MyComponent() {
const { posts, loading, error } = useInstagramPosts('YOUR_API_KEY', {
pageSize: 9,
filters: { type: 'IMAGE' }
});
// Render posts...
}
Advanced Component with Filtering
import { useState } from 'react';
import useInstagramPosts from './hooks/useInstagramPosts';
function InstagramGallery({ apiKey }) {
const [filter, setFilter] = useState('all');
const { posts, loading, error } = useInstagramPosts(apiKey, {
pageSize: 12,
filters: filter !== 'all' ? { type: filter.toUpperCase() } : {},
});
if (loading) {
return (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
</div>
);
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
Error: {error}
</div>
);
}
return (
<div>
{/* Filter Buttons */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setFilter('all')}
className={`px-4 py-2 rounded ${filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
All
</button>
<button
onClick={() => setFilter('image')}
className={`px-4 py-2 rounded ${filter === 'image' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
Images
</button>
<button
onClick={() => setFilter('video')}
className={`px-4 py-2 rounded ${filter === 'video' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
Videos
</button>
</div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
{posts.map((post) => (
<a
key={post.id}
href={post.permalink}
target="_blank"
rel="noopener noreferrer"
className="group relative aspect-square overflow-hidden rounded-lg"
>
<img
src={post.mediaUrl}
alt={post.caption || 'Instagram post'}
className="w-full h-full object-cover transition-transform group-hover:scale-105"
/>
{/* Overlay on hover */}
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-50 transition-opacity flex items-center justify-center">
<p className="text-white opacity-0 group-hover:opacity-100 px-4 text-center line-clamp-3">
{post.caption}
</p>
</div>
</a>
))}
</div>
</div>
);
}
export default InstagramGallery;
Next.js Integration
Server-Side Rendering (SSR)
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async () => {
const response = await fetch(
`https://feedframer.com/api/v1/me?api_key=${process.env.FEEDFRAMER_API_KEY}&page[size]=12`
);
const data = await response.json();
return {
props: {
posts: data.posts,
},
};
};
export default function InstagramPage({ posts }) {
return (
<div className="grid grid-cols-3 gap-4">
{posts.map((post) => (
<img key={post.id} src={post.mediaUrl} alt="" />
))}
</div>
);
}
Static Site Generation (SSG)
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async () => {
const response = await fetch(
`https://feedframer.com/api/v1/me?api_key=${process.env.FEEDFRAMER_API_KEY}&page[size]=12`
);
const data = await response.json();
return {
props: {
posts: data.posts,
},
revalidate: 3600, // Revalidate every hour
};
};
Error Handling
Implement comprehensive error handling:
function InstagramFeed({ apiKey }) {
const { posts, loading, error } = useInstagramPosts(apiKey);
if (loading) {
return <LoadingSpinner />;
}
if (error) {
// Handle different error types
if (error.includes('401')) {
return <ErrorMessage>Invalid API key. Please check your credentials.</ErrorMessage>;
}
if (error.includes('429')) {
return <ErrorMessage>Rate limit exceeded. Please try again later.</ErrorMessage>;
}
return <ErrorMessage>Failed to load Instagram posts: {error}</ErrorMessage>;
}
if (posts.length === 0) {
return <EmptyState>No Instagram posts found.</EmptyState>;
}
return (
<div className="grid grid-cols-3 gap-4">
{posts.map((post) => (
<InstagramPost key={post.id} post={post} />
))}
</div>
);
}
Performance Optimization
Lazy Loading Images
import { useRef, useEffect, useState } from 'react';
function LazyImage({ src, alt }) {
const imgRef = useRef();
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsLoaded(true);
observer.unobserve(entry.target);
}
});
});
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isLoaded ? src : 'data:image/gif;base64,R0lGODlhAQABAAAAACw='}
alt={alt}
className="w-full h-full object-cover"
/>
);
}
Memoization
import { useMemo } from 'react';
function InstagramGrid({ posts }) {
const gridItems = useMemo(() => {
return posts.map((post) => (
<InstagramPost key={post.id} post={post} />
));
}, [posts]);
return <div className="grid grid-cols-3 gap-4">{gridItems}</div>;
}
Testing
Unit Test with Jest
import { render, screen } from '@testing-library/react';
import InstagramFeed from './InstagramFeed';
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({
posts: [
{
id: '18123456789012345',
mediaUrl: 'https://example.com/image.jpg',
caption: 'Test post',
mediaType: 'IMAGE',
permalink: 'https://instagram.com/p/ABC123',
timestamp: '2024-01-15T18:30:00+00:00'
},
],
}),
})
);
test('renders Instagram posts', async () => {
render(<InstagramFeed apiKey="test-key" />);
const post = await screen.findByAlt('Test post');
expect(post).toBeInTheDocument();
});
Environment Variables
Store API key in environment variables:
# .env.local
NEXT_PUBLIC_FEEDFRAMER_API_KEY=your_api_key_here
const apiKey = process.env.NEXT_PUBLIC_FEEDFRAMER_API_KEY;
Best Practices
- Use environment variables - Never hardcode API keys
- Implement caching - Cache API responses to reduce requests
- Handle errors gracefully - Show user-friendly error messages
- Optimize images - Use lazy loading and responsive images
- Add loading states - Provide feedback during data fetching
- Type safety - Use TypeScript for better developer experience
TypeScript Support
interface InstagramPost {
id: string;
caption: string | null;
mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM' | 'REELS';
mediaUrl: string;
thumbnailUrl: string | null;
permalink: string;
timestamp: string;
children?: any[];
comments?: any[];
}
interface FeedframerResponse {
username: string;
name: string;
biography: string | null;
website: string | null;
profilePictureUrl: string;
followersCount: number;
followsCount: number;
mediaCount: number;
posts: InstagramPost[];
pagination: {
nextCursor: string | null;
prevCursor: string | null;
hasMore: boolean;
perPage: number;
};
}