Non
Non

Reputation: 8599

Custom hook error: Hooks can only be called inside of the body of a function component

I am trying to develop a custom hook which seems to be pretty easy but I am getting an error

Uncaught Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app

This is my hook:

import React, { useState, useEffect } from 'react';

const useInfiniteScroll = (isLastPage: boolean, fetchFn: any) => {
  const [pageCount, setPageCount] = useState(0);
  const triggerFetchEvents = (): void => {
    let response;

    setPageCount(() => {
      if (!isLastPage) {
        response = fetchFn(pageCount + 1, 5, 'latest');
      }

      return pageCount + 1;
    });

    return response;
  };

  useEffect(() => {
    triggerFetchEvents();
  }, []);

  return pageCount;
};

export default useInfiniteScroll;

And the component here I am calling it:

import React, { FC } from 'react';
import { connect } from 'react-redux';
import { fetchEvents } from '../../shared/actions/eventActions';
import { AppState } from '../../shared/types/genericTypes';
import EventModel from '../../shared/models/Event.model';
import EventListPage from '../../components/events/EventListPage';
import useInfiniteScroll from '../../shared/services/triggerInfiniteScroll';

type Props = {
  fetchEvents?: any;
  isLastPage: boolean;
  eventsList?: EventModel[];
};

const mapState: any = (state: AppState, props: Props): Props => ({
  eventsList: state.eventReducers.eventsList,
  isLastPage: state.eventReducers.isLastPage,
  ...props
});

const actionCreators = {
  fetchEvents
};

export const EventsScene: FC<Props> = props => {
  const { eventsList, fetchEvents, isLastPage } = props;
  const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);

  useIn();

  // const [pageCount, setPageCount] = useState(0);

  // const triggerFetchEvents = (): void => {
  //   let response;

  //   setPageCount(() => {
  //     if (!isLastPage) {
  //       response = fetchEvents(pageCount + 1, 1, 'latest');
  //     }

  //     return pageCount + 1;
  //   });

  //   return response;
  // };

  // useEffect(() => {
  //   triggerFetchEvents();
  // }, []);

  if (!eventsList || !eventsList.length) return null;

  return (
    <EventListPage
      eventsList={eventsList}
      isLastPage={isLastPage}
      triggerFetchEvents={useIn}
    />
  );
};

export default connect(
  mapState,
  actionCreators
)(EventsScene);

I left the commented code there to show you that if I uncomment the code and remove useInfiniteScroll then it works properly.

What could I be missing?

UPDATE: This is EventListPage component

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import EventModel from '../../shared/models/Event.model';
import { formatDate } from '../../shared/services/date';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Card from 'react-bootstrap/Card';

type Props = {
  eventsList?: EventModel[];
  isLastPage: boolean;
  triggerFetchEvents: any;
};

export const EventListPage: React.FC<Props> = props => {
  const { eventsList, triggerFetchEvents, isLastPage } = props;

  const [isFetching, setIsFetching] = useState(false);

  const fetchMoreEvents = (): Promise<void> =>
    triggerFetchEvents().then(() => {
      setIsFetching(false);
    });

  const handleScroll = (): void => {
    if (
      document.documentElement.offsetHeight -
        (window.innerHeight + document.documentElement.scrollTop) >
        1 ||
      isFetching
    ) {
      return;
    }

    return setIsFetching(true);
  };

  useEffect(() => {
    if (isFetching) return;

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  useEffect(() => {
    if (!isFetching) return;

    if (!isLastPage) fetchMoreEvents();
  }, [isFetching]);

  if (!eventsList) return null;

  return (
    <Container className='article-list mt-5'>
      ///...
    </Container>
  );
};

export default EventListPage;

Upvotes: 1

Views: 3438

Answers (1)

ford04
ford04

Reputation: 74820

In EventsScene, change useInfiniteScroll to be invoked directly at the function body top-level (not sure why you are creating this indirection in the first place):

// before
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();

// after
useInfiniteScroll(isLastPage, fetchEvents)

React expects Hook calls to only happen at the top-level as it relies on the order of Hooks to be always the same. If you wrap the Hook in a function, you can potentially invoke this function in many code locations disturbing the Hooks' order.

There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state. Link

Upvotes: 1

Related Questions