Add quote components to your React application.
This guide will show you how to integrate Echoes API into your React application. You'll learn how to create React components that display quotes and handle API interactions with proper state management and error handling.
First, make sure you have a React project set up. You can use Create React App or Next.js:
# Using Create React App
npx create-react-app echoes-quotes-app
cd echoes-quotes-app
# Or with Next.js
npx create-next-app echoes-quotes-app
cd echoes-quotes-app
Let's create a simple Quote component that displays a single quote:
// components/Quote.jsx
import React from 'react';
const Quote = ({ quote }) => {
if (!quote) return null;
return (
<div className="quote-card">
<blockquote className="quote-text">
{quote.quote}
</blockquote>
<div className="quote-author">
— {quote.author}
</div>
<div className="quote-language">
Language: {quote.lang}
</div>
</div>
);
};
export default Quote;
Now, let's create a container component that fetches quotes from the Echoes API:
// components/QuotesContainer.jsx
import React, { useState, useEffect } from 'react';
import Quote from './Quote';
const QuotesContainer = () => {
const [quote, setQuote] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchRandomQuote = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('https://echoes.soferity.com/api/quotes/random');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setQuote(data);
} catch (err) {
setError('Failed to fetch quote. Please try again later.');
console.error('Error fetching quote:', err);
} finally {
setLoading(false);
}
};
// Fetch a quote when the component mounts
useEffect(() => {
fetchRandomQuote();
}, []);
return (
<div className="quotes-container">
<h2>Quote of the Day</h2>
{loading && <p className="loading">Loading quote...</p>}
{error && <p className="error">{error}</p>}
{!loading && !error && quote && <Quote quote={quote} />}
<button
onClick={fetchRandomQuote}
disabled={loading}
className="new-quote-btn"
>
Get New Quote
</button>
</div>
);
};
export default QuotesContainer;
Let's enhance our components to allow filtering by language and author:
// components/QuoteFilter.jsx
import React, { useState } from 'react';
const QuoteFilter = ({ onFilterChange }) => {
const [language, setLanguage] = useState('');
const [author, setAuthor] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onFilterChange({ language, author });
};
return (
<form onSubmit={handleSubmit} className="quote-filter">
<div className="filter-group">
<label htmlFor="language">Language:</label>
<select
id="language"
value={language}
onChange={(e) => setLanguage(e.target.value)}
>
<option value="">Any Language</option>
<option value="en">English</option>
<option value="tr">Turkish</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</div>
<div className="filter-group">
<label htmlFor="author">Author:</label>
<input
type="text"
id="author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
placeholder="Enter author name"
/>
</div>
<button type="submit" className="filter-btn">Apply Filters</button>
</form>
);
};
export default QuoteFilter;
Now let's update our QuotesContainer to include filtering:
// Updated QuotesContainer.jsx
import React, { useState, useEffect } from 'react';
import Quote from './Quote';
import QuoteFilter from './QuoteFilter';
const QuotesContainer = () => {
const [quote, setQuote] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({ language: '', author: '' });
const fetchRandomQuote = async () => {
setLoading(true);
setError(null);
try {
// Build the URL with filters
let url = 'https://echoes.soferity.com/api/quotes/random';
const params = new URLSearchParams();
if (filters.language) {
params.append('lang', filters.language);
}
if (filters.author) {
params.append('author', filters.author);
}
// Add query string if we have parameters
if (params.toString()) {
url += `?${params.toString()}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setQuote(data);
} catch (err) {
setError('Failed to fetch quote. Please try again later.');
console.error('Error fetching quote:', err);
} finally {
setLoading(false);
}
};
// Handle filter changes
const handleFilterChange = (newFilters) => {
setFilters(newFilters);
};
// Fetch a quote when filters change
useEffect(() => {
fetchRandomQuote();
}, [filters]);
return (
<div className="quotes-container">
<h2>Quote of the Day</h2>
<QuoteFilter onFilterChange={handleFilterChange} />
{loading && <p className="loading">Loading quote...</p>}
{error && <p className="error">{error}</p>}
{!loading && !error && quote && <Quote quote={quote} />}
<button
onClick={fetchRandomQuote}
disabled={loading}
className="new-quote-btn"
>
Get New Quote
</button>
</div>
);
};
export default QuotesContainer;
To support displaying quotes in multiple languages with proper translations for UI elements, you can use libraries like react-i18next. Here's a basic setup:
npm install react-i18next i18next
// i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
const resources = {
en: {
translation: {
quoteOfTheDay: 'Quote of the Day',
loading: 'Loading quote...',
getNewQuote: 'Get New Quote',
language: 'Language',
author: 'Author',
applyFilters: 'Apply Filters',
anyLanguage: 'Any Language'
}
},
tr: {
translation: {
quoteOfTheDay: 'Günün Sözü',
loading: 'Alıntı yükleniyor...',
getNewQuote: 'Yeni Alıntı Getir',
language: 'Dil',
author: 'Yazar',
applyFilters: 'Filtreleri Uygula',
anyLanguage: 'Herhangi Bir Dil'
}
}
};
i18n
.use(initReactI18next)
.init({
resources,
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false
}
});
export default i18n;
Now we can update our components to use translations:
// Updated QuotesContainer.jsx with i18n
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Quote from './Quote';
import QuoteFilter from './QuoteFilter';
const QuotesContainer = () => {
const { t } = useTranslation();
const [quote, setQuote] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({ language: '', author: '' });
// ... rest of the component remains the same
return (
<div className="quotes-container">
<h2>{t('quoteOfTheDay')}</h2>
<QuoteFilter onFilterChange={handleFilterChange} />
{loading && <p className="loading">{t('loading')}</p>}
{error && <p className="error">{error}</p>}
{!loading && !error && quote && <Quote quote={quote} />}
<button
onClick={fetchRandomQuote}
disabled={loading}
className="new-quote-btn"
>
{t('getNewQuote')}
</button>
</div>
);
};
export default QuotesContainer;
Let's create a page that displays multiple quotes with pagination:
// components/QuoteCollection.jsx
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Quote from './Quote';
const QuoteCollection = () => {
const { t } = useTranslation();
const [quotes, setQuotes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const fetchQuotes = async (pageNum) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`https://echoes.soferity.com/api/quotes?page=${pageNum}&perPage=10`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setQuotes(data.quotes);
setTotalPages(data.totalPages || 1);
} catch (err) {
setError('Failed to fetch quotes. Please try again later.');
console.error('Error fetching quotes:', err);
} finally {
setLoading(false);
}
};
// Fetch quotes when page changes
useEffect(() => {
fetchQuotes(page);
}, [page]);
const handlePrevPage = () => {
if (page > 1) setPage(page - 1);
};
const handleNextPage = () => {
if (page < totalPages) setPage(page + 1);
};
return (
<div className="quotes-collection">
<h2>{t('quoteCollection')}</h2>
{loading && <p className="loading">{t('loading')}</p>}
{error && <p className="error">{error}</p>}
{!loading && !error && quotes.length === 0 && (
<p>No quotes found. Try changing filters or page.</p>
)}
{!loading && !error && quotes.length > 0 && (
<>
<div className="quotes-grid">
{quotes.map(quote => (
<Quote key={quote.id} quote={quote} />
))}
</div>
<div className="pagination">
<button
onClick={handlePrevPage}
disabled={page === 1 || loading}
className="pagination-btn"
>
{t('prevPage')}
</button>
<span className="page-indicator">
{t('pageIndicator', { current: page, total: totalPages })}
</span>
<button
onClick={handleNextPage}
disabled={page === totalPages || loading}
className="pagination-btn"
>
{t('nextPage')}
</button>
</div>
</>
)}
</div>
);
};
export default QuoteCollection;
For more complex applications, you might want to use React Context to manage global state:
// context/QuoteContext.js
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
const QuoteContext = createContext();
export function QuoteProvider({ children }) {
const [quote, setQuote] = useState(null);
const [quotes, setQuotes] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({ language: '', author: '' });
const fetchRandomQuote = useCallback(async () => {
setLoading(true);
setError(null);
try {
// Build the URL with filters
let url = 'https://echoes.soferity.com/api/quotes/random';
const params = new URLSearchParams();
if (filters.language) {
params.append('lang', filters.language);
}
if (filters.author) {
params.append('author', filters.author);
}
// Add query string if we have parameters
if (params.toString()) {
url += `?${params.toString()}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setQuote(data);
} catch (err) {
setError('Failed to fetch quote. Please try again later.');
console.error('Error fetching quote:', err);
} finally {
setLoading(false);
}
}, [filters]);
const fetchQuotes = useCallback(async (page = 1, perPage = 10) => {
setLoading(true);
setError(null);
try {
// Build the URL with filters
let url = `https://echoes.soferity.com/api/quotes?page=${page}&perPage=${perPage}`;
const params = new URLSearchParams();
if (filters.language) {
params.append('lang', filters.language);
}
if (filters.author) {
params.append('author', filters.author);
}
// Add query string if we have parameters
if (params.toString()) {
url += `&${params.toString()}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setQuotes(data.quotes || []);
return data;
} catch (err) {
setError('Failed to fetch quotes. Please try again later.');
console.error('Error fetching quotes:', err);
return { quotes: [] };
} finally {
setLoading(false);
}
}, [filters]);
const updateFilters = (newFilters) => {
setFilters(prev => ({ ...prev, ...newFilters }));
};
useEffect(() => {
fetchRandomQuote();
}, [fetchRandomQuote]);
return (
<QuoteContext.Provider value={{
quote,
quotes,
loading,
error,
filters,
fetchRandomQuote,
fetchQuotes,
updateFilters
}}>
{children}
</QuoteContext.Provider>
);
}
export function useQuote() {
const context = useContext(QuoteContext);
if (context === undefined) {
throw new Error('useQuote must be used within a QuoteProvider');
}
return context;
}
Now you can assemble everything into a complete app:
// App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { QuoteProvider } from './context/QuoteContext';
import QuotesContainer from './components/QuotesContainer';
import QuoteCollection from './components/QuoteCollection';
import './i18n';
import './styles/quotes.css';
const LanguageSwitcher = () => {
const { i18n } = useTranslation();
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div className="language-switcher">
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('tr')}>Türkçe</button>
</div>
);
};
function App() {
const { t } = useTranslation();
return (
<QuoteProvider>
<Router>
<div className="app">
<header className="app-header">
<h1>{t('appTitle')}</h1>
<LanguageSwitcher />
<nav>
<Link to="/">{t('randomQuote')}</Link>
<Link to="/collection">{t('quoteCollection')}</Link>
</nav>
</header>
<main>
<Routes>
<Route path="/" element={<QuotesContainer />} />
<Route path="/collection" element={<QuoteCollection />} />
</Routes>
</main>
<footer>
<p>
{t('poweredBy')} <a href="https://github.com/Taiizor/Echoes" target="_blank" rel="noopener noreferrer">Echoes API</a>
</p>
</footer>
</div>
</Router>
</QuoteProvider>
);
}
export default App;
You now have all the components needed to build a full-featured React application using the Echoes API. This guide covered:
- Basic quote display components
- Fetching and displaying random quotes
- Filtering quotes by language and author
- Multi-language support with react-i18next
- Pagination for browsing multiple quotes
- Global state management with React Context
Feel free to expand on these examples by adding features like:
- Favorite quotes functionality
- Share quotes on social media
- Dark/light theme support
- Advanced filtering and search options
Remember to handle error cases gracefully and provide proper loading states to ensure a good user experience.