Race condition in unsubcription of useEffect in useAbility
See original GitHub issueDescribe the bug
Sporadically I get the following error from useAbility:
react_devtools_backend.js:2842 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Inner (https://localhost:3000/src/components/Control.tsx?t=1628859377104:22:3)
at UiPathContextProvider (https://localhost:3000/src/contexts/UiPathContextProvider.tsx:19:3)
at div
at Control (https://localhost:3000/src/components/Control.tsx?t=1628859377104:43:3)
at InfoField (https://localhost:3000/src/components/numeric/InfoField.tsx?t=1628859377104:22:3)
at EmptyField
at Loading (https://localhost:3000/src/components/RenderTag.tsx?t=1628859377104:41:3)
To Reproduce
When I trigger an ability change via an AbilityContext upstream in the UI tree, every 5 to 10 re-renders the error occurs. useAbility is used by a primitive UI control wrapper component used a lot in my project ( I use CASL ability for controlling write access, which re-renders UI controls to a read-only state when use level is not high enough).
Expected behavior No race condition
Interactive example (optional, but highly desirable) I have a large hierarchy and large parts of the UI tree gets sometimes unmounted as a response of an ability change. So it is really difficult to reproduce this error in a minimal project.
As an alternative I tried already a bug fix which seems to work. I replace the original implementation of the useAbility:
import React from 'react';
import { AnyAbility } from '@casl/ability';
export function useAbility<T extends AnyAbility>(context: React.Context<T>): T {
if (process.env.NODE_ENV !== 'production' && typeof React.useContext !== 'function') {
/* istanbul ignore next */
throw new Error('You must use React >= 16.8 in order to use useAbility()');
}
const ability = React.useContext<T>(context);
const [rules, setRules] = React.useState<T['rules']>();
React.useEffect(() => ability.on('updated', (event) => {
if (event.rules !== rules) {
setRules(event.rules);
}
}), []);
return ability;
}
with this:
import { AnyAbility } from '@casl/ability';
import React, { useContext, useEffect, useRef, useState } from 'react';
function useAbility<T extends AnyAbility>(context: React.Context<T>): T {
if (process.env.NODE_ENV !== 'production' && typeof useContext !== 'function') {
/* istanbul ignore next */
throw new Error('You must use React >= 16.8 in order to use useAbility()');
}
const ability = useContext<T>(context);
const [rules, setRules] = useState<T['rules']>();
const subscribed = useRef(false);
useEffect(() => {
const unsubscribe = ability.on('updated', event => {
if (subscribed.current && event.rules !== rules) {
setRules(event.rules);
}
});
subscribed.current = true;
return function () {
subscribed.current = false;
unsubscribe();
};
}, [ability, rules]);
return ability;
}
export default useAbility;
Basically I currently suspect the race condition somewhere inside the unsubscribe function returned from ability.on and just don’t call setRules (which triggers a setState and the bug) anymore as soon as the component is unmounted.
This fixes the racing condition 100%
CASL Version “@casl/ability”: “^5.4.0”, “@casl/react”: “^2.3.0”,
Environment: NodeJS on Windows, newest Chrome
Issue Analytics
- State:
- Created 2 years ago
- Comments:22 (11 by maintainers)
Top GitHub Comments
Output is:
After unsubscribing the head element the other event handlers are lost
If you unsubscribe any other elements except the head its working
AWESOME! Thank you @gunters63 very much for being so responsive and helpful!