DEV Community

Cover image for Day 20 of #100DaysOfCode — Building a Tour App (Part 2)
M Saad Ahmad
M Saad Ahmad

Posted on

Day 20 of #100DaysOfCode — Building a Tour App (Part 2)

Today marks Day 20 of my #100DaysOfCode challenge, and I finally started the actual coding phase of my tour application after setting up the project structure.
In this part, I focused on:

✔ Fetching country data
✔ Using React Query for caching
✔ Fetching images from the Unsplash API
✔ Managing state with Zustand
✔ Displaying the final results in the UI

Let’s walk through everything I built today.


🌍 Fetching Countries with Axios

The first step was building a simple API utility to fetch countries from the API.

import axios from 'axios'

export const fetchCountries = async () => {
  const { data } = await axios.get('https://restcountries.com/v3.1/all?fields=name')
  return data
}
Enter fullscreen mode Exit fullscreen mode

This function sends a GET request to the REST Countries API and extracts only the name field.

  • axios.get() → fetches all countries
  • fields=name → reduces payload size
  • Returns the cleaned data for consuming components and hooks

🔄 Using React Query (useQuery) to Cache Countries

To handle caching and background refetching, I wrapped the API call inside a custom React Query hook.

// hooks/useCountries.js
import { useQuery } from '@tanstack/react-query'
import { fetchCountries } from '../data/countriesapi'

export const useCountries = () => {
  return useQuery({
    queryKey: ['countries'],
    queryFn: fetchCountries,
    staleTime: 1000 * 60 * 10,
  })
}
Enter fullscreen mode Exit fullscreen mode

This hook returns cached country data and keeps it fresh for 10 minutes.

  • queryKey: ensures caching uniqueness
  • queryFn: executes the API function
  • staleTime: prevents unnecessary refetching
  • Keeps the UI fast and responsive

🖼 Fetching Unsplash Images Using the Unsplash API

To make each tour visually appealing, I fetched images dynamically.

import { useQuery } from "@tanstack/react-query";

const UNSPLASH_ACCESS_KEY = "YOUR_API_KEY";

const fetchUnsplashImages = async (query = "city", count = 6, seed = "") => {
  const seedParam = seed ? `&seed=${seed}` : "";
  const res = await fetch(
    `https://api.unsplash.com/photos/random?count=${count}&query=${query}&client_id=${UNSPLASH_ACCESS_KEY}${seedParam}`,
  );

  if (!res.ok) {
    throw new Error("Failed to fetch Unsplash images");
  }

  const data = await res.json();
  return data.map((img) => img.urls.regular);
};

const useImages = (query = "city", count = 6, seed = "") => {
  return useQuery({
    queryKey: ["unsplash-images", query, count, seed],
    queryFn: () => fetchUnsplashImages(query, count, seed),
    staleTime: 1000 * 60 * 5,
    retry: 1,
  });
};

export default useImages;
Enter fullscreen mode Exit fullscreen mode

This hook fetches random images from Unsplash, caches them, and returns only the usable URLs.

  • fetchUnsplashImages() → fetches random images
  • Returns img.urls.regular to simplify image handling
  • useImages(): uses React Query for caching and retry strategies
  • queryKey: avoids collisions and allows unique caching per query

🗂 Managing Tour Search & Filters with Zustand

To keep the app lightweight and scalable, I used Zustand for global state.

// useTourStore.js
import { create } from "zustand";
import tours from "../data/tours";

const useTourStore = create((set, get) => ({
  search: "",
  filterSearch: "all",

  setSearch: (value) => set({ search: value }),
  setFilter: (value) => set({ filterSearch: value }),

  getFilteredTours: (countries = []) => {
    const { search, filterSearch } = get();

    if (!search) return tours;

    return tours.filter((tour) => {
      const destination = tour.destination.toLowerCase();
      const searchLower = search.toLowerCase();

      if (filterSearch === "country") {
        return destination.includes(searchLower);
      }

      if (filterSearch === "city") {
        return destination.includes(searchLower);
      }

      return destination.includes(searchLower);
    });
  },

  isValid: (countries = []) => {
    const { search } = get();
    if (!search) return true;
    return countries.some((c) =>
      c.name?.common?.toLowerCase()?.includes(search.toLowerCase()) ||
      c.name?.official?.toLowerCase()?.includes(search.toLowerCase()),
    );
  },
}));

export default useTourStore;
Enter fullscreen mode Exit fullscreen mode

Zustand manages search input, filter type (city/country), validation, and filtered tour results.

  • search → User search text
  • filterSearch"all" | "city" | "country"
  • setSearch + setFilter → Update state
  • getFilteredTours() → Returns filtered tours based on destination
  • isValid() → Checks if search matches any country name

This keeps the UI logic clean and separates data processing from components.


🖥 Displaying Tours in the UI (React + Zustand + React Query)

Finally, I wired everything together inside the Tours component.

import { useCountries } from "../hooks/useCountries";
import useTourStore from "../hooks/useTourStore";
import TourCard from "../components/TourCard";

const Tours = () => {
  const { data: countries = [] } = useCountries();
  const {
    search,
    filterSeach,
    setSearch,
    setFilter,
    getFilteredTours,
    isValid,
  } = useTourStore();

  const filteredTours = getFilteredTours(countries);
  const validCountry = isValid(countries);

  return (
    <div className="min-h-screen bg-gray-50 py-12">
      <div className="max-w-7xl mx-auto px-4">
        <div className="text-center mb-12">
          <h2 className="text-4xl font-bold text-gray-900 mb-2">Explore Our Tours</h2>
          <p className="text-lg text-gray-600">Discover amazing destinations around the world</p>
        </div>

        <div className="bg-white rounded-lg shadow-md p-8 mb-12">
          <div className="max-w-2xl mx-auto">
            <div className="mb-6">
              <input
                value={search}
                onChange={(e) =>
                  setSearch(e.target.value)
                }
                type="text"
                placeholder="Search by city or country..."
                className="w-full border-2 border-gray-300 rounded-lg p-3 focus:border-chart-5 focus:outline-none transition"
              />
            </div>

            {/* Filter Buttons */}
            <div className="flex gap-3 justify-center flex-wrap">
              <button
                className="bg-chart-2 hover:bg-chart-3 text-white px-6 py-2 rounded-lg font-semibold transition duration-200"
                onClick={() => setFilter("all")}
              >
                All Tours
              </button>
              <button
                className="bg-chart-2 hover:bg-chart-3 text-white px-6 py-2 rounded-lg font-semibold transition duration-200"
                onClick={() => setFilter("city")}
              >
                Search by City
              </button>
              <button
                className="bg-chart-2 hover:bg-chart-3 text-white px-6 py-2 rounded-lg font-semibold transition duration-200"
                onClick={() => setFilter("country")}
              >
                Search by Country
              </button>
            </div>
          </div>
        </div>

        {/* Error Messages */}
        <div className="mb-8">
          {search && !validCountry && (
            <div className="bg-red-50 border border-red-200 text-red-700 px-6 py-4 rounded-lg text-center">
              "{search}" is not a valid country or city
            </div>
          )}

          {search && validCountry && filteredTours.length === 0 && (
            <div className="bg-yellow-50 border border-yellow-200 text-yellow-700 px-6 py-4 rounded-lg text-center">
              Sorry, no tours available for "{search}" at the moment
            </div>
          )}
        </div>

        {/* Tours Grid */}
        {filteredTours.length > 0 ? (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
            {filteredTours.map((tour) => (
              <TourCard key={tour.id} tour={tour} />
            ))}
          </div>
        ) : (
          !search && (
            <div className="text-center py-12">
              <p className="text-gray-600 text-lg">No tours found</p>
            </div>
          )
        )}
      </div>
    </div>
  );
};

export default Tours;
Enter fullscreen mode Exit fullscreen mode

This component connects:

  • React Query → country data
  • Zustand → search, filters, validation
  • TourCard → display layout

  • useCountries() fetches fresh country data

  • useTourStore() retrieves search + filters

  • Conditional rendering shows:

    • Invalid search
    • No tour results
    • Tours grid

Everything updates instantly because Zustand is super fast.


Final Result:

Hero Image

Destination Section

Destination Page

Filtering by search


🧠 What I Learned By Building This Tour App

Today was a solid step forward in building this tour application, and I learned quite a lot through the process.

✔ Fetching Data Cleanly with Axios

I became more comfortable using axios to fetch external data. Setting up a clean API function and keeping logic modular helped make the codebase easier to maintain.

✔ Using React Query to Extract + Cache Data

I also learned how React Query’s useQuery extracts, caches, and manages fetched data efficiently.
Being able to handle background updates, stale times, and instant loading states made the app feel much snappier.

✔ Understanding Zustand in a Real Project

Even though Zustand wasn’t strictly required for this part of the app, I intentionally used it to understand:

  • how global state is created
  • how setters update the UI
  • how selectors and custom logic behave in practice
  • how lightweight Zustand feels compared to larger state managers

Using Zustand for searching, filtering, and validating the input gave me hands-on experience and helped reinforce how powerful and minimal it is.

Overall, today’s work helped me bridge different concepts together, fetching data, caching data, and managing app-level state — all inside a single feature.


Wrap Up

Day 20 was all about putting the foundational pieces together: fetching real data, caching it efficiently, managing global state, and then displaying everything interactively in the UI.

By connecting Axios, React Query, Zustand, and structured components, the project is now moving from setup into functional, real-world behavior. This part of the journey gave me practical experience with data flow, state management, component design, and error handling, all essential pieces for building scalable React applications.

Happy coding (and building)! 🚀

Top comments (0)