Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement

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

Answers (1)

Firing Dev
Firing Dev

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

Related Questions