React Generic Components

May 16, 2021


Banner


Recently I had to do some work in which I had to build a few search bars that would trigger some query when the user typed some input. Then when the results were ready, they had to be displayed in a list. I want to recreate this problem in a simplified way to showcase how this could be approached using generic types and component composition in React. I won’t focus on making it fancy or look pretty. I will skip certain things, specifically:

  • I won’t debounce the user entering input
  • I won’t worry about caching server responses
  • I won’t handle loading states or error states
  • I won’t handle the scenario for no search results
  • I will assume the endpoints always return a nonempty array

I want to focus on the topic of this article, which is using generics and component composition to build better reusable components. I will cover some great ways of representing and handling async data in later posts.

Lets get started

Let’s say we have 2 endpoints that allow us to get some information that we can search on.

This is how our types look like

// Newtype implementation from newtype-ts library import { NonNegativeInteger } from "newtype-ts/lib/NonZero" type CompanyId = NonNegativeInteger interface Company { id: CompanyId name: string } type JobId = NonNegativeInteger interface Job { id: JobId title: string description: string }

As you can see from the types above, Job and Company have different attributes so the search results must display different information for each. Regardless, the search functionality would be practically the same

User types some text -> A query is triggered -> Some results come back

The not very reusable approach

I will start building a search that won’t be very flexible for both Job and Company searches. Then we’ll extract out what changes and make something that can be composable and reusable for any other kind of search bar.

import React, { useState, useEffect } from 'react' // Company search const fetchCompanies = (term: string): Promise<Company[]> => fetch(`"https://composition.free.beeceptor.com/companies"?name=${term}`) .then(res => res.json()) const CompanySearch = () => { const [term, setTerm] = useState('') const [data, setData] = useState([]) useEffect(() => { if (term === '') setData([]) }, [term]) const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { setTerm(event.target.value) const companies = await fetchCompanies(event.target.value) setData(companies) } return ( <React.Fragment> <label for="company-search">Search Companies</label> <input type="search" id="company-search" onChange={handleChange}> <ul> {data.map(company => ( <li key={`company-${id}`}>{company.name}</li> ))} </ul> </React.Fragment> ) } import React, { useState, useEffect } from 'react' // Job Search const fetchJobs = (term: string): Promise<Job[]> => fetch(`"https://composition.free.beeceptor.com/jobs"?title=${term}`) .then(res => res.json()) const JobSearch = () => { const [term, setTerm] = useState('') const [data, setData] = useState([]) useEffect(() => { if (term === '') setData([]) }, [term]) const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { setTerm(event.target.value) const jobs = await fetchJobs(event.target.value) setData(jobs) } return ( <React.Fragment> <label for="job-search">Search Jobs</label> <input type="search" id="job-search" onChange={handleChange}> <ul> data.map(job => ( <li key={`job-${job.id}`}> <p>{job.title}</p> <p>{job.description}</p> </li> )) </ul> </React.Fragment> ) }

Both components mostly do the same thing except for where they fetch the data from and how they display the results. When rendered, they will look like this:

const Layout = () => { return ( <React.Fragment> <SearchCompanies /> <SearchJobs /> </React.Fragment> ) }

Let’s try to make this composable by using eneric types and component composition.

Composition and generics

If you don’t know what I am talking about, you can check the React docs and also watch the video by Michael Jackson (not the singer) below, where he talks about state management using component composition and lifting components.

Alright, here is how we could rewrite the search to make it a reusable component using generic types and composition.

import React, { useState, useEffect } from 'react' interface Props<T> { id: string label: string children: (item: T) => React.ReactNode fetchItems: (term: string) => Promise<T[]> } const Search = <T,>({ id, label, fetchItems, children }: Props<T>) => { const [term, searchTerm] = useState('') const [data, setData] = useState([]) useEffect(() => { if (term === '') setData([]) }, [term]) const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => { setTerm(event.target.value) const items = await fetchItems(event.target.value) setData(items) } return ( <React.Fragment> <label for={id}>{label}</label> <input type="search" id={id} onChange={handleChange}> <ul> data.map(children) </ul> </React.Fragment> ) }

Notice how on our Search component we are defining a generic type T which gets passed to Props<T>. When we pass fetchCompanies to Search, T will become our Company type. Here is how the compiler would resolve our types

declare function fetchCompanies(term: string): Promise<Company[]> // This will cause the compiler to resolve props as interface Props { id: string label: string children: (item: Company) => React.ReactNode fetchItems: (term: string) => Promise<Company[]> }

The composition part

The children prop is a mechanism React provides to do component composition. Whatever you place inside the open and closing brackes of a component will become the children props of that component. Knowing that, we could use it to compose our Search component with how we want the search results to display for Company and Job types

const Layout = () => { return ( <React.Fragment> <Search id="companies" label="Search Companies" fetchItems={fetchCompanies} > {/* Composition happening here */} {company => <li key={`company-${company.id}`}>{company.name}</li>} </Search> <Search id="jobs" label="Search Jobs" fetchItems={fetchJobs}> {/* Composition happening here */} {job => ( <li key={`job-${job.id}`}> <p>{job.title}</p> <p>{job.description}</p> </li> )} </Search> </React.Fragment> ) }

One cool thing about using component composition like this, is that it lifts the search results to the same level where the Search component is defined and it help us prevent prop drilling. Now we can have different onClick events for Jobs vs Companies like this

import React from "react" const Layout = () => { const checkoutCompany = (company: Company) => { console.log("I am going to check this company") } const applyForJob = (job: Job) => { console.log("I am going to apply for this job") } return ( <React.Fragment> {/* Company Search */} <Search id="companies" label="Search Companies" fetchItems={fetchCompanies} > {company => ( <li key={`company-${company.id}`} onClick={() => checkoutCompany(company)} > {company.name} </li> )} </Search> {/* Job Search */} <Search id="jobs" label="Search Jobs" fetchItems={fetchJobs}> {job => ( <li key={`job-${job.id}`} onClick={() => applyForJob(job)}> <p>{job.title}</p> <p>{job.description}</p> </li> )} </Search> </React.Fragment> ) }

With this pattern, we could easily have different search bars that search for different entities and even have them execute different actions when you click them depending on the context. As shown in the example above, we could easily replace the onClick functions and have the search bar results act differently depending on where you use them.


Follow me on twitter Nelson Ochoa
We are what we repeatedly do. Excellence then, is not an act, but a habit. - Aristotle