Reputation: 21
enter image description here enter image description here I am getting this error when I want to fetch an item from my customer list, on the first try.
The scenario that does not fail is when I search for a name that is not in the table and then search for an existing element, but in the end if I then send an empty value it fails again.
At the moment I was trying to take the value of the clients in clientsInLocalUpdated, which is the state that helps me save the filtered clients.
I share the components that interact on this page and the page.
ClientsPage.tsx
import { Box, Card, CardContent, Stack, Paper } from '@mui/material';
import { useClients } from 'src/app/hooks/useClients';
import { HEAD_CLIENT_TABLE, STYLES_BY_COLUMN } from './constants';
import { useNavigate } from 'react-router-dom';
import { Home } from "src/app/modules";
import { Button, Label, Search, Table } from 'src/app/components';
import { useCallback, useEffect, useState } from 'react';
import { IDataRow } from 'src/app/models';
import { useNotifications } from 'reapop';
import { environment } from 'src/environments/environment';
const getActionComponent = () => ((props: any) => {
const navigate = useNavigate();
return <Button
size="small"
onClick={() => navigate(`/clients/${props.row.id}`)}
{...props}
color="secondary"
>
Create Punchlist
</Button>
});
export const ClientsPage = () => {
const { isLoading, clients, notificationMessage } = useClients();
const {notify} = useNotifications();
const [clientsInLocalUpdated, setClientsInLocalUpdated] = useState<IDataRow[]>(clients);
const onFilterClients = useCallback((searchInput: string) => {
if (searchInput === undefined) return;
if (searchInput === '') {
setClientsInLocalUpdated(clients);
} else {
const filteredClients = clients.filter(
(item) =>
item && item?.name && item.name.toLowerCase().includes(searchInput.toLowerCase())
);
setClientsInLocalUpdated(filteredClients);
}
}, [clients, clientsInLocalUpdated])
useEffect(() => {
setClientsInLocalUpdated(clients);
}, [isLoading])
useEffect(() => {
if (notificationMessage) {
notify(notificationMessage);
}
}, [notificationMessage, notify]);
return (
<Home isLoading={isLoading}>
<Box sx={{ p: 2, paddingX: 2 }}>
<Label paddingY={4}>
Clients
</Label>
<Card
variant="outlined"
sx={{ border: 'None', padding: 0, margin: 0 }}
>
<CardContent sx={{ paddingX: 2 }}>
<Stack spacing={2}>
{environment.featureFlags.showHiddenComponents &&
<Search placeholder="Search by name or phone number" onSearch={onFilterClients} />}
<Paper variant="outlined">
{clientsInLocalUpdated && <Table
heads={HEAD_CLIENT_TABLE}
values={clientsInLocalUpdated}
styles={STYLES_BY_COLUMN}
hasActions
actionComponent={{ getActionComponent: getActionComponent(), title: "Actions" }}
/>}
</Paper>
</Stack>
</CardContent>
</Card>
</Box>
</Home>
);
};
Search.tsx
import { useState } from "react";
import { Button, Stack } from "@mui/material";
import { InputText } from "../InputText/InputText";
interface ISearchProps {
placeholder: string;
onSearch: (arg: string) => void;
}
export const Search = ({ placeholder, onSearch }: ISearchProps) => {
const [searchInput, setSearchInput] = useState<string>('');
return (<form
onSubmit={(e) => {
e.preventDefault();
onSearch(searchInput);
}}
>
<Stack direction="row" spacing={2}>
<InputText
width="100%"
size="small"
fullWidth
placeholder={placeholder}
variant="outlined"
value={searchInput}
onChange={(e) => setSearchInput(e?.target?.value || '')}
border
/>
<Button variant="contained" color="primary" onClick={() => onSearch(searchInput)}>
Search
</Button>
</Stack>
</form>)
}
Table.tsx
import {
TableContainer,
Table as BasicTable,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@mui/material";
import { ICellStyles, IDataRow, IHeadItem } from 'src/app/models';
export interface ITableProps {
heads: IHeadItem[]
values: IDataRow[];
styles?: { heads: ICellStyles, values: ICellStyles, [key: string]: ICellStyles };
hasActions?: boolean;
actionComponent?: { getActionComponent: (props: any) => React.ReactElement, title?: string; }
}
const renderCell = (heads: IHeadItem[], values: { [key: string]: string }, _index: number, styles?: any,) => {
return heads.map(({ name, component }) => {
// eslint-disable-next-line react/jsx-no-useless-fragment
if (!name) return (<></>);
const { format } = styles?.[name] || {};
const columnValue = values?.[name];
return (<TableCell align="right" key={`${_index}_${name}_cell`} {...styles}>{component ? component({ value: columnValue, index: _index, name }) : format ? format(columnValue) : columnValue}</TableCell>)
});
}
export const Table = ({ heads, values, styles, hasActions, actionComponent }: ITableProps) => {
return (
<TableContainer>
<BasicTable sx={{ minWidth: 650 }} aria-label="table">
<TableHead>
<TableRow>
{heads.map(({ name, label }, _index) => (
<TableCell key={`${_index}_${name}_cell`} align="right" style={{ fontWeight: 'bold' }} {...styles?.heads as any}>
{label}
</TableCell>
))}
{hasActions && actionComponent?.title && <TableCell align="right" style={{ fontWeight: 'bold' }}>{actionComponent.title}</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{values &&
values.map((row: IDataRow, _index: number) => (
<TableRow
key={`${row.name}_${_index}`}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{renderCell(heads, row, _index, { ...styles, ...styles?.values })}
{hasActions && (
<TableCell align="right" {...styles?.values}>
{actionComponent?.getActionComponent({ row })}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</BasicTable>
</TableContainer>
);
}
Update: HeadClientTable constant
import { IHeadItem } from 'src/app/models';
import { formatDate } from 'src/app/utils';
export const HEAD_CLIENT_TABLE: IHeadItem[] = [
{
name: 'name',
label: 'Client',
},
{
name: 'phoneNumber',
label: 'Phone Number',
},
{
name: 'email',
label: 'Email',
},
{
name: 'lastExportAt',
label: 'Last Created',
},
];
export const STYLES_BY_COLUMN: any = {
heads: { align: 'right', type: 'text' },
values: { align: 'right', type: 'text' },
lastExportAt: {
format: formatDate,
},
};
I have changed the order of the useEffect to the top of the component. I read that this could be one of the causes. I have also changed the way to initialize clientsInLocalUpdated.
Upvotes: 0
Views: 3646
Reputation: 88
How are you? Your error is commonly caused by breaking any of rules of hooks in React.
Here are the rules.
🔴 Do not call Hooks inside conditions or loops.
🔴 Do not call Hooks after a conditional return statement.
🔴 Do not call Hooks in event handlers.
🔴 Do not call Hooks in class components.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
And your error "Rendered fewer hooks than expected", means that the count of hooks is different on some renders.
This is caused by breaking this rule.
🔴 Do not call Hooks after a conditional return statement.
And I think the error might on this page.
ClientsPage.tsx
return (
<Home isLoading={isLoading}>
<Box sx={{ p: 2, paddingX: 2 }}>
<Label paddingY={4}>
Clients
</Label>
<Card
variant="outlined"
sx={{ border: 'None', padding: 0, margin: 0 }}
>
<CardContent sx={{ paddingX: 2 }}>
<Stack spacing={2}>
{environment.featureFlags.showHiddenComponents &&
<Search placeholder="Search by name or phone number" onSearch={onFilterClients} />}
<Paper variant="outlined">
{clientsInLocalUpdated && <Table
heads={HEAD_CLIENT_TABLE}
values={clientsInLocalUpdated}
styles={STYLES_BY_COLUMN}
hasActions
actionComponent={{ getActionComponent: getActionComponent(), title: "Actions" }}
/>}
</Paper>
</Stack>
</CardContent>
</Card>
</Box>
</Home>
);
Here you rendered Table component conditionally using
clientsInLocalUpdated
And Table component has hook in it.
To fix this, you can render Table Component every render, and pass down clientsInLocalUpdated as props.
Like this
<Paper variant="outlined">
<Table
heads={HEAD_CLIENT_TABLE}
values={clientsInLocalUpdated}
styles={STYLES_BY_COLUMN}
hasActions
actionComponent={{ getActionComponent: getActionComponent(), title: "Actions" }}
clientsInLocalUpdated={clientsInLocalUpdated}
/>
</Paper>
And handle clientsInLocalUpdated in Table component.
Hope this answer is helpful. Thank you.
Upvotes: -1