Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cannot perform a react state update on an unmounted component #6704

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2c8edb3
Fix cannot perform a React state update on an unmounted component
chantal-kelm May 23, 2024
5f5ea31
Fix cannot perform a React state update on an unmounted component in …
chantal-kelm May 23, 2024
10d0655
Fix cannot perform a React state update on an unmounted component in …
chantal-kelm May 23, 2024
9dce1d2
fix cannot perform a react status update on an unmounted component in…
chantal-kelm May 24, 2024
9329e41
Merge branch '4.9.0' into fix/6693-cannot-perform-a-react-state-updat…
chantal-kelm May 24, 2024
011ee29
Fix cannot perform a React state update on an unmounted component in …
chantal-kelm May 28, 2024
55a4cd7
Fix cannot perform a React state update on an unmounted component in …
chantal-kelm May 28, 2024
62d8275
Fix cannot perform a React state update on an unmounted component in …
chantal-kelm May 28, 2024
63b284f
Fix cannot perform a React state update on an unmounted component in …
chantal-kelm May 28, 2024
d99ab90
Merge branch '4.9.0' into fix/6693-cannot-perform-a-react-state-updat…
chantal-kelm May 28, 2024
08b0da5
merge with 4.9.0
chantal-kelm Jun 3, 2024
4a9b93b
create and implement useIsMounted
chantal-kelm Jun 3, 2024
5d28496
Update hook useIsMounted
chantal-kelm Jun 5, 2024
57cee28
Update unit test of hook useIsMounted
chantal-kelm Jun 5, 2024
e7311c1
Troubleshoot status update issues on disassembled components in vario…
chantal-kelm Jun 6, 2024
598a413
Merge branch '4.9.0' into fix/6693-cannot-perform-a-react-state-updat…
chantal-kelm Jun 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 40 additions & 24 deletions plugins/main/public/components/agents/stats/agent-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
import { getErrorOrchestrator } from '../../../react-services/common-services';
import { endpointSummary } from '../../../utils/applications';
import { getCore } from '../../../kibana-services';
import { useIsMounted } from '../../common/hooks/use-is-mounted';

const tableColumns = [
{
Expand Down Expand Up @@ -145,46 +146,61 @@ export const MainAgentStats = compose(
)(AgentStats);

function AgentStats({ agent }) {
const [loading, setLoading] = useState();
const [loading, setLoading] = useState(false);
const [dataStatLogcollector, setDataStatLogcollector] = useState({});
const [dataStatAgent, setDataStatAgent] = useState();

const { isComponentMounted, getAbortController } = useIsMounted();

useEffect(() => {
(async function () {
const fetchData = async () => {
setLoading(true);
try {
const signal = getAbortController().signal;

const responseDataStatLogcollector = await WzRequest.apiReq(
'GET',
`/agents/${agent.id}/stats/logcollector`,
{},
{ signal },
);
const responseDataStatAgent = await WzRequest.apiReq(
'GET',
`/agents/${agent.id}/stats/agent`,
{},
);
setDataStatLogcollector(
responseDataStatLogcollector?.data?.data?.affected_items?.[0] || {},
);
setDataStatAgent(
responseDataStatAgent?.data?.data?.affected_items?.[0] || undefined,
{ signal },
);

if (isComponentMounted()) {
setDataStatLogcollector(
responseDataStatLogcollector?.data?.data?.affected_items?.[0] || {},
);
setDataStatAgent(
responseDataStatAgent?.data?.data?.affected_items?.[0] || undefined,
);
}
} catch (error) {
const options: UIErrorLog = {
context: `${AgentStats.name}.useEffect`,
level: UI_LOGGER_LEVELS.ERROR as UILogLevel,
severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity,
error: {
error: error,
message: error.message || error,
title: error.name || error,
},
};
getErrorOrchestrator().handleError(options);
if (isComponentMounted()) {
const options = {
context: `${AgentStats.name}.useEffect`,
level: UI_LOGGER_LEVELS.ERROR as UILogLevel,
severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity,
error: {
error: error,
message: error.message || error,
title: error.name || error,
},
};
getErrorOrchestrator().handleError(options);
}
} finally {
setLoading(false);
if (isComponentMounted()) {
setLoading(false);
}
}
})();
}, []);
};

fetchData();
}, [agent.id]);

return (
<EuiPage>
<EuiPageBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
tFilterManager,
} from '../index';
import { PinnedAgentManager } from '../../../wz-agent-selector/wz-agent-selector-service';
import { useIsMounted } from '../../../common/hooks/use-is-mounted';

type tUseDataSourceProps<T extends object, K extends PatternDataSource> = {
DataSource: IDataSourceFactoryConstructor<K>;
Expand Down Expand Up @@ -70,6 +71,8 @@ export function useDataSource<
const pinnedAgentManager = new PinnedAgentManager();
const pinnedAgent = pinnedAgentManager.getPinnedAgent();

const { isComponentMounted, getAbortController } = useIsMounted();

const setFilters = (filters: tFilter[]) => {
if (!dataSourceFilterManager) {
return;
Expand All @@ -83,7 +86,8 @@ export function useDataSource<
if (!dataSourceFilterManager) {
return;
}
return await dataSourceFilterManager?.fetch(params);
const paramsWithSignal = { ...params, signal: getAbortController().signal };
return await dataSourceFilterManager.fetch(paramsWithSignal);
};

useEffect(() => {
Expand All @@ -101,28 +105,30 @@ export function useDataSource<
if (!dataSource) {
throw new Error('No valid data source found');
}
setDataSource(dataSource);
const dataSourceFilterManager = new PatternDataSourceFilterManager(
dataSource,
initialFilters,
injectedFilterManager,
initialFetchFilters,
);
// what the filters update
subscription = dataSourceFilterManager.getUpdates$().subscribe({
next: () => {
// this is necessary to remove the hidden filters from the filter manager and not show them in the search bar
dataSourceFilterManager.setFilters(
dataSourceFilterManager.getFilters(),
);
setAllFilters(dataSourceFilterManager.getFilters());
setFetchFilters(dataSourceFilterManager.getFetchFilters());
},
});
setAllFilters(dataSourceFilterManager.getFilters());
setFetchFilters(dataSourceFilterManager.getFetchFilters());
setDataSourceFilterManager(dataSourceFilterManager);
setIsLoading(false);
if (isComponentMounted()) {
setDataSource(dataSource);
const dataSourceFilterManager = new PatternDataSourceFilterManager(
dataSource,
initialFilters,
injectedFilterManager,
initialFetchFilters,
);
subscription = dataSourceFilterManager.getUpdates$().subscribe({
next: () => {
if (isComponentMounted()) {
dataSourceFilterManager.setFilters(
dataSourceFilterManager.getFilters(),
);
setAllFilters(dataSourceFilterManager.getFilters());
setFetchFilters(dataSourceFilterManager.getFetchFilters());
}
},
});
setAllFilters(dataSourceFilterManager.getFilters());
setFetchFilters(dataSourceFilterManager.getFetchFilters());
setDataSourceFilterManager(dataSourceFilterManager);
setIsLoading(false);
}
})();

return () => subscription && subscription.unsubscribe();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { renderHook } from '@testing-library/react-hooks';

import { useIsMounted } from './use-is-mounted';

describe('useIsMounted()', () => {
it('should return true when component is mounted', () => {
const { result } = renderHook(() => useIsMounted());

expect(result.current.isComponentMounted()).toBe(true);
});

it('should return false when component is unmounted', () => {
const { result, unmount } = renderHook(() => useIsMounted());

unmount();

expect(result.current.isComponentMounted()).toBe(false);
});
});
26 changes: 26 additions & 0 deletions plugins/main/public/components/common/hooks/use-is-mounted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCallback, useEffect, useRef } from 'react';

export const useIsMounted = () => {
const isMounted = useRef(false);
const abortControllerRef = useRef(new AbortController());

useEffect(() => {
isMounted.current = true;

return () => {
isMounted.current = false;
abortControllerRef.current.abort();
};
}, []);

const getAbortController = useCallback(() => {
if (!isMounted.current) {
abortControllerRef.current = new AbortController();
}
return abortControllerRef.current;
}, []);

const isComponentMounted = useCallback(() => isMounted.current, []);

return { isComponentMounted, getAbortController };
};
59 changes: 35 additions & 24 deletions plugins/main/public/components/common/hooks/useGenericRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,44 @@ import {
export function useGenericRequest(method, path, params, formatFunction) {
const [items, setItems] = useState({});
const [isLoading, setisLoading] = useState(true);
const [error, setError] = useState("");
const [error, setError] = useState('');

useEffect( () => {
try{
setisLoading(true);
const fetchData = async() => {
const response = await GenericRequest.request(method, path, params);
useEffect(() => {
let isMounted = true;

const fetchData = async () => {
try {
setisLoading(true);
const response = await GenericRequest.request(method, path, params);
if (isMounted) {
setItems(response);
setisLoading(false);
}
fetchData();
} catch(error) {
setError(error);
setisLoading(false);
const options: UIErrorLog = {
context: `${useGenericRequest.name}.fetchData`,
level: UI_LOGGER_LEVELS.ERROR as UILogLevel,
severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity,
error: {
error: error,
message: error.message || error,
title: error.name || error,
},
};
getErrorOrchestrator().handleError(options);
}
}, [params]);
} catch (error) {
if (isMounted) {
setError(error);
setisLoading(false);
const options: UIErrorLog = {
context: `${useGenericRequest.name}.fetchData`,
level: UI_LOGGER_LEVELS.ERROR as UILogLevel,
severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity,
error: {
error: error,
message: error.message || error,
title: error.name || error,
},
};
getErrorOrchestrator().handleError(options);
}
}
};

fetchData();

return () => {
isMounted = false;
};
}, [method, path, params]);

return {isLoading, data: formatFunction(items), error};
return { isLoading, data: formatFunction(items), error };
}
39 changes: 28 additions & 11 deletions plugins/main/public/components/common/modules/main-agent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,31 @@ import {
} from '../data-source';
import { useAsyncAction } from '../hooks';

export class MainModuleAgent extends Component {
props!: {
[key: string]: any;
};
state: {
selectView: Boolean;
loadingReport: Boolean;
switchModule: Boolean;
showAgentInfo: Boolean;
};
interface MainModuleAgentProps {
agent: any;
section: string;
selectView: boolean;
tabs?: any[];
renderTabs: () => React.ReactNode;
}

interface MainModuleAgentState {
selectView: boolean;
loadingReport: boolean;
switchModule: boolean;
showAgentInfo: boolean;
}

export class MainModuleAgent extends Component<
MainModuleAgentProps,
MainModuleAgentState
> {
isMounted: boolean;
reportingService: ReportingService;
filterHandler: FilterHandler;
router: any;

constructor(props) {
constructor(props: MainModuleAgentProps) {
super(props);
this.reportingService = new ReportingService();
this.filterHandler = new FilterHandler(AppState.getCurrentPattern());
Expand All @@ -62,13 +73,19 @@ export class MainModuleAgent extends Component {
switchModule: false,
showAgentInfo: false,
};
this.isMounted = false;
}

async componentDidMount() {
this.isMounted = true;
const $injector = getAngularModule().$injector;
this.router = $injector.get('$route');
}

componentWillUnmount() {
this.isMounted = false;
}

renderTitle() {
return (
<EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ export function getForceNow() {

////////////////////////////////////////////////////////////////////////////////////

interface Options {
signal?: AbortSignal;
}

export const search = async (
params: SearchParams,
options: Options = {},
): Promise<SearchResponse | void> => {
const {
indexPattern,
Expand Down Expand Up @@ -123,7 +128,7 @@ export const search = async (
searchSource.setField('aggs', aggs);
}
try {
return await searchParams.fetch();
return await searchParams.fetch({ signal: options.signal });
} catch (error) {
if (error.body) {
throw error.body;
Expand Down
Loading
Loading