How to Optimize Search with React

Build a React.js Job Finder Application

This is a job finder application that I have built with ReactJs. And I use JSearch API to gather job postings and other data.

And here is searching page. And the search keywords are based on the optional parameters of Jsearch API.

code snippets

jobSearchApi.js - this is the fetching data query using @reduxjs/toolkit

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

// key need to move to env file when production mode
const apiKey = process.env.REACT_APP_RAPID_API_KEY;

export const jobSearchApi = createApi({
  reducerPath: 'jobSearchApi',
  baseQuery: fetchBaseQuery({
    baseUrl: 'https://jsearch.p.rapidapi.com/',
    prepareHeaders: (headers) => {
      headers.set('X-RapidAPI-Key', apiKey);

      return headers;
    },
  }),

  endpoints: (builder) => ({
    // search jobs
    getSearch: builder.query({
      query: ({ searchTerm, page, employmentTypes, jobRequirements, categories, employers, datePosted = '3days', numPages = 1 }) => {
        const params = {
          query: searchTerm,
          page,
          employment_types: employmentTypes,
          job_requirements: jobRequirements,
          categories,
          employers,
          date_posted: datePosted,
          num_pages: numPages,
        };

        // create a new object with only the keys and values that have truthy values
        const filteredParams = Object.keys(params)
          .filter((key) => params[key])
          .reduce((obj, key) => {
            obj[key] = params[key];
            return obj;
          }, {});

        // the URLSearchParams constructor is used to create a new URLSearchParams object
        const queryString = new URLSearchParams(filteredParams).toString();

        return `/search?${queryString}`;
      },
    }),
    // search filter
    getSearchFilter: builder.query({
      query: (searchTerm) => `/search-filters?query=${searchTerm}`,
    }),
    // get estimated-salary
    // required for jobtitle, location, radius
    getEstimatedSalaries: builder.query({
      query: ({ jobTitle, location, radius }) => `/estimated-salary?job_title=${jobTitle}&location=${location}&radius=${radius}`,
    }),
    // Get Job with ID
    getJobWithID: builder.query({
      query: ({ jobID }) => `/job-details?job_id=${jobID}`,
    }),
  }),
});

export const {
  useGetSearchQuery,
  useGetSearchFilterQuery,
  useGetEstimatedSalariesQuery,
  useGetJobWithIDQuery,
} = jobSearchApi;

currentSearchOrFilter.js - this is the reducers that are used on this project.

import { createSlice } from '@reduxjs/toolkit';

export const currentSearch = createSlice({
  name: 'currentSearch',
  initialState: {
    searchTerm: 'google',
    page: 1,
    employmentTypesArray: [],
    jobRequirementArray: [],
    categoriesArray: [],
    salaryBounds: [],
    datePosted: 'today',
  },
  reducers: {
    selectCategoriesAdd: (state, action) => {
      state.categoriesArray.push(action.payload);
    },
    selectCategoriesRemove: (state, action) => {
      state.categoriesArray = state.categoriesArray.filter((item) => item !== action.payload);
    },
    selectTypeAdd: (state, action) => {
      state.employmentTypesArray.push(action.payload);
    },
    selectTypeRemove: (state, action) => {
      state.employmentTypesArray = state.employmentTypesArray.filter((item) => item !== action.payload);
    },
    selectJobRequirementAdd: (state, action) => {
      state.jobRequirementArray.push(action.payload);
    },
    selectJobRequirementRemove: (state, action) => {
      state.jobRequirementArray = state.jobRequirementArray.filter((item) => item !== action.payload);
    },
    selectSalaryAdd: (state, action) => {
      state.salaryBounds.push(action.payload);
    },
    selectSalaryRemove: (state, action) => {
      state.salaryBounds = state.salaryBounds.filter((item) => item.min !== action.payload.min);
    },
    searchJob: (state, action) => {
      state.searchTerm = action.payload;
    },
    selectDatePosted: (state, action) => {
      state.datePosted = action.payload;
    },
  },
});

export const {
  selectCategoriesAdd,
  selectCategoriesRemove,
  selectTypeAdd,
  selectTypeRemove,
  selectJobRequirementAdd,
  selectJobRequirementRemove,
  selectSalaryAdd,
  selectSalaryRemove,
  searchJob,
  selectDatePosted,
} = currentSearch.actions;
export default currentSearch.reducer;

SearchJobForm.jsx - this is the search form. The following is a piece of code.

import React, { useState } from 'react';

import { useDispatch } from 'react-redux';
import { searchJob } from '../features/currentSearchOrFilter';
import { search, pin, briefCase } from '../assets/icons';

const SearchJobForm = () => {
  const initialValues = {
    jobTitle: '',
    location: '',
    jobType: '',
  };
  const [values, setValues] = useState(initialValues);
  const dispatch = useDispatch();

  const handleInputChange = (e) => {
    const { name, value } = e.target;

    setValues({
      ...values,
      [name]: value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    // contact query in three search iunput
    const contactQuery = `${values.jobTitle}${values.location ? `${values.location},` : ''}${values.jobType ? `${values.jobType},` : ''}`;
    // remove the space between three input
    const query = contactQuery.replace(/\s/g, '');
    if (query.length) {
      dispatch(searchJob(query));
    }
  };

JobSearch.jsx - on the search page, we used pagination.

import React, { useState } from 'react';
import { useSelector } from 'react-redux';

import { Error, Loader, JobCardSearchPage, SearchJobForm, JobsSortForm, SearchSideBarForm, Pagination, JobAlert } from '../components';
import { useGetSearchQuery, useGetSearchFilterQuery } from '../redux/services/jobSearchApi';
import { showToday, filterSalaryRange, sortJobsByCompanyName, sortJobsByDatePosted } from '../utils';

const JobSearch = () => {
  const [page, setPage] = useState(1);
  const [sortby, setSortby] = useState('date-posted-asc');
  const { searchTerm, employmentTypesArray, jobRequirementArray, salaryBounds, categoriesArray } = useSelector((state) => state.currentSearch);
  // sidebar search filter array
  const categories = categoriesArray?.join();
  const employmentTypes = employmentTypesArray?.join();
  const jobRequirements = jobRequirementArray?.join();

  // search keywords
  const { data, error, isLoading } = useGetSearchQuery({ searchTerm, page, employmentTypes, jobRequirements, categories });
  const jobs = data?.data;
  console.log('jobs', jobs);
  // search filter
  const { data: filterData, isLoading: isLoadingFilterData } = useGetSearchFilterQuery(searchTerm);
  const today = showToday();
  if (isLoading && isLoadingFilterData) return <Loader />;
  if (error) return <Error />;

  const employmentTypeData = filterData?.data?.employment_types;
  const jobRequirementData = filterData?.data?.job_requirements;
  const categoriesData = filterData?.data?.categories;

  /* calculate the total job number based on employment type and pass to job search */
  const reduceCount = employmentTypeData?.reduce((a, b) => ({ est_count: a.est_count + b.est_count }));
  const totalCount = reduceCount.est_count;
  const jobsFoundMessage = parseInt(totalCount, 10) > 0 ? `${totalCount} Jobs` : 'No Jobs Found';
  const totalPages = Math.floor(totalCount / 10) + 1;

  let displayData = salaryBounds.length ? filterSalaryRange(jobs, salaryBounds) : jobs;

  if (jobs?.length) {
    if (sortby === 'company') {
      displayData = sortJobsByCompanyName({ jobs });
    } else if (sortby === 'date-posted-asc') {
      displayData = sortJobsByDatePosted({ jobs, ASC: true });
    } else if (sortby === 'date-posted-desc') {
      displayData = sortJobsByDatePosted({ jobs, ASC: false });
    }
  }

  if (displayData?.length > 4) {
    displayData = displayData.slice(0, 4);
  }

  return (
    <div className="bg-secondary px-4 sm:px-20 dark:bg-black_BG">
      <h1 className="text-2xl font-bold text-black_1 pt-12 dark:text-white">Let’s find your dream job</h1>
      <h2 className=" text-natural py-1">{today}</h2>
      <SearchJobForm />
      <div className="sm:flex justify-between gap-5">
        <div className="flex-col rounded-sm lg:min-w-max">
          <JobAlert />
          <div className="hidden sm:block">
            <SearchSideBarForm employmentTypeData={employmentTypeData} jobRequirementData={jobRequirementData} categoriesData={categoriesData} />
          </div>
        </div>
        <div className="flex flex-col">
          {/* Jobs header */}
          <div className="flex justify-between items-center">
            {/* Number of jobs found message  */}
            <div className="text-sm my-2.5">
              <p className="text-natural_3">Showing: <span className="text-black_1 font-bold dark:text-white">{jobsFoundMessage}</span></p>
            </div>
            {/* Sort */}
            <JobsSortForm currentSortby={sortby} setSortby={setSortby} />
          </div>
          <div>
            {displayData?.map((job) => (
              <JobCardSearchPage job={job} key={job.job_id} />
            ))}
          </div>
          {/* Pagination */}
          <Pagination className="mt-4 mb-8" currentPage={page} setPage={setPage} totalPages={totalPages} />
        </div>
      </div>
    </div>
  );
};

export default JobSearch;

Discuss searching points from API.

How can we optimize our project and reduce the search time?

A lot of the time, it depends on where you're getting your data from. In this case, I've optimized your API calls by paginating them (which already significantly helped reduce the API call). It's not so much about a different sorting / search algorithm here, because my data depends on an external source. In an interview, some other concepts I might touch upon would be abstracting the api layer from my app entirely and building my own fetching backend service .. for example, instead of calling the API directly from my frontend app, I might develop my own backend that caches the API calls / limits the results (such as the case of using a graphql server) using a service like expressjs or nestjs (not nextjs) and then from my frontend app calling my own backend service instead of the api directly -- meaning I have more control over how quickly the data might reach your frontend app, and you can also revalidate and update the requests on the backend without slowing down your frontend app.

In the case of this API, and because of the way it's built, I might look deeper into the caching system that redux toolkit uses and see how I might optimize that -- meaning that I could cache the initial api call for longer, store it somewhere, etc. But it's not so much about implementing another algorithm because my data source is external, and I can't control it other than using the queries they allow me to use.

Optimization

You can perform searching on the frontend (if the data isn't too much) or on backend (best approach when you have lots of data). Both have pros & cons. On the frontend, the results will be quick. But on the backend it will take time to fetch and display the result.

There is one way to optimize my searching. In my application, to ensure that we're not doing too much of calls to the backend while a user is searching, an optimized debouncing technique is used as a best practice. Here is an article on it: https://javascript.plainenglish.io/how-to-create-an-optimized-real-time-search-with-react-6dd4026f4fa9

The goal is simple: Wait a few seconds before making the actual API call. It will cost you (server) a lot of bucks if you fire an API call as your user types in.

The same approach can be considered for other things: Let's say for those filter checkboxes. Users don't care what's happening beneath. They just do what they want to do. Suppose someone just starts checking boxes for fun. That can potentially break the application if not handled properly.