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
- Checked for multiple versions of react
- disabling react hot loader and setConfig pureSFC to true
- 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