3AM React Warrior: Build a News App In one hour 30 min😎
Fonyuy Gita

Fonyuy Gita @fonyuygita

About: Fetch->Decode->Execute->Store

Location:
Cameroon-Bamenda
Joined:
Oct 5, 2023

3AM React Warrior: Build a News App In one hour 30 min😎

Publish Date: May 11
4 0

🔥 As much as I know, I have twice to learn

Are you tired of React tutorials that leave you with boring, useless projects? Struggling to stay motivated while your peers are out partying? Good. While they sleep, you build. This intensive 90-minute tutorial is for the night owls, the grinders, the future developers who understand that the path to success isn't comfortable.

Let's build something employers actually want to see: a professional news application with real-world API integration, clean routing, and state management. If you can follow along at 3AM, you've got what it takes.

📋 What We're Building

A professional-grade News App that:

  • Fetches real data from a public News API
  • Implements proper routing with React Router
  • Features search functionality
  • Displays article details
  • Uses modern React patterns and hooks

⏱️ Jump to Section:


Setup & Environment (00:00 – 00:10)

First, let's set up our project environment. Open your terminal and run:

npx create-react-app react-news-app
cd react-news-app
npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Next, we need an API key. Go to newsapi.org, sign up for a free account, and get your API key.

Create a .env file in your project root folder with:

REACT_APP_NEWS_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

⚠️ Security Note: In a real-world application, you'd handle API keys on the server side. For this tutorial, we're using client-side for simplicity.

Quick Check: Did your project folder structure get created correctly? Run ls or dir to confirm all files are in place.


File Structure & Routing (00:10 – 00:20)

Now, let's set up our file structure and basic routes. Create these folders and files:

src/
├── pages/
│   ├── Home.js
│   └── ArticleDetails.js
├── Layout.js
└── App.js (update this file)
Enter fullscreen mode Exit fullscreen mode

Open App.js and replace its contents with:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './Layout';
import Home from './pages/Home';
import ArticleDetails from './pages/ArticleDetails';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="article/:index" element={<ArticleDetails />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Understanding the Routes:

  1. <BrowserRouter>: Provides routing capabilities to our app
  2. <Routes>: Contains all our route definitions
  3. <Route path="/" element={<Layout />}>: Our parent route with the shared layout
  4. <Route index element={<Home />}>: The default route (homepage)
  5. <Route path="article/:index" element={<ArticleDetails />}>: Dynamic route for article details

What's that :index thing? This is a URL parameter. When a user visits /article/5, the value 5 will be accessible in the ArticleDetails component using useParams(). We'll explore this more in a later section.


Building the Layout (00:20 – 00:30)

Now, let's create our Layout.js file with navigation and an <Outlet /> to display nested routes:

import { Link, Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div style={{ 
      maxWidth: '800px', 
      margin: '0 auto', 
      padding: '20px',
      fontFamily: 'Arial, sans-serif'
    }}>
      <header>
        <h1>React News App</h1>
        <nav>
          <Link to="/" style={{ 
            textDecoration: 'none', 
            color: '#0066cc', 
            fontWeight: 'bold' 
          }}>Home</Link>
        </nav>
      </header>
      <hr />
      <main>
        <Outlet />
      </main>
    </div>
  );
}

export default Layout;
Enter fullscreen mode Exit fullscreen mode

Understanding <Outlet />:

Think of <Outlet /> as a placeholder where child routes will be rendered. In our case:

  • When user visits / → Home component renders inside <Outlet />
  • When user visits /article/5 → ArticleDetails component renders inside <Outlet />

This pattern allows us to maintain consistent UI elements (header, navigation, footer) across different pages without duplicating code.

Example Scenario:

  1. User clicks "Home" link → URL changes to /
  2. React Router finds matching route in App.js
  3. Layout component renders first
  4. Inside Layout, <Outlet /> renders the Home component

Fetching & Displaying Articles (00:30 – 00:45)

Now for the core of our app: fetching and displaying news articles. Create Home.js with:

import { useState, useEffect } from 'react';
import { Link, useSearchParams } from 'react-router-dom';

function Home() {
  const [articles, setArticles] = useState([]);
  const [loading, setLoading] = useState(true);
  const [searchParams, setSearchParams] = useSearchParams();
  const [query, setQuery] = useState(searchParams.get('q') || 'technology');
  const apiKey = process.env.REACT_APP_NEWS_API_KEY;

  useEffect(() => {
    setLoading(true);
    fetch(`https://newsapi.org/v2/everything?q=${query}&apiKey=${apiKey}`)
      .then(res => res.json())
      .then(data => {
        setArticles(data.articles || []);
        setLoading(false);
      })
      .catch(err => {
        console.error("Error fetching news:", err);
        setLoading(false);
      });
  }, [query, apiKey]);

  const handleSearch = (e) => {
    e.preventDefault();
    setSearchParams({ q: query });
  };

  return (
    <div>
      <form onSubmit={handleSearch} style={{ marginBottom: '20px' }}>
        <input 
          type="text"
          value={query} 
          onChange={(e) => setQuery(e.target.value)}
          style={{ padding: '8px', width: '70%' }}
          placeholder="Search for news..."
        />
        <button 
          type="submit"
          style={{
            padding: '8px 16px',
            backgroundColor: '#0066cc',
            color: 'white',
            border: 'none',
            marginLeft: '10px'
          }}
        >
          Search
        </button>
      </form>

      {loading ? (
        <p>Loading articles...</p>
      ) : (
        <div>
          {articles.length === 0 ? (
            <p>No articles found. Try another search term.</p>
          ) : (
            articles.map((article, index) => (
              <div 
                key={index}
                style={{
                  marginBottom: '20px',
                  padding: '15px',
                  borderRadius: '5px',
                  boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
                }}
              >
                <h3>
                  <Link 
                    to={`/article/${index}`} 
                    state={{ articles }}
                    style={{ color: '#0066cc', textDecoration: 'none' }}
                  >
                    {article.title}
                  </Link>
                </h3>
                <p style={{ color: '#666', fontSize: '0.9em' }}>
                  {article.source.name} | {new Date(article.publishedAt).toLocaleDateString()}
                </p>
                <p>{article.description?.slice(0, 150)}...</p>
              </div>
            ))
          )}
        </div>
      )}
    </div>
  );
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

Understanding useSearchParams:

useSearchParams is a hook from React Router that gives us access to URL query parameters (everything after the ? in a URL).

Let's break down how we're using it:

const [searchParams, setSearchParams] = useSearchParams();
const [query, setQuery] = useState(searchParams.get('q') || 'technology');
Enter fullscreen mode Exit fullscreen mode
  1. searchParams works like a Map object - we use get('q') to read the value of the q parameter
  2. setSearchParams lets us update the URL query parameters
  3. If no q parameter exists in URL, we default to "technology"

Example:

  • When user visits / → query defaults to "technology"
  • When user searches for "bitcoin" → URL becomes /?q=bitcoin
  • If user refreshes page or shares URL, the search is preserved

The Search Flow:

const handleSearch = (e) => {
  e.preventDefault();
  setSearchParams({ q: query });
};
Enter fullscreen mode Exit fullscreen mode
  1. User types "climate change" in the input field
  2. User clicks "Search" button
  3. handleSearch prevents default form submission
  4. setSearchParams({ q: query }) updates URL to /?q=climate%20change
  5. URL change triggers our useEffect hook
  6. We fetch new articles matching "climate change"

This pattern creates bookmarkable search pages and maintains a clean user experience.


Article Details Page (00:45 – 00:55)

Now, let's create ArticleDetails.js to show the full article when clicked:

import { useParams, useLocation, Link } from 'react-router-dom';

function ArticleDetails() {
  const { index } = useParams();
  const location = useLocation();
  const articles = location.state?.articles || [];
  const article = articles[index];

  if (!article) {
    return (
      <div>
        <p>Article not found or expired. Please go back to search.</p>
        <Link to="/" style={{ color: '#0066cc' }}>Back to Home</Link>
      </div>
    );
  }

  return (
    <div>
      <div style={{ marginBottom: '20px' }}>
        <Link to="/" style={{ color: '#0066cc', textDecoration: 'none' }}>
          ← Back to Articles
        </Link>
      </div>

      <h2>{article.title}</h2>

      <div style={{ color: '#666', margin: '10px 0' }}>
        <p>Source: {article.source.name}</p>
        <p>Published: {new Date(article.publishedAt).toLocaleString()}</p>
        {article.author && <p>Author: {article.author}</p>}
      </div>

      {article.urlToImage && (
        <img 
          src={article.urlToImage} 
          alt={article.title}
          style={{ maxWidth: '100%', marginBottom: '20px' }} 
        />
      )}

      <p>{article.content || article.description}</p>

      <a 
        href={article.url} 
        target="_blank" 
        rel="noreferrer"
        style={{
          display: 'inline-block',
          marginTop: '20px',
          padding: '10px 15px',
          backgroundColor: '#0066cc',
          color: 'white',
          textDecoration: 'none',
          borderRadius: '4px'
        }}
      >
        Read Full Article
      </a>
    </div>
  );
}

export default ArticleDetails;
Enter fullscreen mode Exit fullscreen mode

Understanding useParams:

useParams is a React Router hook that gives us access to dynamic parameters in the URL path.

const { index } = useParams();
Enter fullscreen mode Exit fullscreen mode

When a user visits /article/5:

  1. React Router matches the path pattern article/:index
  2. useParams() returns { index: "5" }
  3. We destructure to get the index value
  4. We use this value to find the corresponding article in our articles array

The Data Transfer Method:

Notice how we passed data between pages:

// In Home.js
<Link to={`/article/${index}`} state={{ articles }}>
  {article.title}
</Link>

// In ArticleDetails.js
const location = useLocation();
const articles = location.state?.articles || [];
Enter fullscreen mode Exit fullscreen mode

We're using React Router's state property to pass the articles array to the details page. This avoids unnecessary API calls when navigating between pages.

Important Notes:

  1. This approach is good for teaching but has limitations in production apps
  2. If user refreshes the page, location.state will be lost
  3. In a real app, you might use a state management library (Redux, Context API) or make a new API call using article IDs

Code Review & Concepts (00:55 – 01:10)

Let's review what we've built and understand the key React concepts we've used:

1. Project Architecture

  • Created a multi-page application with React Router
  • Used nested routes with a shared layout
  • Implemented URL parameters and query parameters

2. Data Flow

API Request → State Storage → Component Display → Navigation with State
Enter fullscreen mode Exit fullscreen mode

3. React Hooks Used

  • useState: Managing local component state (articles, query)
  • useEffect: Side effects like API calls
  • useSearchParams: Reading and updating URL query parameters
  • useParams: Accessing URL path parameters
  • useLocation: Accessing route state (passed from navigation)

4. Key Concepts Explained

Controlled Inputs:

<input 
  value={query} 
  onChange={(e) => setQuery(e.target.value)}
/>
Enter fullscreen mode Exit fullscreen mode

The input value is controlled by React state, ensuring UI and state stay in sync.

Effect Dependencies:

useEffect(() => {
  // Fetch articles
}, [query, apiKey]);
Enter fullscreen mode Exit fullscreen mode

The effect runs when query or apiKey changes, preventing unnecessary API calls.

Conditional Rendering:

{loading ? (
  <p>Loading articles...</p>
) : (
  // Render articles
)}
Enter fullscreen mode Exit fullscreen mode

Different UI is displayed based on application state.

Optional Chaining:

article.description?.slice(0, 150)
Enter fullscreen mode Exit fullscreen mode

Safely access properties that might be undefined without causing errors.


Extensions & Advanced Features (01:10 – 01:30)

Now that you have a working application, here are some real-world enhancements you can add:

1. Add Categories

// Add this to Home.js
const categories = ['technology', 'business', 'health', 'sports', 'entertainment'];

// Add this to your JSX
<div style={{ marginBottom: '20px' }}>
  {categories.map(cat => (
    <button
      key={cat}
      onClick={() => {
        setQuery(cat);
        setSearchParams({ q: cat });
      }}
      style={{
        margin: '0 5px 5px 0',
        padding: '5px 10px',
        background: query === cat ? '#0066cc' : '#f0f0f0',
        color: query === cat ? 'white' : 'black',
        border: 'none',
        borderRadius: '3px'
      }}
    >
      {cat.charAt(0).toUpperCase() + cat.slice(1)}
    </button>
  ))}
</div>
Enter fullscreen mode Exit fullscreen mode

2. Add Pagination

// Add these to state in Home.js
const [page, setPage] = useState(parseInt(searchParams.get('page')) || 1);
const [totalResults, setTotalResults] = useState(0);

// Update your fetch call
fetch(`https://newsapi.org/v2/everything?q=${query}&page=${page}&apiKey=${apiKey}`)
  .then(res => res.json())
  .then(data => {
    setArticles(data.articles || []);
    setTotalResults(data.totalResults || 0);
    setLoading(false);
  });

// Add pagination controls
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '20px' }}>
  <button
    onClick={() => {
      const newPage = page - 1;
      setPage(newPage);
      setSearchParams({ q: query, page: newPage });
    }}
    disabled={page <= 1}
    style={{ opacity: page <= 1 ? 0.5 : 1 }}
  >
    Previous
  </button>
  <span>Page {page}</span>
  <button
    onClick={() => {
      const newPage = page + 1;
      setPage(newPage);
      setSearchParams({ q: query, page: newPage });
    }}
    disabled={articles.length < 20}
    style={{ opacity: articles.length < 20 ? 0.5 : 1 }}
  >
    Next
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode

3. Save Recent Searches

// Add to Home.js
const [recentSearches, setRecentSearches] = useState(
  JSON.parse(localStorage.getItem('recentSearches')) || []
);

// Update handleSearch
const handleSearch = (e) => {
  e.preventDefault();

  // Save search to recent searches
  if (query.trim() && !recentSearches.includes(query)) {
    const updatedSearches = [query, ...recentSearches.slice(0, 4)];
    setRecentSearches(updatedSearches);
    localStorage.setItem('recentSearches', JSON.stringify(updatedSearches));
  }

  setSearchParams({ q: query });
};

// Add this under your search form
{recentSearches.length > 0 && (
  <div style={{ marginBottom: '20px' }}>
    <p style={{ fontSize: '0.9em', color: '#666' }}>Recent searches:</p>
    <div>
      {recentSearches.map(term => (
        <button
          key={term}
          onClick={() => {
            setQuery(term);
            setSearchParams({ q: term });
          }}
          style={{
            margin: '0 5px 5px 0',
            padding: '3px 8px',
            background: '#f0f0f0',
            border: 'none',
            borderRadius: '3px',
            fontSize: '0.8em'
          }}
        >
          {term}
        </button>
      ))}
    </div>
  </div>
)}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Congratulations, 3AM warrior! You've just built a real-world React application that:

  1. Uses proper routing with React Router
  2. Fetches data from an external API
  3. Implements search functionality with URL persistence
  4. Shows detailed views for individual articles
  5. Uses modern React patterns and hooks

What's Next?

  • Add authentication to save favorite articles
  • Implement a dark mode theme toggle
  • Create a backend proxy to hide your API key
  • Add unit tests with React Testing Library

Remember: while others slept, you built. While they scrolled social media, you coded. This mentality is what separates successful developers from the rest. Keep this hunger for learning and building, and you'll go far.

Now go get some sleep—you've earned it!


with love ❤💜

Comments 0 total

    Add comment