DEV Community

Joffy122
Joffy122

Posted on

How to Build a Real-Time Search and News Feed Component in React with Joffstrends Search API

How to Build a Real-Time Search and News Feed Component in React with Joffstrends Search API

In today's fast-paced digital landscape, users expect instant access to information. Whether you are building a personal dashboard, a curated news aggregator, or a market intelligence tool, integrating real-time web search capabilities directly into your frontend can significantly elevate the user experience.

However, many established search APIs are either prohibitively expensive or overly complex to set up. Enter the Joffstrends Search API—a lightweight, fast, and highly cost-effective search API designed specifically for developers who need reliable search results without the enterprise price tag.

In this comprehensive tutorial, we will walk through how to build a fully functional, responsive real-time search and news feed component in React using the Joffstrends Search API. We will cover everything from project setup and API authentication to state management, error handling, and modern UI styling.


Prerequisites

To follow along with this tutorial, you will need:

  • Node.js (v16 or higher) and npm/yarn installed on your machine.
  • A basic understanding of React (specifically functional components and hooks like useState and useEffect).
  • A Joffstrends Search API Key.

Getting Your API Key

If you don't have an API key yet, you can obtain one instantly via Gumroad. Joffstrends offers three flexible tiers depending on your project needs:

  • Starter Plan: £4.99 one-time payment for 7 days of full access (perfect for testing and hackathons). Get Starter Plan
  • Monthly Subscription: £9.99/month for 1,000 searches/month (ideal for hobbyists and small apps). Get Monthly Plan
  • Annual Subscription: £89.99/year for 1,000 searches/month, saving you about 3 months of subscription costs. Get Annual Plan

Once purchased, your unique API key will be emailed to you within approximately one minute.


Step 1: Setting Up Your React Project

We will use Vite to scaffold a modern, ultra-fast React application. Open your terminal and run the following commands:

# Create a new Vite project with React and TypeScript/JavaScript
npm create vite@latest joffstrends-search-app -- --template react

# Navigate into the project directory
cd joffstrends-search-app

# Install dependencies
npm install
Enter fullscreen mode Exit fullscreen mode

For styling, we will use standard CSS in this guide to keep it dependency-free, but you can easily adapt the classes to Tailwind CSS or styled-components.


Step 2: Securing Your API Key

To prevent hardcoding your API key directly into your source code, we will use environment variables. Create a file named .env in the root of your project:

VITE_JOFFSTRENDS_API_KEY=your_actual_api_key_here
Enter fullscreen mode Exit fullscreen mode

Note: In a production environment, making direct API calls from the client-side exposes your API key to anyone inspecting network traffic. For production apps, we highly recommend routing these requests through a simple backend proxy or serverless function. For the purpose of this frontend integration tutorial, we will use the environment variable directly.


Step 3: Designing the Search Component

Let's create a dedicated component for our search feed. Create a new file at src/components/SearchFeed.jsx (or .tsx if using TypeScript).

We need to manage several states:

  1. query: The current search term entered by the user.
  2. results: An array of search results returned by the API.
  3. loading: A boolean to track whether an API request is in progress.
  4. error: A string to capture and display any API or network errors.

Here is the complete implementation of the SearchFeed component:

import React, { useState } from 'react';
import './SearchFeed.css';

const SearchFeed = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSearch = async (e) => {
    e.preventDefault();
    if (!query.trim()) return;

    setLoading(true);
    setError('');
    setResults([]);

    const apiKey = import.meta.env.VITE_JOFFSTRENDS_API_KEY;
    const endpoint = `https://api.joffstrends.co.uk/search?q=${encodeURIComponent(query)}`;

    try {
      const response = await fetch(endpoint, {
        method: 'GET',
        headers: {
          'x-api-key': apiKey,
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) {
        if (response.status === 401 || response.status === 403) {
          throw new Error('Unauthorized: Please check your Joffstrends API key.');
        }
        throw new Error(`Error: ${response.status} ${response.statusText}`);
      }

      const data = await response.json();

      // Assuming the API returns an array of results directly or inside a results field
      // Adjust this based on the exact payload structure
      const searchResults = Array.isArray(data) ? data : data.results || [];
      setResults(searchResults);
    } catch (err) {
      console.error('Search API Error:', err);
      setError(err.message || 'Something went wrong. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="search-container">
      <header className="search-header">
        <h1>Joffstrends Real-Time Feed</h1>
        <p>Search the web instantly using the Joffstrends Search API</p>
      </header>

      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          placeholder="Search for news, trends, or topics..."
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          className="search-input"
        />
        <button type="submit" className="search-button" disabled={loading}>
          {loading ? 'Searching...' : 'Search'}
        </button>
      </form>

      {error && <div className="search-error">{error}</div>}

      {loading && (
        <div className="search-loader">
          <div className="spinner"></div>
          <p>Fetching latest results...</p>
        </div>
      )}

      {!loading && results.length === 0 && query && !error && (
        <div className="search-no-results">
          No results found for "{query}". Try another search term!
        </div>
      )}

      <div className="search-results-grid">
        {results.map((item, index) => (
          <article key={index} className="result-card">
            <h2 className="result-title">
              <a href={item.link || item.url} target="_blank" rel="noopener noreferrer">
                {item.title}
              </a>
            </h2>
            <p className="result-snippet">{item.snippet || item.description}</p>
            <div className="result-footer">
              <span className="result-source">{item.source || 'Web Result'}</span>
              <a href={item.link || item.url} target="_blank" rel="noopener noreferrer" className="result-link">
                Read More &rarr;
              </a>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
};

export default SearchFeed;
Enter fullscreen mode Exit fullscreen mode

Step 4: Styling the Component

To make our component look modern and clean, let's add some CSS. Create a file named src/components/SearchFeed.css and paste the following styles:

.search-container {
  max-width: 900px;
  margin: 0 auto;
  padding: 40px 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  color: #333;
}

.search-header {
  text-align: center;
  margin-bottom: 40px;
}

.search-header h1 {
  font-size: 2.5rem;
  color: #111;
  margin-bottom: 10px;
}

.search-header p {
  font-size: 1.1rem;
  color: #666;
}

.search-form {
  display: flex;
  gap: 12px;
  margin-bottom: 30px;
}

.search-input {
  flex: 1;
  padding: 14px 20px;
  font-size: 1rem;
  border: 2px solid #ddd;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.2s ease;
}

.search-input:focus {
  border-color: #0070f3;
}

.search-button {
  padding: 14px 28px;
  font-size: 1rem;
  font-weight: 600;
  color: #fff;
  background-color: #0070f3;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.search-button:hover {
  background-color: #0051a2;
}

.search-button:disabled {
  background-color: #999;
  cursor: not-allowed;
}

.search-error {
  padding: 15px;
  background-color: #fee2e2;
  color: #dc2626;
  border-radius: 8px;
  margin-bottom: 30px;
  font-weight: 500;
}

.search-loader {
  text-align: center;
  padding: 40px 0;
  color: #666;
}

.spinner {
  border: 4px solid rgba(0, 0, 0, 0.1);
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border-left-color: #0070f3;
  animation: spin 1s linear infinite;
  margin: 0 auto 15px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.search-no-results {
  text-align: center;
  padding: 40px 0;
  color: #666;
  font-size: 1.1rem;
}

.search-results-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
}

@media (min-width: 600px) {
  .search-results-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

.result-card {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.result-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

.result-title {
  font-size: 1.25rem;
  margin: 0 0 12px 0;
  line-height: 1.4;
}

.result-title a {
  color: #111;
  text-decoration: none;
  transition: color 0.2s ease;
}

.result-title a:hover {
  color: #0070f3;
}

.result-snippet { 
  font-size: 0.95rem;
  color: #4b5563;
  line-height: 1.6;
  margin: 0 0 20px 0;
  flex-grow: 1;
}

.result-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.85rem;
}

.result-source {
  background-color: #f3f4f6;
  color: #4b5563;
  padding: 4px 8px;
  border-radius: 4px;
  font-weight: 500;
}

.result-link {
  color: #0070f3;
  text-decoration: none;
  font-weight: 600;
}

.result-link:hover {
  text-decoration: underline;
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Integrating the Component

Now, let's render our new SearchFeed component in the main application file. Open src/App.jsx and replace its content with:

import React from 'react';
import SearchFeed from './components/SearchFeed';
import './App.css';

function App() {
  return (
    <div className="App">
      <SearchFeed />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run your development server to see it in action:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open your browser to the local URL provided by Vite (usually http://localhost:5173), enter a search query, and watch your real-time news feed populate instantly!


Best Practices & Next Steps

Now that you have a working integration, here are a few ways to optimize and enhance your application:

  1. Implement Debouncing: If you want to fetch results as the user types instead of requiring a form submission, use a debounce function (like Lodash's debounce) to delay the API call until the user has stopped typing for 300-500ms. This prevents hitting your API rate limits unnecessarily.
  2. Build a Backend Proxy: As mentioned earlier, to secure your API key in production, create a simple Express.js or Next.js API route that acts as a middleman. Your React frontend calls your backend endpoint, and your backend appends the x-api-key header and forwards the request to api.joffstrends.co.uk.
  3. Add Pagination: If your application requires browsing multiple pages of results, you can easily implement pagination by passing offset or page parameters to the Joffstrends Search API (refer to the official documentation for supported query parameters).

Conclusion

Integrating real-time web search into a React application doesn't have to be expensive or complicated. With the Joffstrends Search API and standard React hooks, you can build a responsive, beautiful search feed in under an hour.

Ready to build your own? Head over to Gumroad to grab your API key and start building today:

Happy coding!

Top comments (0)