Fync

React Usage Examples

Learn how to integrate Fync with React applications, including hooks, state management, and common patterns

React Usage Examples

This guide shows how to use Fync in React applications, from basic usage to advanced patterns.

Quick Start with React

Basic Component Integration

import { useState, useEffect } from 'react';
import { GitHub } from '@remcostoeten/fync';

const github = GitHub({ token: process.env.NEXT_PUBLIC_GITHUB_TOKEN! });

export function UserProfile({ username }: { username: string }) {
  const [user, setUser] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const userData = await github.getUser(username);
        setUser(userData);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch user');
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [username]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div className="user-profile">
      <img src={user.avatar_url} alt={user.name} width={100} height={100} />
      <h2>{user.name || user.login}</h2>
      <p>{user.bio}</p>
      <p>{user.public_repos} repositories</p>
      <p>👥 {user.followers} followers</p>
    </div>
  );
}

Custom Hooks

useGitHub Hook

import { useState, useEffect, useCallback } from 'react';
import { GitHub } from '@remcostoeten/fync';

const github = GitHub({ token: process.env.NEXT_PUBLIC_GITHUB_TOKEN! });

export function useGitHub<T = any>(
  fetcher: () => Promise<T>,
  deps: React.DependencyList = []
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const refetch = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const result = await fetcher();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  }, deps);

  useEffect(() => {
    refetch();
  }, [refetch]);

  return { data, loading, error, refetch };
}

// Usage examples:
export function useUser(username: string) {
  return useGitHub(
    () => github.getUser(username),
    [username]
  );
}

export function useUserRepos(username: string) {
  return useGitHub(
    () => github.users.getUserRepos({ username, per_page: 10 }),
    [username]
  );
}

useSpotify Hook

import { useState, useEffect } from 'react';
import { Spotify } from '@remcostoeten/fync';

const spotify = Spotify({ token: process.env.NEXT_PUBLIC_SPOTIFY_TOKEN! });

export function useSpotify<T = any>(
  fetcher: () => Promise<T>,
  deps: React.DependencyList = []
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);
        const result = await fetcher();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Spotify API error');
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, deps);

  return { data, loading, error };
}

// Specific Spotify hooks
export function useCurrentlyPlaying() {
  return useSpotify(
    () => spotify.player.getCurrentlyPlaying(),
    []
  );
}

export function usePlaylist(playlistId: string) {
  return useSpotify(
    () => spotify.playlists.getPlaylist({ playlist_id: playlistId }),
    [playlistId]
  );
}

State Management with React Context

API Context Provider

import React, { createContext, useContext, ReactNode } from 'react';
import { GitHub, Spotify, GoogleCalendar } from '@remcostoeten/fync';

// Create API instances
const github = GitHub({ token: process.env.NEXT_PUBLIC_GITHUB_TOKEN! });
const spotify = Spotify({ token: process.env.NEXT_PUBLIC_SPOTIFY_TOKEN! });
const calendar = GoogleCalendar({ token: process.env.NEXT_PUBLIC_GOOGLE_TOKEN! });

interface APIContextType {
  github: ReturnType<typeof GitHub>;
  spotify: ReturnType<typeof Spotify>;
  calendar: ReturnType<typeof GoogleCalendar>;
}

const APIContext = createContext<APIContextType | null>(null);

export function APIProvider({ children }: { children: ReactNode }) {
  return (
    <APIContext.Provider value={{ github, spotify, calendar }}>
      {children}
    </APIContext.Provider>
  );
}

export function useAPI() {
  const context = useContext(APIContext);
  if (!context) {
    throw new Error('useAPI must be used within an APIProvider');
  }
  return context;
}

// Usage in components
export function GitHubRepoList() {
  const { github } = useAPI();
  const [repos, setRepos] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchRepos() {
      try {
        const userRepos = await github.users.getUserRepos({
          username: 'octocat',
          per_page: 10
        });
        setRepos(userRepos);
      } catch (error) {
        console.error('Failed to fetch repos:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchRepos();
  }, [github]);

  if (loading) return <div>Loading repositories...</div>;

  return (
    <div>
      <h3>Repositories</h3>
      <ul>
        {repos.map((repo) => (
          <li key={repo.id}>
            <a href={repo.html_url} target="_blank" rel="noopener noreferrer">
              {repo.name}
            </a>
            <p>⭐ {repo.stargazers_count}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

Real-time Data Updates

Polling for Updates

import { useState, useEffect, useRef } from 'react';
import { Spotify } from '@remcostoeten/fync';

const spotify = Spotify({ token: process.env.NEXT_PUBLIC_SPOTIFY_TOKEN! });

export function useCurrentlyPlayingPolling(intervalMs: number = 5000) {
  const [currentlyPlaying, setCurrentlyPlaying] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);
  const intervalRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    async function fetchCurrentlyPlaying() {
      try {
        const data = await spotify.player.getCurrentlyPlaying();
        setCurrentlyPlaying(data);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch current track');
      }
    }

    // Initial fetch
    fetchCurrentlyPlaying();

    // Set up polling
    intervalRef.current = setInterval(fetchCurrentlyPlaying, intervalMs);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [intervalMs]);

  return { currentlyPlaying, error };
}

// Component with real-time updates
export function SpotifyNowPlaying() {
  const { currentlyPlaying, error } = useCurrentlyPlayingPolling(3000);

  if (error) return <div>Error: {error}</div>;
  if (!currentlyPlaying?.item) return <div>Nothing playing</div>;

  return (
    <div className="now-playing">
      <h3>Now Playing</h3>
      <p>
        <strong>{currentlyPlaying.item.name}</strong> by {currentlyPlaying.item.artists[0].name}
      </p>
      <div className="progress">
        <div
          className="progress-bar"
          style={{
            width: `${(currentlyPlaying.progress_ms / currentlyPlaying.item.duration_ms) * 100}%`
          }}
        />
      </div>
    </div>
  );
}

Authentication in React

OAuth Flow Component

import { useState, useCallback } from 'react';

interface UseOAuthReturn {
  isAuthenticated: boolean;
  token: string | null;
  login: () => void;
  logout: () => void;
  loading: boolean;
  error: string | null;
}

export function useOAuth(provider: 'github' | 'spotify' | 'google'): UseOAuthReturn {
  const [token, setToken] = useState<string | null>(
    localStorage.getItem(`${provider}_token`)
  );
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const isAuthenticated = !!token;

  const login = useCallback(() => {
    setLoading(true);
    setError(null);

    // Redirect to OAuth provider
    const authUrls = {
      github: 'https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID',
      spotify: 'https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID',
      google: 'https://accounts.google.com/oauth/authorize?client_id=YOUR_CLIENT_ID'
    };

    window.location.href = authUrls[provider];
  }, [provider]);

  const logout = useCallback(() => {
    localStorage.removeItem(`${provider}_token`);
    setToken(null);
  }, [provider]);

  return { isAuthenticated, token, login, logout, loading, error };
}

// Component that handles OAuth callback
export function OAuthCallback({ provider }: { provider: string }) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function handleCallback() {
      try {
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');

        if (code) {
          // Exchange code for token (this should be done on your backend)
          const response = await fetch('/api/auth/callback', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ provider, code })
          });

          const { token } = await response.json();
          localStorage.setItem(`${provider}_token`, token);

          // Redirect to app
          window.location.href = '/dashboard';
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Authentication failed');
      } finally {
        setLoading(false);
      }
    }

    handleCallback();
  }, [provider]);

  if (loading) return <div>Completing authentication...</div>;
  if (error) return <div>Error: {error}</div>;

  return <div>Authentication successful! Redirecting...</div>;
}

Data Fetching Patterns

Server-Side Rendering with Next.js

// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { GitHub } from '@remcostoeten/fync';

const github = GitHub({ token: process.env.GITHUB_TOKEN! });

export default function Dashboard({ userData, repos }: {
  userData: any;
  repos: any[]
}) {
  return (
    <div>
      <h1>Welcome back, {userData.name}!</h1>
      <div className="stats">
        <p>{userData.public_repos} public repositories</p>
        <p>{userData.followers} followers</p>
        <p>{userData.following} following</p>
      </div>

      <h2>Your Recent Repositories</h2>
      <div className="repo-grid">
        {repos.map((repo) => (
          <div key={repo.id} className="repo-card">
            <h3>{repo.name}</h3>
            <p>{repo.description}</p>
            <div className="repo-stats">
              <span>{repo.stargazers_count} stars</span>
              <span>{repo.forks_count} forks</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  try {
    // Fetch data server-side
    const userData = await github.getUser('octocat');
    const repos = await github.users.getUserRepos({
      username: 'octocat',
      per_page: 6,
      sort: 'updated'
    });

    return {
      props: {
        userData,
        repos
      }
    };
  } catch (error) {
    return {
      props: {
        userData: null,
        repos: []
      }
    };
  }
};

Client-Side Data Fetching with SWR

import useSWR from 'swr';
import { GitHub } from '@remcostoeten/fync';

const github = GitHub({ token: process.env.NEXT_PUBLIC_GITHUB_TOKEN! });

// SWR fetcher function
const fetcher = async (url: string) => {
  const response = await fetch(url, {
    headers: {
      'Authorization': `token ${process.env.NEXT_PUBLIC_GITHUB_TOKEN!}`
    }
  });

  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }

  return response.json();
};

export function GitHubUser({ username }: { username: string }) {
  const { data: user, error, isLoading } = useSWR(
    `https://api.github.com/users/${username}`,
    fetcher
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading user</div>;
  if (!user) return <div>No user data</div>;

  return (
    <div className="user-card">
      <img src={user.avatar_url} alt={user.login} width={80} height={80} />
      <h3>{user.name || user.login}</h3>
      <p>{user.bio}</p>
    </div>
  );
}

Advanced React Patterns

Multi-Provider Data Component

import { useState, useEffect } from 'react';
import { GitHub, Spotify, GoogleCalendar } from '@remcostoeten/fync';

const github = GitHub({ token: process.env.NEXT_PUBLIC_GITHUB_TOKEN! });
const spotify = Spotify({ token: process.env.NEXT_PUBLIC_SPOTIFY_TOKEN! });
const calendar = GoogleCalendar({ token: process.env.NEXT_PUBLIC_GOOGLE_TOKEN! });

interface DashboardData {
  githubUser: any;
  currentlyPlaying: any;
  upcomingEvents: any[];
}

export function IntegratedDashboard() {
  const [data, setData] = useState<DashboardData | null>(null);
  const [loading, setLoading] = useState(true);
  const [errors, setErrors] = useState<string[]>([]);

  useEffect(() => {
    async function fetchDashboardData() {
      try {
        setLoading(true);
        setErrors([]);

        // Fetch data from multiple providers in parallel
        const [githubUser, currentlyPlaying, upcomingEvents] = await Promise.allSettled([
          github.getUser('octocat'),
          spotify.player.getCurrentlyPlaying(),
          calendar.events.listEvents({ calendarId: 'primary', maxResults: 5 })
        ]);

        const results: Partial<DashboardData> = {};
        const newErrors: string[] = [];

        if (githubUser.status === 'fulfilled') {
          results.githubUser = githubUser.value;
        } else {
          newErrors.push('Failed to load GitHub data');
        }

        if (currentlyPlaying.status === 'fulfilled') {
          results.currentlyPlaying = currentlyPlaying.value;
        } else {
          newErrors.push('Failed to load Spotify data');
        }

        if (upcomingEvents.status === 'fulfilled') {
          results.upcomingEvents = upcomingEvents.value.items || [];
        } else {
          newErrors.push('Failed to load Calendar data');
        }

        setData(results as DashboardData);
        setErrors(newErrors);
      } catch (error) {
        setErrors(['Failed to load dashboard data']);
      } finally {
        setLoading(false);
      }
    }

    fetchDashboardData();
  }, []);

  if (loading) return <div>Loading dashboard...</div>;

  return (
    <div className="dashboard">
      <h1>Your Integrated Dashboard</h1>

      {errors.length > 0 && (
        <div className="errors">
          {errors.map((error, index) => (
            <div key={index} className="error">{error}</div>
          ))}
        </div>
      )}

      <div className="dashboard-grid">
        {data?.githubUser && (
          <section className="github-section">
            <h2>GitHub Profile</h2>
            <div className="user-summary">
              <h3>{data.githubUser.name}</h3>
              <p>{data.githubUser.public_repos} repos</p>
            </div>
          </section>
        )}

        {data?.currentlyPlaying?.item && (
          <section className="spotify-section">
            <h2>Now Playing</h2>
            <div className="track-info">
              <p>{data.currentlyPlaying.item.name}</p>
              <p>{data.currentlyPlaying.item.artists[0]?.name}</p>
            </div>
          </section>
        )}

        {data?.upcomingEvents && (
          <section className="calendar-section">
            <h2>Upcoming Events</h2>
            <ul>
              {data.upcomingEvents.slice(0, 3).map((event: any) => (
                <li key={event.id}>
                  <h4>{event.summary}</h4>
                  <p>{new Date(event.start.dateTime).toLocaleDateString()}</p>
                </li>
              ))}
            </ul>
          </section>
        )}
      </div>
    </div>
  );
}

Form Integration

Form with API Submission

import { useState } from 'react';
import { GitHub } from '@remcostoeten/fync';

const github = GitHub({ token: process.env.NEXT_PUBLIC_GITHUB_TOKEN! });

export function CreateRepoForm() {
  const [formData, setFormData] = useState({
    name: '',
    description: '',
    private: false
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setSuccess(false);

    try {
      await github.repos.createRepo(formData);
      setSuccess(true);
      setFormData({ name: '', description: '', private: false });
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create repository');
    } finally {
      setLoading(false);
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const { name, value, type } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
    }));
  };

  return (
    <form onSubmit={handleSubmit} className="repo-form">
      <h2>Create New Repository</h2>

      <div className="form-group">
        <label htmlFor="name">Repository Name</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="description">Description</label>
        <textarea
          id="description"
          name="description"
          value={formData.description}
          onChange={handleChange}
          rows={3}
        />
      </div>

      <div className="form-group">
        <label>
          <input
            type="checkbox"
            name="private"
            checked={formData.private}
            onChange={handleChange}
          />
          Private repository
        </label>
      </div>

      {error && <div className="error">{error}</div>}
      {success && <div className="success">Repository created successfully!</div>}

      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create Repository'}
      </button>
    </form>
  );
}

Best Practices

1. Environment Variables

Always use environment variables for sensitive data:

# .env.local
NEXT_PUBLIC_GITHUB_TOKEN=your_github_token
NEXT_PUBLIC_SPOTIFY_TOKEN=your_spotify_token
NEXT_PUBLIC_GOOGLE_TOKEN=your_google_token

2. Error Boundaries

Wrap your components in error boundaries:

import React, { Component, ErrorInfo, ReactNode } from 'react';

class APIErrorBoundary extends Component<
  { children: ReactNode },
  { hasError: boolean }
> {
  constructor(props: { children: ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(_: Error) {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('API Error Boundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong with the API call.</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
<APIErrorBoundary>
  <UserProfile username="octocat" />
</APIErrorBoundary>

3. Loading States

Provide meaningful loading states:

function LoadingSkeleton() {
  return (
    <div className="loading-skeleton">
      <div className="skeleton-avatar" />
      <div className="skeleton-line" />
      <div className="skeleton-line short" />
    </div>
  );
}

// Use in components
if (loading) return <LoadingSkeleton />;

4. TypeScript Integration

Leverage TypeScript for better type safety:

interface GitHubUser {
  id: number;
  login: string;
  name: string | null;
  bio: string | null;
  public_repos: number;
  followers: number;
  following: number;
  avatar_url: string;
}

export function TypedGitHubUser({ username }: { username: string }) {
  const [user, setUser] = useState<GitHubUser | null>(null);

  // ... rest of component with proper typing
}

These patterns will help you build robust, maintainable React applications with Fync.