Fixes test warnings where state updates were being triggered after component unmounts

This commit is contained in:
mabashian 2021-04-30 15:31:36 -04:00
parent f63312c811
commit 5b71681494
9 changed files with 59 additions and 54 deletions

View File

@ -1,11 +1,5 @@
import 'styled-components/macro';
import React, {
Fragment,
useState,
useCallback,
useEffect,
useRef,
} from 'react';
import React, { Fragment, useState, useCallback, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
@ -18,6 +12,7 @@ import OptionsList from '../OptionsList';
import useRequest from '../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../util/qs';
import Lookup from './Lookup';
import useIsMounted from '../../util/useIsMounted';
const QS_CONFIG = getQSConfig('credentials', {
page: 1,
@ -32,9 +27,9 @@ async function loadCredentials(params, selectedCredentialTypeId) {
}
function MultiCredentialsLookup(props) {
const isMounted = useRef(null);
const { value, onChange, onError, history, i18n } = props;
const [selectedType, setSelectedType] = useState(null);
const isMounted = useIsMounted();
const {
result: credentialTypes,
@ -44,22 +39,18 @@ function MultiCredentialsLookup(props) {
} = useRequest(
useCallback(async () => {
const types = await CredentialTypesAPI.loadAllTypes();
if (!isMounted.current) {
return;
}
const match = types.find(type => type.kind === 'ssh') || types[0];
setSelectedType(match);
if (isMounted.current) {
setSelectedType(match);
}
return types;
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []),
[]
);
useEffect(() => {
isMounted.current = true;
fetchTypes();
return () => {
isMounted.current = false;
};
}, [fetchTypes]);
const {
@ -86,10 +77,6 @@ function MultiCredentialsLookup(props) {
CredentialsAPI.readOptions(),
]);
if (!isMounted.current) {
return;
}
results.map(result => {
if (result.kind === 'vault' && result.inputs?.vault_id) {
result.label = `${result.name} | ${result.inputs.vault_id}`;
@ -119,11 +106,7 @@ function MultiCredentialsLookup(props) {
);
useEffect(() => {
isMounted.current = true;
fetchCredentials();
return () => {
isMounted.current = false;
};
}, [fetchCredentials]);
useEffect(() => {

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import useIsMounted from '../../util/useIsMounted';
/*
Hook for using PatternFly's <Select> component when a pre-existing value
@ -9,8 +10,12 @@ import { useState, useEffect } from 'react';
export default function useSyncedSelectValue(value, onChange) {
const [options, setOptions] = useState([]);
const [selections, setSelections] = useState([]);
const isMounted = useIsMounted();
useEffect(() => {
if (!isMounted.current) {
return;
}
const newOptions = [];
if (value !== selections && options.length) {
const syncedValue = value.map(item => {
@ -41,7 +46,11 @@ export default function useSyncedSelectValue(value, onChange) {
selections: options.length ? addToStringToObjects(selections) : [],
onSelect,
options,
setOptions: newOpts => setOptions(addToStringToObjects(newOpts)),
setOptions: newOpts => {
if (isMounted.current) {
setOptions(addToStringToObjects(newOpts));
}
},
};
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { object } from 'prop-types';
@ -7,6 +7,7 @@ import { InventoriesAPI, CredentialTypesAPI } from '../../../api';
import ContentLoading from '../../../components/ContentLoading';
import InventoryForm from '../shared/InventoryForm';
import { getAddedAndRemoved } from '../../../util/lists';
import useIsMounted from '../../../util/useIsMounted';
function InventoryEdit({ inventory }) {
const [error, setError] = useState(null);
@ -14,10 +15,9 @@ function InventoryEdit({ inventory }) {
const [contentLoading, setContentLoading] = useState(true);
const [credentialTypeId, setCredentialTypeId] = useState(null);
const history = useHistory();
const isMounted = useRef(null);
const isMounted = useIsMounted();
useEffect(() => {
isMounted.current = true;
const loadData = async () => {
try {
const [
@ -47,9 +47,7 @@ function InventoryEdit({ inventory }) {
}
};
loadData();
return () => {
isMounted.current = false;
};
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [inventory.id, contentLoading, inventory, credentialTypeId]);
const handleCancel = () => {

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@ -23,6 +23,7 @@ import Popover from '../../../components/Popover';
import useRequest from '../../../util/useRequest';
import { InventorySourcesAPI } from '../../../api';
import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails';
import useIsMounted from '../../../util/useIsMounted';
function InventorySourceDetail({ inventorySource, i18n }) {
const {
@ -57,7 +58,7 @@ function InventorySourceDetail({ inventorySource, i18n }) {
} = inventorySource;
const [deletionError, setDeletionError] = useState(false);
const history = useHistory();
const isMounted = useRef(null);
const isMounted = useIsMounted();
const {
result: sourceChoices,
@ -75,11 +76,7 @@ function InventorySourceDetail({ inventorySource, i18n }) {
);
useEffect(() => {
isMounted.current = true;
fetchSourceChoices();
return () => {
isMounted.current = false;
};
}, [fetchSourceChoices]);
const handleDelete = async () => {

View File

@ -47,6 +47,7 @@ import {
removeParams,
getQSConfig,
} from '../../../util/qs';
import useIsMounted from '../../../util/useIsMounted';
const QS_CONFIG = getQSConfig('job_output', {
order_by: 'start_line',
@ -275,10 +276,10 @@ const cache = new CellMeasurerCache({
function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
const location = useLocation();
const listRef = useRef(null);
const isMounted = useRef(false);
const previousWidth = useRef(0);
const jobSocketCounter = useRef(0);
const interval = useRef(null);
const isMounted = useIsMounted();
const history = useHistory();
const [contentError, setContentError] = useState(null);
const [cssMap, setCssMap] = useState({});
@ -292,7 +293,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
const [results, setResults] = useState({});
useEffect(() => {
isMounted.current = true;
loadJobEvents();
if (isJobRunning(job.status)) {
@ -319,7 +319,6 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
ws.close();
}
clearInterval(interval.current);
isMounted.current = false;
};
}, [location.search]); // eslint-disable-line react-hooks/exhaustive-deps

View File

@ -45,6 +45,7 @@ import { JobTemplatesAPI } from '../../../api';
import LabelSelect from './LabelSelect';
import PlaybookSelect from './PlaybookSelect';
import WebhookSubForm from './WebhookSubForm';
import useIsMounted from '../../../util/useIsMounted';
const { origin } = document.location;
@ -67,6 +68,7 @@ function JobTemplateForm({
const [enableWebhooks, setEnableWebhooks] = useState(
Boolean(template.webhook_service)
);
const isMounted = useIsMounted();
const [askInventoryOnLaunchField] = useField('ask_inventory_on_launch');
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
@ -119,8 +121,11 @@ function JobTemplateForm({
return;
}
const { data } = await JobTemplatesAPI.readInstanceGroups(template.id);
setFieldValue('initialInstanceGroups', data.results);
setFieldValue('instanceGroups', [...data.results]);
if (isMounted.current) {
setFieldValue('initialInstanceGroups', data.results);
setFieldValue('instanceGroups', [...data.results]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setFieldValue, template])
);

View File

@ -4,8 +4,12 @@ import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
import { t } from '@lingui/macro';
import { LabelsAPI } from '../../../api';
import { useSyncedSelectValue } from '../../../components/MultiSelect';
import useIsMounted from '../../../util/useIsMounted';
async function loadLabelOptions(setLabels, onError) {
async function loadLabelOptions(setLabels, onError, isMounted) {
if (!isMounted.current) {
return;
}
let labels;
try {
const { data } = await LabelsAPI.read({
@ -32,11 +36,12 @@ async function loadLabelOptions(setLabels, onError) {
function LabelSelect({ value, placeholder, onChange, onError, createText }) {
const [isLoading, setIsLoading] = useState(true);
const [isExpanded, setIsExpanded] = useState(false);
const isMounted = useIsMounted();
const { selections, onSelect, options, setOptions } = useSyncedSelectValue(
value,
onChange
);
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = toggleValue => {
setIsExpanded(toggleValue);
@ -44,7 +49,10 @@ function LabelSelect({ value, placeholder, onChange, onError, createText }) {
useEffect(() => {
(async () => {
await loadLabelOptions(setOptions, onError);
await loadLabelOptions(setOptions, onError, isMounted);
if (!isMounted.current) {
return;
}
setIsLoading(false);
})();
/* eslint-disable-next-line react-hooks/exhaustive-deps */

View File

@ -0,0 +1,12 @@
import { useEffect, useRef } from 'react';
export default function useIsMounted() {
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
});
return isMounted;
}

View File

@ -1,10 +1,11 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import {
parseQueryString,
replaceParams,
encodeNonDefaultQueryString,
} from './qs';
import useIsMounted from './useIsMounted';
/*
* The useRequest hook accepts a request function and returns an object with
@ -22,14 +23,7 @@ export default function useRequest(makeRequest, initialValue) {
const [result, setResult] = useState(initialValue);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const isMounted = useRef(null);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const isMounted = useIsMounted();
return {
result,