Building a Search Component using React

Building a Search Component using React

I started learning React earlier this year and took a long pause from it because of some personal issues. I am back on it and decided to document some of my learning processes.

This is a personal react learning journal but if you learn from it, awesome😁

Using React hooks, I would be creating a simple country search app(No CSS used). This project is part of the fullstackopen.com exercises.

Configuration

First off, we create our react project using

npx create-react-app dataforcountry

The app name is dataforcountry, and npx is one of the ways to create a react app.

After the react app has been successfully created, we navigate to the folder

cd dataforcountry

In the src folder, we can see some starter files installed for us by react, we can delete the unnecessary ones and leave only index.js and app.js (CSS files are deleted because I do not intend to style this app for now).

Open the app.js file and also delete the content.

We start the app by running

npm start

A blank page shows up because we have deleted most of the startup content.

The index.js file should look like this

index.jpg

Fetch data from API

We have to fetch data for the app from an API using axios, axios is installed by running

npm install axios

After installation, open the APP.js file, import react, useState and useEffect from react, import axios from axios

import react, {useState, useEffect} from 'react'
import axios from 'axios'

Fetch data from the API using useEffect and axios, for this project we would be using data from [https://restcountries.com/](https://restcountries.com/) API.

const App=()=>{
  useEffect(()=>{
  axios.get(https://restcountries.com/v3.1/all)
  .then(response)=>{
  console.log(response.data)
   }
   },[])
}

Open the console to view the data fetched from the API. This data need to be shown on the page so we create a state using useState hook and initialize it to an empty array.

const [countries, setCountries] = useState([])

Then we go back to the useEffect to update the state to the data gotten from the API

const App=()=>{
  useEffect(()=>{
  axios.get(https://restcountries.com/v3.1/all)
  .then(response)=>{
  setCountries(response.data)
   }
   },[])
}

if you log countries to the console, you would see an object with over 250 countries and their details. we have gotten our data, so now we need to display them on our app and not on the console.

Display data

We have to loop over the countries array to display them and we do this using the map method. The map method would return the individual country.

const App=()=>{
  useEffect(()=>{
  axios.get(https://restcountries.com/v3.1/all)
  .then(response)=>{
  setCountries(response.data)
   }
   },[])

 return(
   <>
    {countries.map(country=><h1>{country.name.common}</h1>)}
   </>
  )
}

Make sure to thoroughly go through and API data so as to know what to render, the name of all countries can be found in a nested object. The app would throw an error if you just use country.name

download.png

Now the page displays all country names from the API.

Create Individual Country component

React is known for its's reusable components and frankly creating different components for your project also makes it neater.

We want our app to also display the country's capital, population, languages, flag, and some other things.

In the src folder, we create a folder named Components and create a file called Country.js.

In the App.js file, as we map through countries, instead of returning <h1>{country.name.common}</h1> we return <Country/>

We are going to get an error compiling because we haven't exported Country.js and we haven't imported it into our App.js, so let's go ahead and do that.

In App.js we import Country,

import Country from './Components/Country'

Open the Country.js file, import react as we did in the app.js, write the Country component as a function, and export it.

import react from 'react'

  const Country = () =>{
  return()

}
export default Country

For us to access the data from the API into our Country component, we have to pass them as a prop.

Since we are mapping through the array, we have to add a unique key. Usually, the APIs provide IDs that can be used as keys, There's no unique ID on this so I'll use country.cca3.

<Country key={country.cca3} country={country}/>

country can now be accessed from the Country Component and we can add as much data as we want.

import react from 'react'

  const Country = ({country}) =>{
  return(
    <>
      <h2>{country.name.common}</h2>
      <p>Capital: {country.capital}</p>
      <p>Population: {country.population}</p>
      <p>Continent: {country.continents}</p>
      <p>Timezone:{country.timezones} </p>
      <img src={country.flags.png} alt='' />
    </>

   )

}
export default Country

Hiccups

One of the challenges I faced while working on displaying data from the endpoint was accessing the language object, the language object had different numbers for each country and it was an object inside of an object so doing country.languages could not work, and I also could not do country.languages[a specific language] because that brought out undefined for a lot of countries.

download.png

I tried to use Object.values(country.languages) to get the values of the languages object and then used forEach to loop and return them but this kept throwing an error, in fact, I got stuck at this point.

I tried to map through country.languages and got an error that it wasn't a function even though when I logged country.languages to the console I got an object with each country's language displaying.

I googled implementing loop on components to see other methods I could use and came across this article that helped me in solving my problem and accessing languages for all countries.

Looping through nested objects in react

In Country.js, we create a variable called language and assign it to an empty array, then we loop through country.languages and push it to the empty variable. language is then used in the component.

import React from 'react';

const Country = ({ country }) => {
  const languages = country.languages;

  const language = [];

  for (const lang in languages) {
    language.push(<li key={language.length + 1}>{languages[lang]}</li>);
  }

  return (
    <>
      <h2>{country.name.common}</h2>
      <p>Capital: {country.capital}</p>
      <p>Population: {country.population}</p>
      <p>Continent: {country.continents}</p>
      <p>Timezone:{country.timezones} </p>
      <h3>Languages</h3>
      <ul>{language}</ul>
      <img src={country.flags.png} alt='' />
    </>
  );
};

export default Country;

Remember we have to give a unique key as we loop through and since there is no ID provided we can use the length of the array + 1, which would generate a unique number after each loop.

Search Component

We create a Search component just like the Country Component.

In App.js, we add the component just above the Country component, we pass it two props, onChange and value.

We also create a new state to hold the input of the search.

const [search, setSearch] = useState('')
<Search onChange={handleSearch} value={search}/>

The handleSearch function handles the search input and it is passed into the onChange.

const handleSearch = (e)=>{
   setSearch(e.target.value)
}

In the Search Component, we pass the props down

import React from 'react';

const Search = ({ onChange, value }) => {
  return (
    <>
      <div>
        find countries <input onChange={onChange} value={value} type='search' />
      </div>
    </>
  );
};

export default Search;

On the display page, we can see the search input field, we can also type inside but nothing happens. Now we need to make our component respond to our search queries. This can be done by using the filter method.

In App.js,

  const filteredSearch = countries.filter((country) =>
    country.name.common.toLowerCase().includes(search.toLowerCase())
  );
  console.log(filteredSearch);

This filters the countries' names to display only those countries that their name are part of the search query being keyed in. Even with this code, our search feature doesn't still work and that is because we need to map through this filteredSearch instead of countries.

In App.js, we change

 {countries.map(country=><Country key={country.cca3} country={country} />)}

to

 {filteredSearch.map(country=><Country key={country.cca3} country={country} />)}

Now, this would return searched countries with all their details.

Conditional rendering

The Search feature works but now we want to add some conditions to control the display.

The first one is - If there are too many (over 10) countries that match the query, then the user is prompted to make their query more specific.

19b1.png

The second one is - If there are ten or fewer countries, but more than one, then only country names matching the query are shown

19b2.png

We have to create another component called CountryName to render only country names on the app.

The third is - When there is only one country matching the query, then the basic data of the country, its flag, and the languages, are shown

19b3.png

We also want the data to be shown only when a search is being made, therefore the page should have just the search input when it loads.

To achieve all these, we are going to use a chained ternary expression.

We check to see if the search state is empty, if true we return an empty array.

{search === ' ' ? [ ] :  }

then we execute our first condition on the list, checking to see if the length of filtered search is above 10 — if true, it returns a string telling user to use another filter.

{search === ' ' ? [ ] 
   :  filteredSearch.length > 10 
  ? <p>Too many searches, specify another filter</p>
  }

This is an incomplete ternary expression and would result in an error, but we aren't done, we still have to fix our second and third conditions.

{search === ' ' ? [ ] 
  :  filteredSearch.length > 10 
  ? <p>Too many searches, specify another filter</p>
  : filteredSearch.map((country)=>
       filteredSearch.length <= 10 && filteredSearch.length > 1 
       ? <CountryName key={country.cca3} country={country} />
        :  <Country key={country.cca3} country={country} />
       )
 }

Simply put, if the search state is empty render nothing on the page, otherwise if the number of countries displayed after a search is more than 10 render a text telling the user to specify another filter — if it is less than 10 but greater than 1 then display country names only, if it is just one then display the country name with its details.

Other features would be added to this project later, If there are better or simpler ways to achieve this please drop a comment, also pardon all grammatical errors 😃

Link to repo