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

  1. Use environment variables - Never hardcode API keys
  2. Implement caching - Cache API responses to reduce requests
  3. Handle errors gracefully - Show user-friendly error messages
  4. Optimize images - Use lazy loading and responsive images
  5. Add loading states - Provide feedback during data fetching
  6. 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;
  };
}

Additional Resources