reactjs - React 上下文提供程序中的陈旧闭包
问题描述
我有一个 React Native 应用程序,我想在其中创建一种方法来启用/禁用来自应用程序流中多个位置的通知。启用通知还涉及保留一些取消订阅处理程序,这些处理程序应在禁用期间调用以进行适当的清理。它还需要写入/读取到/从 react-redux 存储。
为此,我创建了一个上下文来与应用程序中的任意组件共享功能。您可以在下面看到一个简化版本。
我似乎遇到了一个过时的关闭问题,我不知道如何正确解决。我添加了解释代码中内联问题的注释。
如果这是错误的方法,我也会很高兴了解如何以更好的方式解决所述用例。
interface NotificationContextProps {
enableNotifications: (promptForPermission: boolean) => Promise<void>;
disableNotifications: () => void;
}
const SharedNotificationContext = React.createContext<NotificationContextProps>({
enableNotifications: async () => {/**/},
disableNotifications: () => {/**/},
});
export const SharedNotificationProvider: FunctionComponent = ({children}) => {
const notificationListenerUnsub = useRef(() => {/**/});
const tokenRefreshListenerUnsub = useRef(() => {/**/});
// PROBLEM: Would have liked to use `useState` here instead of useRef
// but the value change never reflected in the functions using the value.
// Probably same stale closure issue as the other PROBLEM.
const isEnabled = useRef(false);
// PROBLEM: When udating the state with the `registerToken` function, this gets
// updated correctly. When I place a `console.log(existingDeviceToken);` below this,
// I can see that the change propagates here correctly.
// BUT the `registerDeviceToken` function which does a check if the token changed,
// (called by `enableNotifications` which is called on every app state change)
// still uses the "old" `existingDeviceToken value` for the comparison
const existingDeviceToken = useSelector((state: any) => state.firebase.token);
const dispatch = useDispatch();
const registerToken = (token: string) => dispatch({ type: FirebaseEvents.TOKEN, payload: token });
const registerDeviceToken = async () => {
const deviceToken = await getFirebaseToken();
// PROBLEM: Even though the `existingDeviceToken` changed,
// it's still using the old value.
if (deviceToken && deviceToken !== existingDeviceToken) {
registerToken(deviceToken);
}
};
const enableNotifications = async (promptForPermission: boolean) => {
const permissionStatus = await getPushPermission(promptForPermission);
if (permissionStatus.hasPermission && !isEnabled.current) {
// Has Push Permissions & Listeners have not yet been setup
cleanupNotificationHandlers();
await registerDeviceToken();
await updateNotificationListeners();
isEnabled.current = true;
} else if (permissionStatus.hasPermission && isEnabled.current) {
// Has Push Permissions but Listeners already exist
await registerDeviceToken();
} else if (permissionStatus.authStatus === messaging.AuthorizationStatus.DENIED) {
// Push Permissions were declined or removed
disableNotifications(true);
}
};
const disableNotifications = async (permissionsDeclined: boolean) => {
await removeFirebaseToken();
registerToken("");
cleanupNotificationHandlers();
isEnabled.current = false;
}
const updateNotificationListeners = async () => {
// Create listener and store the unsub function (simplified for demo)
notificationListenerUnsub.current = () => {};
tokenRefreshListenerUnsub.current = () => {};
};
const cleanupNotificationHandlers = () => {
notificationListenerUnsub.current();
notificationListenerUnsub.current = () => {/**/};
tokenRefreshListenerUnsub.current();
tokenRefreshListenerUnsub.current = () => {/**/};
isEnabled.current = false;
};
return (
<SharedNotificationContext.Provider value={{
enableNotifications,
disableNotifications
}}>
{children}
</SharedNotificationContext.Provider>
);
};
export const useNotifications = () => {
return React.useContext(SharedNotificationContext);
};
我已经厌倦了用useCallback
这样的包装函数:
const registerDeviceToken = useCallback(async () => {/**/}, [existingDeviceToken]);
const enableNotifications = useCallback(async (promptForPermission: boolean) => {/**/}, [registerDeviceToken]);
但这并没有帮助。
existingDeviceToken
起作用的是将a打包useRef
,然后使用 a
useEffect(() => {existingDeviceTokenRef.current = existingDeviceToken}, [existingDeviceToken]);
更新它。但这确实感觉它只是一种解决方法,而不是真正的解决方案。
正如您可能已经收集到的,React 对我来说仍然是新的,所以我也不是 100% 确定如何做到这一点,以尽量减少过多地更改 value 属性,这样提供者就不会导致应用程序重新渲染。
解决方案
我怀疑您在使用时缺少依赖项,enableNotifications
或者disableNotifications
使用任何方法在您的问题中没有代码。
这是仅在现有设备令牌更改时创建函数的代码。
export const SharedNotificationProvider = ({
children,
}) => {
const [, setState] = useState({
notificationListenerUnsub: (x) => x,
tokenRefreshListenerUnsub: (x) => x,
});
//best to keep this in a ref so you don't create
// a needless dependency
const isEnabled = useRef(false);
const existingDeviceToken = useSelector(
(state) => state.firebase.token
);
const dispatch = useDispatch();
const registerToken = useCallback(
(token) =>
dispatch({
type: FirebaseEvents.TOKEN,
payload: token,
}),
[dispatch]
);
//all these functions are created on mount and only
// change when existingDeviceToken changes
const registerDeviceToken = useCallback(async () => {
const deviceToken = await getFirebaseToken();
if (
deviceToken &&
deviceToken !== existingDeviceToken
) {
registerToken(deviceToken);
}
}, [existingDeviceToken, registerToken]);
const cleanupNotificationHandlers = useCallback(
() =>
setState((state) => {
state.notificationListenerUnsub();
state.tokenRefreshListenerUnsub();
return {
notificationListenerUnsub: () => {
/**/
},
tokenRefreshListenerUnsub: () => {
/**/
},
isEnabled: false,
};
}),
[] //no deps, this method never changes after mount
);
const disableNotifications = useCallback(
async (permissionsDeclined) => {
await removeFirebaseToken();
registerToken('');
cleanupNotificationHandlers();
setState((state) => ({ ...state, isEnabled: false }));
},
[cleanupNotificationHandlers, registerToken]
);
const updateNotificationListeners = useCallback(
async () =>
// Create listener and store the unsub function (simplified for demo)
setState((state) => ({
...state,
notificationListenerUnsub: () => {},
tokenRefreshListenerUnsub: () => {},
})),
[] //no dependencies, function never changes
);
const enableNotifications = useCallback(
async (promptForPermission) => {
const permissionStatus = await getPushPermission(
promptForPermission
);
if (
permissionStatus.hasPermission &&
!isEnabled.current
) {
// Has Push Permissions & Listeners have not yet been setup
cleanupNotificationHandlers();
await registerDeviceToken();
await updateNotificationListeners();
isEnabled.current = true;
} else if (
permissionStatus.hasPermission &&
isEnabled.current
) {
// Has Push Permissions but Listeners already exist
await registerDeviceToken();
} else if (
permissionStatus.authStatus ===
messaging.AuthorizationStatus.DENIED
) {
// Push Permissions were declined or removed
disableNotifications(true);
}
},
[
cleanupNotificationHandlers,
disableNotifications,
registerDeviceToken,
updateNotificationListeners,
]
);
return (
<SharedNotificationContext.Provider
value={{
enableNotifications,
disableNotifications,
}}
>
{children}
</SharedNotificationContext.Provider>
);
};
推荐阅读
- python - pivot_table 中的错误:Dropna=False:数组太大;`arr.size * arr.dtype.itemsize` 大于最大可能大小
- javascript - HTML CSS JS,无法打开我制作的实时代码编辑器
- javascript - 如何随机替换特定 URL 的 HREF?innerHTML 影响延迟加载
- pandas - 从日期范围中删除几个日期
- django - 芹菜时间表
- android - 如何拦截Android中的所有onClick事件?
- string - 如何从 Rust 中的另一个字符串中删除单个尾随字符串?
- c# - 来自测试的 Autofac RegisterAssemblyTypes
- pdf - Netsuite 发票 Pdf 显示应用金额
- python - Python如何获取一些property.setter的绑定方法