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_token2. 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.