2

I need to fetch data from API based on key and place the data inside a tablecell. I have tried something like the following but didn't work. It is showing an uncaught error.In that case, I know hooks shouldn't be called inside loops, conditions, or nested functions. Then how I would get the item.id?

Uncaught Error: Rendered more hooks than during the previous render.

My code is:

import React, { useState, useEffect } from 'react';
import {
  Table, TableRow, TableCell, TableHead, TableBody,
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { useEffectAsync } from '../reactHelper';
import { useTranslation } from '../common/components/LocalizationProvider';
import PageLayout from '../common/components/PageLayout';
import SettingsMenu from './components/SettingsMenu';
import CollectionFab from './components/CollectionFab';
import CollectionActions from './components/CollectionActions';
import TableShimmer from '../common/components/TableShimmer';

const useStyles = makeStyles((theme) => ({
  columnAction: {
    width: '1%',
    paddingRight: theme.spacing(1),
  },
}));

const StoppagesPage = () => {
  const classes = useStyles();
  const t = useTranslation();

  const [timestamp, setTimestamp] = useState(Date.now());
  const [items, setItems] = useState([]);
  const [geofences, setGeofences] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    fetch('/api/geofences')
      .then((response) => response.json())
      .then((data) => setGeofences(data))
      .catch((error) => {
        throw error;
      });
  }, []);

  useEffectAsync(async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/stoppages');
      if (response.ok) {
        setItems(await response.json());
      } else {
        throw Error(await response.text());
      }
    } finally {
      setLoading(false);
    }
  }, [timestamp]);

  return (
    <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsStoppages']}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>{t('settingsStoppage')}</TableCell>
            <TableCell>{t('settingsCoordinates')}</TableCell>
            <TableCell>{t('sharedRoutes')}</TableCell>
            <TableCell className={classes.columnAction} />
          </TableRow>
        </TableHead>
        <TableBody>
          {!loading ? items.map((item) => (
            <TableRow key={item.id}>
              <TableCell>{item.name}</TableCell>
              <TableCell>{`Latitude: ${item.latitude}, Longitude: ${item.longitude}`}</TableCell>
              <TableCell>
                {
                  geofences.map((geofence) => geofence.name).join(', ')
                }
              </TableCell>
              <TableCell className={classes.columnAction} padding="none">
                <CollectionActions itemId={item.id} editPath="/settings/stoppage" endpoint="stoppages" setTimestamp={setTimestamp} />
              </TableCell>
            </TableRow>
          )) : (<TableShimmer columns={2} endAction />)}
        </TableBody>
      </Table>
      <CollectionFab editPath="/settings/stoppage" />
    </PageLayout>
  );
};

export default StoppagesPage;
cosmic
  • 53
  • 7
  • "I know hooks shouldn't be called inside loops" So don't do that. Instead, when you get `items`, use `const ids = items.map((item) => item.id);` to get an array of ids, (from [From an array of objects, extract value of a property as array](https://stackoverflow.com/q/19590865/215552)), then use that to create an array of promises (i.e., [Javascript push and wait for async promises in array](https://stackoverflow.com/q/72183740/215552)). – Heretic Monkey Oct 25 '22 at 03:02
  • Does this answer your question? [Javascript push and wait for async promises in array](https://stackoverflow.com/questions/72183740/javascript-push-and-wait-for-async-promises-in-array) – Heretic Monkey Oct 25 '22 at 03:03
  • I got ids. But I am not sure how to proceed next exactly according to suggested link. Can you be more specific on that? – cosmic Oct 25 '22 at 04:55

2 Answers2

1

Refactor the mapped JSX into an actual React component so it can use the useEffect hook (and all other React hooks).

Example:

const Item = ({ item }) => {
  const [newItems, setNewItems] = useState([]);

  useEffect(() => {
    fetch(`/api/newItems?newItemId=${item.id}`)
      .then((response) => response.json())
      .then((data) => setNewItems(data))
      .catch((error) => {
        throw error;
      });
  }, []);

  return (
    <TableRow>
      <TableCell>{item.name}</TableCell>
      <TableCell>{item.latitude}</TableCell>
      <TableCell>{item.longitude}</TableCell>
      <TableCell>
        {newItems.map((newItem) => newItem.name).join(", ")}
      </TableCell>
    <TableRow/>
  );
};

...

const StoppagesPage = () => {
  ...

  return (
    <PageLayout
      menu={<SettingsMenu />}
      breadcrumbs={['settingsTitle', 'settingsStoppages']}
    >
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>{t('settingsStoppage')}</TableCell>
            <TableCell>{t('settingsCoordinates')}</TableCell>
            <TableCell>{t('sharedRoutes')}</TableCell>
            <TableCell className={classes.columnAction} />
          </TableRow>
        </TableHead>
        <TableBody>
          {loading
            ? <TableShimmer columns={2} endAction />
            : items.map((item) => <Item key={item.id} item={item} />)
          }
        </TableBody>
      </Table>
      <CollectionFab editPath="/settings/stoppage" />
    </PageLayout>
  );
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
0

But I need to place data inside a table and render them as well. My question was simple. Since I can't fetch data inside the JSX, On the other hand I need item.id to fetch. So how would I fetch data by item.id and render it inside the table cell?

Example:

{!loading ? items.map((item) => (
     <TableRow key={item.id}>
       <TableCell>{item.name}</TableCell>
       <TableCell>{`Latitude: ${item.latitude}, Longitude: ${item.longitude}`}</TableCell>
       <TableCell>
         {
           # need to fetch and render data here
           geofences.map((geofence) => geofence.name).join(', ')
         }
       </TableCell>
       <TableCell className={classes.columnAction} padding="none">
         <CollectionActions itemId={item.id} editPath="/settings/stoppage" endpoint="stoppages" setTimestamp={setTimestamp} />
       </TableCell>
     </TableRow>
   )) : (<TableShimmer columns={2} endAction />)}
cosmic
  • 53
  • 7
  • Are you trying to answer your own question, or trying to implement the solution I provided and have question about it? – Drew Reese Oct 26 '22 at 05:29
  • The second one. I am saying mapped Item should be inside Table. Are you saying that All have to be replaced by [original react component] ? or what is the last snippet of code all about? please explain a bit. Very much need to solve. - @Drew Reese – cosmic Oct 26 '22 at 08:15
  • Yes, the `items.map` callback is converted to a React component so it can use the `useEffect` hook and make the GET request and store some local state and map the result to the comma-joined string list value. The last snippet is what is mapped instead, i.e. instead of mapping to the table components you now map to the new component you created when refactoring the code. – Drew Reese Oct 26 '22 at 08:22
  • it's giving another uncaught error! can not read 'id'. May I show you the whole page of code if that helps!! - Drew reese – cosmic Oct 26 '22 at 09:19
  • Did you pass `item` as a prop to the new React component like in my example, i.e. `items.map((item) => )`? Sure, you can edit your post (*not answer*) to include the new and relevant details and code example. – Drew Reese Oct 26 '22 at 15:46
  • yes I did so far. Alright I have edited the post. Please check. @Drew Reese – cosmic Oct 26 '22 at 17:22
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249085/discussion-between-drew-reese-and-cosmic). – Drew Reese Oct 26 '22 at 18:54