skip to Main Content

I have a component Body.jsx, it contained logic of fetching data from an API and filtering as well.
I tried to create a custom hook useResData which separates out logic of fetching data as shown-

export const Body = () => {
  const resDataList = useResData();

  const [filteredList, setFiltererdList] = useState(resDataList);
  const [inputText, setInputText] = useState('');

  const filterList = () => {
    let filteredList = resDataList.filter((item) => item.info.avgRating > 4);
    setFiltererdList(filteredList);
  }

  if (resDataList.length === 0) return <Shimmer />;
        
  return (
    <div className="Body"> 
      <div className="search">
        <input
          className="search-input"
          value={inputText}
          onChange={(e) => {
            setInputText(e.target.value)
          }}
        />
        <button
          className="search-btn" 
          onClick={() => {
            let list = resDataList.filter((data) => data.info.name.toLowerCase().includes(inputText))
            setFiltererdList(list);
          }}
        >
          Search
        </button>
      </div>
      <div className="filter-btn">
        <button onClick={filterList}>Top Rated Restaurants</button>
      </div>
      <div className="cardContainer">
        {filteredList.map((item) =>
          <Link
            to={'restaurants/' + item.info.id}
            key={item.info.id}
          >
            <Card resData={item?.info} key={item.info.id} />
          </Link>
        )}
      </div>
    </div>
  )
}

And custom hook is –

const useResData = () => {
  const [resDataList, setResDataList] = useState([]);

  useEffect(() => {
    getData = async () => {
      let res = await fetch(
        "https://www.swiggy.com/dapi/restaurants/list/v5?lat=28.65200&lng=77.16630&is-seo-homepage-enabled=true&page_type=DESKTOP_WEB_LISTING"
      );
      let data = await res.json();
      setResDataList(data?.data?.cards?.[4]?.card?.card?.gridElements?.infoWithStyle?.restaurants)
    }
    getData();
  }, [])

  return resDataList;
}

export default useResData;

Now the issue is when resDataList is populated in Body.tsx, it does not populate filteredList. I know that for filteredList to be populated I need to use setFiltererdList, but I read somewhere that each time a component renders it creates a separate copy of state variable, by this logic const [filteredList, setFiltererdList] = useState(resDataList); should initialize filteredList with resDataList each time Body component is rendered.

2

Answers


  1. The initial value of useState is only used on the first render, at which point resDataList is still empty.

    You could instead store a filter in the state and avoid redundant state by directly computing the filtered result during rendering.

    const [filter, setFilter] = useState(() => () => true);
    const filteredList = resDataList.filter(filter);
    
    const filterList= () => setFilter(() => item => item.info.avgRating > 4);
    // and so on
    
    Login or Signup to reply.
  2. Now the issue is when resDataList is populated in Body.tsx, it does
    not populate filteredList.

    This is because the useEffect hook runs at the end of the render cycle, so the Body component was passed the initial, unpopulated resDataList state value which is the empty array.

    I read somewhere that each time a component renders it creates a
    separate copy of state variable, by this logic const [filteredList, setFiltererdList] = useState(resDataList); should initialize
    filteredList with resDataList each time Body component is
    rendered.

    This is not true. React components mount and the state is initialized exactly once. After that it’s up to the component to update and maintain its own state as necessary.

    The trivial solution is to use a useEffect hook with a dependency on the returned resDataList value and enqueue state updates to synchronize the local filteredList state.

    Example:

    const resDataList = useResData();
    
    const [filteredList, setFiltererdList] = useState(resDataList);
    
    useEffect(() => {
      setFilteredList(resDataList);
    }, [resDataList]);
    

    Be aware that as-is this is a general React anti-pattern and should be avoided, e.g. copying the derived filtered value from the "useResData" state into Body. Just about any time you catch yourself having coded a useStateuseEffect couplet you should actually use the useMemo hook to compute and provide a memoized/stable derived/computed value to the component.

    Instead of waiting for the user to click a button to filter the results, just compute and return the correct data to be rendered. Use the inputText to also filter further inline when rendering, or if you feel this is computationally expensive, you can just include this in the memoization.

    const resDataList = useResData();
    
    const filteredList = useMemo(() => {
      return resDataList.filter((item) => item.info.avgRating > 4);
    }, [resDataList]);
    
    export const Body = () => {
      const resDataList = useResData();
    
      const [inputText, setInputText] = useState('');
    
      const filteredList = useMemo(() => {
        return resDataList.filter((item) => item.info.avgRating > 4);
      }, [resDataList]);
    
      if (!resDataList.length) return <Shimmer />;
            
      return (
        <div className="Body"> 
          <div className="search">
            <input
              className="search-input"
              value={inputText}
              onChange={(e) => {
                setInputText(e.target.value)
              }}
            />
          </div>
          <div className="cardContainer">
            {filteredList
              .filter(
                item => item.info.name.toLowerCase().includes(
                  inputText.toLowerCase()
                ))
              )
              .map((item) => (
                <Link
                  key={item.info.id}
                  to={'restaurants/' + item.info.id}
                >
                  <Card resData={item?.info} key={item.info.id} />
                </Link>
              ))
            }
          </div>
        </div>
      )
    }
    

    or

    export const Body = () => {
      const resDataList = useResData();
    
      const [inputText, setInputText] = useState('');
    
      const filteredList = useMemo(() => {
        return resDataList.filter((item) => {
          const { info: { avgRating, name } } = item;
          return avgRating > 4 && name.toLowerCase().includes(
            inputText.toLowerCase()
          ));
        });
      }, [inputText, resDataList]);
    
      if (!resDataList.length) return <Shimmer />;
            
      return (
        <div className="Body"> 
          <div className="search">
            <input
              className="search-input"
              value={inputText}
              onChange={(e) => {
                setInputText(e.target.value)
              }}
            />
          </div>
          <div className="cardContainer">
            {filteredList.map((item) => (
              <Link
                key={item.info.id}
                to={'restaurants/' + item.info.id}
              >
                <Card key={item.info.id} resData={item.info} />
              </Link>
            ))}
          </div>
        </div>
      )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search