问题描述
我有一个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
打包到useRef
中,然后使用
useEffect(() => {existingDevicetokenRef.current = existingDevicetoken},[existingDevicetoken]);
正如您可能已经收集到的那样,React对我来说仍然是新手,因此我也不是100%知道如何以尽量减少更改价值道具的方式执行此操作,以使提供商不会导致应用重新渲染。
解决方法
我怀疑您在使用enableNotifications
或disableNotifications
时在效果上缺少依赖性,但是使用任何一种方法在您的问题中都没有代码。
这是仅当现有DeviceToken更改时才创建函数的代码。
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,}}
>
{children}
</SharedNotificationContext.Provider>
);
};