4

I added a search function for my table in React to filter out items fetched from an external webservice. Since I don't want to call the API every time you search for project I figured I'd assign the data retrieved to two different useState hooks.

One to hold the complete dataset and the other to contain the filtered items based on the search.

Could I write cleaner code without using 2 hooks? Any side effects of the way the code is handling this?

Any input is appreciated.

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Table from '@mui/material/Table';
import TableHead from '@mui/material/TableHead';
import TableBody from '@mui/material/TableBody';
import TableRow from '@mui/material/TableRow';
import TableCell from '@mui/material/TableCell';
import DeleteIcon from '@mui/icons-material/Delete';
import TextField from '@mui/material/TextField';

export default function ShowProject() {
    const [data, setData] = useState([]);
    const [filter, setFilter] = useState([])

    useEffect(() => {
        const fetchData = async () => {
            const result = await axios(
                'http://127.0.0.1:5000/pr'
            );
            setData(result.data);
            setFilter(result.data);
        }
        fetchData()
        
    }, []);

    const requestSearch = (searchedVal) => {
        const filteredRows = data.filter((row) => {
            return row.customer.toString().toLowerCase().includes(searchedVal.toString().toLowerCase());
        });
        if (searchedVal.length < 1) {
            setFilter(data)
        }
        else {
            setFilter(filteredRows)
        }
      };
    
    return (
        <div>
            <div>
                <TextField onChange={(e) => requestSearch(e.target.value)} />
                <Table>
                    <TableHead>
                        <TableRow>
                            <TableCell>Project</TableCell>
                            <TableCell>Code</TableCell>
                            <TableCell>Customer</TableCell>
                            <TableCell></TableCell>
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {filter.map(item => (
                            <TableRow key={item.db_id}>
                                <TableCell>{item.project_name}</TableCell>
                                <TableCell>{item.project_code}</TableCell>
                                <TableCell>{item.customer}</TableCell>
                                <TableCell><DeleteIcon /></TableCell>
                            </TableRow>
                        ))}
                    </TableBody>
                </Table>
            </div>
        </div>
    )
}
2
  • Two considerations, before even looking at your code-- 1) How frequently is the data updated? Should the user expect to see updates to the data in the server reflected in their browser? Because you have cached the data fetch, so they won't get fresh data unless the refresh the page or remount the component... 2) How large can the dataset be? If it can be very large, you'd probably be better served by implementing pagination or lazy-loading and letting the server handle sorting/searching/etc rather than doing it in memory on the client machine. Commented Oct 6, 2021 at 17:28
  • 1) The dataset is updated approximately once a month, I do expect them to see a live update once I add a submit form allowing them to add new items to the db. 2) The dataset will grow over the years but within the first year i'm expecting around 200 objects so I would not consider it as a big dataset, no.
    – Alecbalec
    Commented Oct 6, 2021 at 17:38

1 Answer 1

4

I don't see any particular reason to use the filter state variable. You're scanning data and assigning the filtered result to filter every time your TextField changes, so why not just do the filtering directly in the JSX and instead store the query text as state? More concretely, something like the following:

  const [searchedVal, setSearchedVal] = useState("");

  return (
    <div>
      <div>
        {/* simply set the query text here instead of triggering requestSearch */}
        <TextField onChange={(e) => setSearchedVal(e.target.value)} />
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>Project</TableCell>
              <TableCell>Code</TableCell>
              <TableCell>Customer</TableCell>
              <TableCell></TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {data
              .filter((row) =>
                // note that I've incorporated the searchedVal length check here
                !searchedVal.length || row.customer
                  .toString()
                  .toLowerCase()
                  .includes(searchedVal.toString().toLowerCase()) 
              )
              .map((item) => (
                <TableRow key={item.db_id}>
                  <TableCell>{item.project_name}</TableCell>
                  <TableCell>{item.project_code}</TableCell>
                  <TableCell>{item.customer}</TableCell>
                  <TableCell>
                    <DeleteIcon />
                  </TableCell>
                </TableRow>
              ))}
          </TableBody>
        </Table>
      </div>
    </div>
  );
5
  • Thanks for the input, I'll adjust the code. Would this approach also improve the performance (imagine the dataset being quite large)?
    – Alecbalec
    Commented Oct 6, 2021 at 17:46
  • 1
    Not particularly. This is just a cleaner way to do roughly the same thing you did. As another user noted, holding large datasets on the client is generally going to cause problems. You're better off doing pagination/lazy loading on the client side and handling filtering on the server side. In order to accomplish this, the server would need to expose an API that accepts a query string and results range (e.g. results 1-100, 101-200, etc.) and returns the relevant rows. On the client, you would either want a submit action or a slight delay between user input and API calls for performance's sake. Commented Oct 6, 2021 at 18:02
  • Thanks for clarifying, I'll keep that in mind for further development. So basically whenever the text input is changed it would trigger an API call with the current query string?
    – Alecbalec
    Commented Oct 6, 2021 at 18:24
  • That's the idea, yes, with the caveat that you probably don't want to trigger the API call, which may be expensive, immediately on every keystroke. Instead, as I mentioned above, either give the user a submit button, or insert a slight delay after keystrokes before calling the API. useEffect and setTimeout are good candidates for the delay tactic (see e.g. this answer for the general idea). The idea is the effect triggers on every keystroke and sets a timeout to call the API, but the timeout gets cleaned up if the effect is triggered again Commented Oct 6, 2021 at 19:17
  • Got it. Added a expand/collapse button that fetches the payload that works fine for now. No I'm working on an auto-refresh of the table component when a post request is made from another function component. Thanks for the input so far.
    – Alecbalec
    Commented Oct 9, 2021 at 10:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.