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
}
This function sends a GET request to the REST Countries API and extracts only the
namefield.
axios.get()→ fetches all countriesfields=name→ reduces payload size- Returns the cleaned
datafor 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,
})
}
This hook returns cached country data and keeps it fresh for 10 minutes.
queryKey: ensures caching uniquenessqueryFn: executes the API functionstaleTime: 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;
This hook fetches random images from Unsplash, caches them, and returns only the usable URLs.
fetchUnsplashImages()→ fetches random images- Returns
img.urls.regularto simplify image handlinguseImages(): uses React Query for caching and retry strategiesqueryKey: 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;
Zustand manages search input, filter type (city/country), validation, and filtered tour results.
search→ User search textfilterSearch→"all" | "city" | "country"setSearch+setFilter→ Update stategetFilteredTours()→ Returns filtered tours based on destinationisValid()→ 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;
This component connects:
- React Query → country data
- Zustand → search, filters, validation
TourCard → display layout
useCountries()fetches fresh country data
useTourStore()retrieves search + filtersConditional rendering shows:
- Invalid search
- No tour results
- Tours grid
Everything updates instantly because Zustand is super fast.
Final Result:
🧠 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)