javascriptreactjsserver-side-renderingreact-contextclient-side

What is the best scalable way to implement a ContextProvider to fetch and filter data


I have been working on a project that requires fetching a bunch of data and provides multiple ways to filter/search the data, and I though about keeping filtering on the server-side considering I'll be using pagination.

I started using React Context recently and tried an approach that didn't work.

here's the custom React hook I'm using to fetch data

import { useState, useEffect } from 'react';

const useFetch = (url, url_params) => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [refresh, setRefresh] = useState(0);

    if (url_params) {
        url = url + '?' + url_params.toString();
    }

    const refreshData = () => {
        setRefresh((prev) => prev + 1);
    }

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                const result = await response.json();
                setData(result);
            } catch (error) {
                setError(error);
            } finally {
                setLoading(false);
            }
        };

        fetchData();
    }, [refresh, url]);

    return { data, loading, error, refreshData };
};

export default useFetch;

and here is the contextProvider:

import { createContext, useState, useEffect, useContext } from "react";
import { listing_url } from "../../Hooks/api_endpoints";
import useFetch from "../../Hooks/useFetch";
import { useSearchParams } from "react-router-dom";

export const HouseContext = createContext();

export const HouseProvider = ({ children }) => {
    const [selectedHouse, setSelectedHouse] = useState(null);
    // I have tried multiple approaches for searchParams (one is search params and the other is testParams
    const [searchParams, setSearchparams] = useState({
        address__icontains: '',
        apartment_no__icontains: '',
        block__iexact: '',
        currency__iexact: '',
        floor__iexact: '',
        gross__gte: '',
        gross__lte: '',
        net__gte: '',
        net__lte: '',
        num_of_floors__iexact: '',
        num_of_rooms__iexact: '',
        owner__icontains: '',
        owner_phone_num__icontains: '',
        price__gte: '',
        price__lte: '',
        status__iexact: '',
        view__icontains: '',
    });

    // second attempt at implementing searchParams
    const testParams = new URLSearchParams({
        address__icontains: '',
        apartment_no__icontains: '',
        block__iexact: '',
        currency__iexact: '',
        floor__iexact: '',
        gross__gte: '',
        gross__lte: '',
        net__gte: '',
        net__lte: '',
        num_of_floors__iexact: '',
        num_of_rooms__iexact: '',
        owner__icontains: '',
        owner_phone_num__icontains: '',
        price__gte: '',
        price__lte: '',
        status__iexact: '',
        view__icontains: '',
    });

    const { data: houses, error: housesError, loading: housesLoading, refreshData: houseRefresh } = useFetch(listing_url, testParams);

    const createHouse = async (house) => {

        const newHouse = await fetch(listing_url, {
            method: "POST",
            body: JSON.stringify(houseData),
        });
        setHouses([...houses, newHouse]);
    }

    // useEffect(() => {
    //     console.log("houses were updated")
    //     console.log(' search params were updated')
    // }, [houses, testParams])

    const updateSearchParams = (key, value) => {
        console.log("firing updateSearchParams")
        houseRefresh();
        testParams.set(key, value);
    }

    return (
        <HouseContext.Provider value={{ houses, selectedHouse, housesError, housesLoading, setSelectedHouse, createHouse, testParams, updateSearchParams }}>
            {children}
        </HouseContext.Provider>
    )
}

and here's how I tried to use the filtering options in my Homepage:

import { HouseContext } from '../../Context/House/House';

// .....

export default function Home() {
// .....

    const { testParams, updateSearchParams } = useContext(HouseContext)

    // ......
    return (
          // ......

          <div className='flex'>
            <span className='text-sm/3 my-auto pr-2 dark:text-white'>floor</span>
            <input type='number' placeholder='floor' min={0} onChange={(e) => { updateSearchParams('floor__iexact', e.target.value) }} className='w-14 rounded border border-black' />
          </div>
    // .....

now I have 2 questions about this:

  1. why is the houses list not getting updated when updateSearchParams is firing?
  2. is this approach valid or should I just resort to fetching the data and then filtering client-side? and if so, how will I do so once pagination is added?

Solution

  • I believe the issue you are running into here is caused by nothing triggering a re-render, hence you are not seeing any changes with houses.
    The easiest way to fix this I believe is the following:

    const [testParams, setTestParams] = useState(new URLSearchParams({ ... rest of the tings })
    

    Then update updateSearchParams to do the following instead of the testParams setting you were doing:

    setTestParams(prev => {
        const newParam = new URLSearchParams(prev);
        newParam.set(key, value);
        return newParam;
    });
    

    This changing should now cause React to trigger a re-render and hopefully show you the updated houses.
    I would also be careful with the onChange you are currently doing, this can cause some issues and a lot of requests being sent.