React Hooks throws error Hooks can only be called inside of the body of a function component.. after upgrade to web pack 4

220 views Asked by At

I have Rails Application with React On Rails, recently upgraded webpack 3 to webpack 4

Rails : 4 Ruby: 2+ React: 16.9 Webpack: 4.41.5

everything works well except below code which works well with webpack 3

In the console I see below 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:

An error occurred during a React Lifecycle function

import Error from '@material-ui/icons/Error';
import MoreHoriz from '@material-ui/icons/MoreHoriz';
import { Button, FixedFooterModal, SafeAnchor, Util } from 'lib-components';
import moment from 'moment-timezone';
import prettyBytes from 'pretty-bytes';
import React, { useState, useCallback, useMemo, useEffect } from 'react';

import Helpers from '../../../lib/lib-components/src/js/services/Helpers'; // eslint-disable-line

import styles from './ExportForm.css';

interface Export {
  created_at: string;
  filename: string;
  filesize: number;
  id: number;
  project_id: number;
  status: string;
  updated_at: string;
  options: string;
}

function parseOptions(options: string) {
  return options
    .split(',')
    .map(option => {
      switch (option) {
        case 'include_done_stories':
          return 'Done';
        case 'include_current_backlog_stories':
          return 'Current/Backlog';
        case 'include_icebox_stories':
          return 'Icebox';
        case 'include_epics':
          return 'Epics';
        case 'include_attachments':
          return 'Attachments';
        case 'include_project_history':
          return 'Project History';
      }
    })
    .join(', ');
}

const ExportStatus: React.FunctionComponent<{
  status: string;
  projectId: number;
  exportId: number;
  csrfToken: string;
  setModalOpen: (a: boolean) => void;
}> = ({ status, projectId, exportId, csrfToken, setModalOpen }) => {
  const handleRetryClick = useCallback(() => {
    Helpers.update(csrfToken, {
      method: 'GET',
      url: `/projects/${projectId}/export/${exportId}/retry`,
    });
    setModalOpen(true);
  }, [projectId, exportId, csrfToken, setModalOpen]);
  switch (status) {
    case 'failed':
      return (
        <div data-aid='ExportForm__failed' className={styles['ExportForm__failed']}>
          <div className={styles['ExportForm__failedIcon']}>
            <Error fontSize='small' style={{ marginRight: '4px', color: '#CB2B1E' }} />
            <span>Export failed</span>
          </div>
          <div data-aid='ExportForm__retryLink' className={styles['ExportForm__retryLink']}>
            <SafeAnchor onClick={handleRetryClick}>retry</SafeAnchor>
          </div>
        </div>
      );
    case 'completed':
      return (
        <div data-aid='ExportForm__completed' className={styles['ExportForm__completed']}>
          <div className={styles['ExportForm__completedIcon']}>
            <CheckCircle fontSize='small' style={{ marginRight: '4px', color: '#6D902A' }} />
            <span>Complete</span>
          </div>
          <div className={styles['ExportForm__completedDownloadLink']}>
            <SafeAnchor href={`/projects/${projectId}/export/${exportId}/download`}>download</SafeAnchor>
          </div>
        </div>
      );
    default:
    case 'in progress':
      return (
        <div className={styles['ExportForm__inProgressIcon']}>
          <div className={styles['ExportForm__inProgressIconBg']}>
            <MoreHoriz fontSize='small' />
          </div>
          <span data-aid='ExportForm__inProgress' className={styles['ExportForm__inProgress']}>
            In Progress
          </span>
        </div>
      );
  }
};

const ExportForm: React.FunctionComponent<{
  csrfToken: string;
  id: number;
  exports: Export[];
  timezone: string;
}> = ({ csrfToken, id, exports, timezone }) => {
  const [modalOpen, setModalOpen] = useState(false);
  const [includeDoneStories, setIncludeDoneStories] = useState(true);
  const [includeCurrentBacklogStories, setIncludeCurrentBacklogStories] = useState(true);
  const [includeIceboxStories, setIncludeIceboxStories] = useState(true);
  const [includeEpics, setIncludeEpics] = useState(true);
  const [includeAttachments, setIncludeAttachments] = useState(true);
  const [includeProjectHistory, setIncludeProjectHistory] = useState(true);

  const setDoneStoriesFromEvent = useCallback(e => setIncludeDoneStories(e.target.checked), []);
  const setCurrentBacklogStoriesFromEvent = useCallback(e => setIncludeCurrentBacklogStories(e.target.checked), []);
  const setIceboxStoriesFromEvent = useCallback(e => setIncludeIceboxStories(e.target.checked), []);
  const setEpicsFromEvent = useCallback(e => setIncludeEpics(e.target.checked), []);
  const setAttachmentsFromEvent = useCallback(e => setIncludeAttachments(e.target.checked), []);
  const setProjectHistoryFromEvent = useCallback(e => setIncludeProjectHistory(e.target.checked), []);

  const handleExportClicked = useCallback(() => {
    Helpers.update(
      csrfToken,
      {
        method: 'POST',
        url: `/projects/${id}/export`,
      },
      {
        options: {
          include_done_stories: includeDoneStories,
          include_current_backlog_stories: includeCurrentBacklogStories,
          include_icebox_stories: includeIceboxStories,
          include_epics: includeEpics,
          include_attachments: includeAttachments,
          include_project_history: includeProjectHistory,
        },
      }
    );
    setModalOpen(true);
  }, [
    csrfToken,
    id,
    includeDoneStories,
    includeCurrentBacklogStories,
    includeIceboxStories,
    includeEpics,
    includeAttachments,
    includeProjectHistory,
  ]);
  const handleCloseModal = useCallback(() => {
    setModalOpen(false);
    Util.windowLocation().assign(`/projects/${id}/export`);
  }, [id]);
  const justRefresh = useMemo(() => exports.some(e => e.status === 'in progress'), [exports]);
  useEffect(() => {
    let timer: ReturnType<typeof setTimeout>;
    if (justRefresh) {
      timer = setTimeout(() => Util.windowLocation().reload(), 30000);
    }
    return () => {
      clearTimeout(timer);
    };
  }, [justRefresh]);

  return (
    <div className={styles['ExportForm']}>
      <h2>Create New Export</h2>
      <p>
        Stories, Epics, and Project History will be exported as a CSV. All files will be available to download from the
        exports section below. Files are available for two weeks.
      </p>
      <div className={styles['ExportForm__options']}>
        <div className={styles['ExportForm__option']}>
          <label>
            <input type='checkbox' checked={includeDoneStories} onChange={setDoneStoriesFromEvent} />
            All Done Stories
          </label>
        </div>
        <div className={styles['ExportForm__option']}>
          <label>
            <input
              type='checkbox'
              checked={includeCurrentBacklogStories}
              onChange={setCurrentBacklogStoriesFromEvent}
            />
            All Current/Backlog Stories
          </label>
        </div>
        <div className={styles['ExportForm__option']}>
          <label>
            <input type='checkbox' checked={includeIceboxStories} onChange={setIceboxStoriesFromEvent} />
            All Icebox Stories
          </label>
        </div>
        <div className={styles['ExportForm__option']}>
          <label>
            <input type='checkbox' checked={includeEpics} onChange={setEpicsFromEvent} />
            All Epics
          </label>
        </div>
        <div className={styles['ExportForm__option']}>
          <label>
            <input
              data-aid='ExportForm__attachments'
              type='checkbox'
              checked={includeAttachments}
              onChange={setAttachmentsFromEvent}
            />
            All Attachments
          </label>
        </div>
        <div className={styles['ExportForm__option']}>
          <label>
            <input
              data-aid='ExportForm__attachments'
              type='checkbox'
              checked={includeProjectHistory}
              onChange={setProjectHistoryFromEvent}
            />
            Project History
          </label>
        </div>
      </div>
      <Button
        label={<span>Export</span>}
        size='large'
        type='primary'
        data-aid='ExportForm__exportButton'
        onClick={handleExportClicked}
      />
      {modalOpen && (
        <FixedFooterModal
          buttons={[
            {
              props: {
                label: 'Close',
                type: 'lined',
                onClick: handleCloseModal,
                align: 'right',
                'data-aid': 'ExportForm__closeModal',
              },
            },
          ]}
          title='Export in progress'
          onClose={handleCloseModal}
          noScroll={true}
        >
          <div className={styles['ExportForm__inProgressModal']}>
            <p>We're on it! We will send you an email once your data is available to download.</p>
            <p>
              Check back here to find all of your available exports. You can access your exported data here for two
              weeks.
            </p>
          </div>
        </FixedFooterModal>
      )}
      <h2>Exports</h2>
      {exports.length > 0 ? (
        exports.map(exp => (
          <div key={exp.id} className={styles['ExportForm__export']} data-aid='ExportForm__export'>
            <div className={styles['ExportForm__exportInfo']}>
              <div className={styles['ExportForm__exportTimes']}>
                <span data-aid='ExportForm__created'>
                  {moment.tz(exp.created_at, timezone).format('MMM Do, YYYY [at] h:mm A z')}
                </span>
                {exp.filesize ? ` (${prettyBytes(exp.filesize)}) • ` : ` • `}
                <span data-aid='ExportForm__expires'>
                  Expires on {moment.tz(exp.created_at, timezone).add(14, 'days').format('MMM Do, YYYY')}
                </span>
              </div>
              <div className={styles['ExportForm__exportOptions']}>
                <span data-aid='ExportForm__exportOptions'>({parseOptions(exp.options)})</span>
              </div>
            </div>
            <div className={styles['ExportForm__download']}>
              <ExportStatus
                status={exp.status}
                projectId={exp.project_id}
                exportId={exp.id}
                csrfToken={csrfToken}
                setModalOpen={setModalOpen}
              />
            </div>
          </div>
        ))
      ) : (
        <div className={styles['ExportForm__emptyMessage']} data-aid='ExportForm__emptyMessage'>
          <p>No exports have been created.</p>
        </div>
      )}
    </div>
  );
};

export default ExportForm;

I tried following solutions for similar error but of no help

  1. Checked for multiple versions of react
  2. disabling react hot loader and setConfig pureSFC to true
  3. Used React.FunctionComponent

Also this componenet is rendered by React.lazy() later I figured all the components rendered by lazy loading have inavlid hook call

0

There are 0 answers