Merge pull request #12 from edx/jhynes/microba-1621_data-tables-continued

feat: Implement task history and pending task tables
This commit is contained in:
Justin Hynes
2022-01-25 11:10:59 -05:00
committed by GitHub
7 changed files with 478 additions and 151 deletions

View File

@@ -4,25 +4,35 @@ import { useParams } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Alert, Button, DataTable, Modal,
Button, Icon, Modal, StatefulButton,
} from '@edx/paragon';
import { SpinnerSimple } from '@edx/paragon/icons';
import messages from './messages';
import { getSentEmailHistory } from './api';
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
export function BulkEmailContentHistory({ intl }) {
const { courseId } = useParams();
const BUTTON_STATE = {
DEFAULT: 'default',
PENDING: 'pending',
COMPLETE: 'complete',
};
const [emailHistoryData, setEmailHistoryData] = useState();
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
const [showHistoricalEmailContentTable, setShowHistoricalEmailContentTable] = useState(false);
const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
const [messageContent, setMessageContent] = useState();
const [buttonState, setButtonState] = useState(BUTTON_STATE.DEFAULT);
/**
* Async function that makes a REST API call to retrieve historical email message data sent by the bulk course email
* tool from edx-platform.
*/
async function fetchSentEmailHistoryData() {
setButtonState(BUTTON_STATE.PENDING);
let data = null;
try {
data = await getSentEmailHistory(courseId);
@@ -35,6 +45,24 @@ export function BulkEmailContentHistory({ intl }) {
setEmailHistoryData(emails);
setShowHistoricalEmailContentTable(true);
}
setButtonState(BUTTON_STATE.COMPLETE);
}
/**
* This utility function transforms the data stored in `emailHistoryData` to make it easier to display in the Paragon
* DataTable component. Some of the information we want displayed is in an inner object so we extract it and move it
* up a level (the `subject` field). We also convert the `sent_to` data to be a String rather than an array to fix a
* display bug in the table.
*/
function transformDataForTable() {
const tableData = emailHistoryData.map((item) => ({
...item,
subject: item.email.subject,
sent_to: item.sent_to.join(', '),
}));
return tableData;
}
/**
@@ -47,34 +75,6 @@ export function BulkEmailContentHistory({ intl }) {
setIsMessageModalOpen(true);
};
/**
* Render function for the email content history table. If an error occurs while attempting to fetch data from
* edx-platform we will render this error instead of the table.
*/
const renderError = () => (
<div>
<Alert variant="danger">
<p className="font-weight-bold">
{intl.formatMessage(messages.errorFetchingData)}
</p>
</Alert>
</div>
);
/**
* Render function for the email content history table. If there is no data to display in our table we will render
* this informative message instead.
*/
const renderEmpty = () => (
<div className="pt-1">
<Alert variant="warning">
<p className="font-weight-bold">
{intl.formatMessage(messages.noEmailData)}
</p>
</Alert>
</div>
);
/**
* Renders a modal that will display the contents of a single historical email message sent via the bulk course email
* tool to a user.
@@ -132,83 +132,52 @@ export function BulkEmailContentHistory({ intl }) {
</div>
);
const tableColumns = [
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderSubject)}`,
accessor: 'subject',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderAuthor)}`,
accessor: 'requester',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderRecipients)}`,
accessor: 'sent_to',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderTimeSent)}`,
accessor: 'created',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderNumberSent)}`,
accessor: 'number_sent',
},
];
/**
* Render function for the email content history table. This function is responsible for displaying data inside of
* the table when the `Show Sent Email History` button is pressed on the page.
* Paragon's DataTable supports the ability to add extra columns that might not directly coincide with the data being
* represented in the table. We are using an additional column to embed a button that will open a Modal to display the
* contents of a previously sent message.
*/
const renderTable = () => {
// Do a little data manipulation to make it easier to display what we want in the table. Pull the email subject out
// of the email data. Transforms the `sent_to` array to a string for easier display in our table.
const tableData = emailHistoryData.map((item) => ({
...item,
subject: item.email.subject,
sent_to: item.sent_to.join(', '),
}));
const additionalColumns = () => {
const tableData = transformDataForTable();
return (
<div className="pb-3">
<p className="font-italic">
{intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)}
</p>
<DataTable
itemCount={emailHistoryData.length}
columns={[
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderSubject)}`,
accessor: 'subject',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderAuthor)}`,
accessor: 'requester',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderRecipients)}`,
accessor: 'sent_to',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderTimeSent)}`,
accessor: 'created',
},
{
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderNumberSent)}`,
accessor: 'number_sent',
},
]}
data={tableData}
additionalColumns={[
{
id: 'view_message',
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderViewMessage)}`,
Cell: ({ row }) => (
<Button variant="link" className="px-1" onClick={() => onViewMessageClick(tableData[row.index])}>
{intl.formatMessage(messages.buttonViewMessage)}
</Button>
),
},
]}
/>
</div>
[
{
id: 'view_message',
Header: `${intl.formatMessage(messages.emailHistoryTableColumnHeaderViewMessage)}`,
Cell: ({ row }) => (
<Button variant="link" className="px-1" onClick={() => onViewMessageClick(tableData[row.index])}>
{intl.formatMessage(messages.buttonViewMessage)}
</Button>
),
},
]
);
};
/**
* Today there can be three states which the renderTableData function will handle:
* 1. There was an error retrieving data from edx-platform and we can't display anything (for now).
* 2. There is no email history for this course-run and we have nothing to display to the end user.
* 3. We were able to receive historical email content and it will be presented in a table.
*/
const renderTableData = () => {
if (errorRetrievingData) {
return renderError();
}
if (!emailHistoryData.length) {
return renderEmpty();
}
return renderTable();
};
return (
<div>
<div>
@@ -218,10 +187,35 @@ export function BulkEmailContentHistory({ intl }) {
<p>
{intl.formatMessage(messages.emailHistoryTableSectionButtonHeader)}
</p>
<Button variant="outline-primary" className="btn btn-outline-primary mb-2" onClick={async () => { await fetchSentEmailHistoryData(); }}>
<StatefulButton
className="btn btn-outline-primary mb-2"
variant="outline-primary"
type="submit"
onClick={async () => { await fetchSentEmailHistoryData(); }}
labels={{
default: `${intl.formatMessage(messages.emailHistoryTableSectionButton)}`,
pending: `${intl.formatMessage(messages.emailHistoryTableSectionButton)}`,
complete: `${intl.formatMessage(messages.emailHistoryTableSectionButton)}`,
}}
icons={{
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
}}
disabledStates={['error']}
state={buttonState}
>
{intl.formatMessage(messages.emailHistoryTableSectionButton)}
</Button>
{showHistoricalEmailContentTable && renderTableData()}
</StatefulButton>
{ showHistoricalEmailContentTable && (
<BulkEmailTaskManagerTable
error={errorRetrievingData}
tableData={transformDataForTable()}
tableDescription={intl.formatMessage(messages.emailHistoryTableViewMessageInstructions)}
alertWarningMessage={intl.formatMessage(messages.noEmailData)}
alertErrorMessage={intl.formatMessage(messages.errorFetchingEmailHistoryData)}
columns={tableColumns}
additionalColumns={additionalColumns()}
/>
)}
</div>
</div>
);

View File

@@ -0,0 +1,86 @@
import { Alert, DataTable } from '@edx/paragon';
import PropTypes from 'prop-types';
import React from 'react';
export default function BulkEmailTaskManagerTable(props) {
const {
errorRetrievingData,
tableData,
tableDescription,
alertWarningMessage,
alertErrorMessage,
columns,
additionalColumns,
} = props;
/**
* Sub-render function that creates an Alert component with a specific type and message for display to a user.
*/
const renderAlert = (alertType, alertMessage) => (
<div className="pt-1">
<Alert variant={`${alertType}`}>
<p className="font-weight-bold">
{`${alertMessage}`}
</p>
</Alert>
</div>
);
/**
* Responsible for rendering the tables used by the BulkEmailContentHistory, BulkEmailTaskManager, and
* BulkEmailTaskHistory components. Conditionally renders a table description as well.
*/
const renderTable = () => (
<div className="pb-3">
{tableDescription && (
<p className="font-italic">
{tableDescription}
</p>
)}
<DataTable
itemCount={tableData.length}
columns={columns}
data={tableData}
additionalColumns={additionalColumns}
/>
</div>
);
/**
* Sub-render function that determines if we can render the DataTable. If not, we will render an Alert component to
* inform the user why the data/table cannot be displayed.
*/
const canRenderTable = () => {
if (errorRetrievingData) {
return renderAlert('danger', alertErrorMessage);
}
if (!tableData.length) {
return renderAlert('warning', alertWarningMessage);
}
return renderTable();
};
return (
<div>
{canRenderTable()}
</div>
);
}
BulkEmailTaskManagerTable.propTypes = {
errorRetrievingData: PropTypes.bool.isRequired,
tableData: PropTypes.arrayOf(PropTypes.object),
tableDescription: PropTypes.string,
alertWarningMessage: PropTypes.string.isRequired,
alertErrorMessage: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
additionalColumns: PropTypes.arrayOf(PropTypes.object),
};
BulkEmailTaskManagerTable.defaultProps = {
tableData: [],
tableDescription: '',
additionalColumns: [],
};

View File

@@ -1,32 +1,91 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getInstructorTasks } from './api';
import messages from './messages';
import useInterval from '../../../utils/useInterval';
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
export default function BulkEmailPendingTasks() {
export function BulkEmailPendingTasks({ intl }) {
const { courseId } = useParams();
const [instructorTaskData, setInstructorTaskData] = useState(); // eslint-disable-line no-unused-vars
const [instructorTaskData, setInstructorTaskData] = useState();
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
useEffect(() => {
/**
* We use a custom hook (`useInterval`) here to setup a timer that will refresh the pending instructor task data
* displayed in the table of this component.
*/
useInterval(() => {
async function fetchPendingInstructorTasksData() {
const data = await getInstructorTasks(courseId);
const { tasks } = data;
setInstructorTaskData(tasks);
}
fetchPendingInstructorTasksData();
}, []);
try {
fetchPendingInstructorTasksData();
} catch (error) {
setErrorRetrievingData(true);
}
}, 30000);
const tableColumns = [
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`,
accessor: 'task_type',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskInputs)}`,
accessor: 'task_input',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskId)}`,
accessor: 'task_id',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskRequester)}`,
accessor: 'requester',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskSubmittedDate)}`,
accessor: 'created',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskDuration)}`,
accessor: 'duration_sec',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskState)}`,
accessor: 'task_state',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskStatus)}`,
accessor: 'status',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskProgress)}`,
accessor: 'task_message',
},
];
return (
<div>
<p>
<FormattedMessage
id="bulk.email.pending.tasks.section.info"
defaultMessage="Email actions run in the background. The status for any active tasks - including email tasks - appears in the table below"
description="A section to see pending and executing Instructor Tasks"
/>
</p>
<div className="pb-4">
<BulkEmailTaskManagerTable
error={errorRetrievingData}
tableData={instructorTaskData}
tableDescription={intl.formatMessage(messages.pendingTaskSectionInfo)}
alertWarningMessage={intl.formatMessage(messages.noPendingTaskData)}
alertErrorMessage={intl.formatMessage(messages.errorFetchingPendingTaskData)}
columns={tableColumns}
/>
</div>
);
}
BulkEmailPendingTasks.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(BulkEmailPendingTasks);

View File

@@ -1,41 +1,129 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Icon, StatefulButton } from '@edx/paragon';
import { SpinnerSimple } from '@edx/paragon/icons';
import { getEmailTaskHistory } from './api';
import messages from './messages';
export default function BulkEmailTaskHistory() {
import BulkEmailTaskManagerTable from './BulkEmailHistoryTable';
export function BulkEmailTaskHistory({ intl }) {
const { courseId } = useParams();
const BUTTON_STATE = {
DEFAULT: 'default',
PENDING: 'pending',
COMPLETE: 'complete',
};
const [emailTaskHistoryData, setEmailTaskHistoryData] = useState(); // eslint-disable-line no-unused-vars
const [emailTaskHistoryData, setEmailTaskHistoryData] = useState();
const [showHistoricalTaskContentTable, setShowHistoricalTaskContentTable] = useState(false);
const [errorRetrievingData, setErrorRetrievingData] = useState(false);
const [buttonState, setButtonState] = useState(BUTTON_STATE.DEFAULT);
useEffect(() => {
async function fetchEmailTaskHistoryData() {
const data = await getEmailTaskHistory(courseId);
/**
* Async function that makes a REST API call to retrieve historical bulk email (Instructor) task data for display
* within this component.
*/
async function fetchEmailTaskHistoryData() {
setButtonState(BUTTON_STATE.PENDING);
let data = null;
try {
data = await getEmailTaskHistory(courseId);
} catch (error) {
setErrorRetrievingData(true);
}
if (data) {
const { tasks } = data;
setEmailTaskHistoryData(tasks);
setShowHistoricalTaskContentTable(true);
}
fetchEmailTaskHistoryData();
}, []);
setButtonState(BUTTON_STATE.COMPLETE);
}
const tableColumns = [
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskType)}`,
accessor: 'task_type',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskInputs)}`,
accessor: 'task_input',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskId)}`,
accessor: 'task_id',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskRequester)}`,
accessor: 'requester',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskSubmittedDate)}`,
accessor: 'created',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskDuration)}`,
accessor: 'duration_sec',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskState)}`,
accessor: 'task_state',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskStatus)}`,
accessor: 'status',
},
{
Header: `${intl.formatMessage(messages.taskHistoryTableColumnHeaderTaskProgress)}`,
accessor: 'task_message',
},
];
return (
<div>
<div>
<p>
<FormattedMessage
id="bulk.email.task.history.section.heading"
defaultMessage="To see the status for all email tasks submitted for this course, click this button:"
description="Instructions for course staff and admins to view historical bulk course email task data"
/>
{intl.formatMessage(messages.emailTaskHistoryTableSectionButtonHeader)}
</p>
<button type="button" className="btn btn-outline-primary mb-2">
<FormattedMessage
id="bulk.email.view.task.history.button"
defaultMessage="Show Task Email History"
description="Button that displays a table with historical bulk email task data for a course-run"
<StatefulButton
className="btn btn-outline-primary mb-2"
variant="outline-primary"
type="submit"
onClick={async () => { await fetchEmailTaskHistoryData(); }}
labels={{
default: `${intl.formatMessage(messages.emailTaskHistoryTableSectionButton)}`,
pending: `${intl.formatMessage(messages.emailTaskHistoryTableSectionButton)}`,
complete: `${intl.formatMessage(messages.emailTaskHistoryTableSectionButton)}`,
}}
icons={{
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
}}
disabledStates={['error']}
state={buttonState}
>
{intl.formatMessage(messages.emailHistoryTableSectionButton)}
</StatefulButton>
{showHistoricalTaskContentTable && (
<BulkEmailTaskManagerTable
error={errorRetrievingData}
tableData={emailTaskHistoryData}
alertWarningMessage={intl.formatMessage(messages.noTaskHistoryData)}
alertErrorMessage={intl.formatMessage(messages.errorFetchingTaskHistoryData)}
columns={tableColumns}
/>
</button>
)}
</div>
</div>
);
}
BulkEmailTaskHistory.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(BulkEmailTaskHistory);

View File

@@ -1,30 +1,23 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import BulkEmailContentHistory from './BulkEmailContentHistory';
import BulkEmailPendingTasks from './BulkEmailPendingTasks';
import BulkEmailTaskHistory from './BulkEmailTaskHistory';
import messages from './messages';
export default function BulkEmailTaskManager() {
export function BulkEmailTaskManager({ intl }) {
return (
<div className="px-5">
<div>
<h2 className="h3">
<FormattedMessage
id="bulk.email.pending.tasks.section.heading"
defaultMessage="Pending Tasks"
description="A section to see pending and executing Instructor Tasks"
/>
{intl.formatMessage(messages.pendingTasksHeader)}
</h2>
<BulkEmailPendingTasks />
</div>
<div>
<h2 className="h3">
<FormattedMessage
id="bulk.email.task.manager.heading"
defaultMessage="Email Task History"
description="Title of the Email task History section of the Bulk Course Email tool"
/>
{intl.formatMessage(messages.emailTaskHistoryHeader)}
</h2>
<BulkEmailContentHistory />
</div>
@@ -34,3 +27,9 @@ export default function BulkEmailTaskManager() {
</div>
);
}
BulkEmailTaskManager.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(BulkEmailTaskManager);

View File

@@ -2,13 +2,13 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
/* BulkEmailContentHistory.jsx Messages */
errorFetchingData: {
errorFetchingEmailHistoryData: {
id: 'bulk.email.content.history.table.alert.errorFetchingData',
defaultMessage: 'An error occurred retrieving email history data for this course. Please try again later.',
},
noEmailData: {
id: 'bulk.email.content.history.table.alert.noEmailData',
defaultMessage: 'There is no email history for this course',
defaultMessage: 'There is no email history for this course.',
},
buttonViewMessage: {
id: 'bulk.email.content.history.table.button.viewMessage',
@@ -71,8 +71,81 @@ const messages = defineMessages({
defaultMessage: 'Show Sent Email History',
},
/* BulkEmailTaskManager.jsx messages */
pendingTasksHeader: {
id: 'bulk.email.pending.tasks.header',
defaultMessage: 'Pending Tasks',
},
emailTaskHistoryHeader: {
id: 'bulk.email.email.task.history.header',
defaultMessage: 'Email Task History',
},
/* BulkEmailPendingTasks.jsx messages */
pendingTaskSectionInfo: {
id: 'bulk.email.pending.tasks.section.info',
defaultMessage: 'Email actions run in the background. The status for any active tasks - including email tasks - appears in the table below.',
},
errorFetchingPendingTaskData: {
id: 'bulk.email.pending.tasks.table.alert.errorFetchingData',
defaultMessage: 'Error fetching Instructor Task data. This request will be retried automatically.',
},
noPendingTaskData: {
id: 'bulk.email.pending.tasks.table.alert.noTaskData',
defaultMessage: 'No tasks currently running.',
},
/* BulkEmailTaskHistory.jsx messages */
emailTaskHistoryTableSectionButtonHeader: {
id: 'bulk.email.task.history.table.button.header',
defaultMessage: 'To see the status for all email tasks submitted for this course, click this button:',
},
emailTaskHistoryTableSectionButton: {
id: 'bulk.email.task.history.table.button',
defaultMessage: 'Show Email Task History',
},
errorFetchingTaskHistoryData: {
id: 'bulk.email.task.history.table.alert.errorFetchingData',
defaultMessage: 'Error fetching email task history data for this course. Please try again later.',
},
noTaskHistoryData: {
id: 'bulk.email.task.history.table.alert.noTaskData',
defaultMessage: 'There is no email task history for this course.',
},
/* Common Messages */
taskHistoryTableColumnHeaderTaskType: {
id: 'bulk.email.task.history.table.column.header.taskType',
defaultMessage: 'Task Type',
},
taskHistoryTableColumnHeaderTaskInputs: {
id: 'bulk.email.task.history.table.column.header.taskInputs',
defaultMessage: 'Task Inputs',
},
taskHistoryTableColumnHeaderTaskId: {
id: 'bulk.email.task.history.table.column.header.taskId',
defaultMessage: 'Task Id',
},
taskHistoryTableColumnHeaderTaskRequester: {
id: 'bulk.email.task.history.table.column.header.taskRequester',
defaultMessage: 'Requester',
},
taskHistoryTableColumnHeaderTaskSubmittedDate: {
id: 'bulk.email.task.history.table.column.header.taskSubmittedDate',
defaultMessage: 'Submitted',
},
taskHistoryTableColumnHeaderTaskDuration: {
id: 'bulk.email.task.history.table.column.header.taskDuration',
defaultMessage: 'Duration (seconds)',
},
taskHistoryTableColumnHeaderTaskState: {
id: 'bulk.email.task.history.table.column.header.taskState',
defaultMessage: 'State',
},
taskHistoryTableColumnHeaderTaskStatus: {
id: 'bulk.email.task.history.table.column.header.taskStatus',
defaultMessage: 'Status',
},
taskHistoryTableColumnHeaderTaskProgress: {
id: 'bulk.email.task.history.table.column.header.taskProgress',
defaultMessage: 'Task Progress',
},
});
export default messages;

28
src/utils/useInterval.js Normal file
View File

@@ -0,0 +1,28 @@
import { useEffect, useRef } from 'react';
/**
* A custom hook used by the BulkEmailPendingTasks component to periodically make an API call on a regular interval.
* This is lifted from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/.
*/
export default function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
return null;
}, [delay]);
}