Best practice for logging out of React Native app? - react-native

I have currently implemented the sign out functionality on my app with (using firebase authentication):
const handleSignOut = () => {
signOut(auth)
.then(() => navigation.navigate("Login"))
.catch((err) => console.error(err));
};
However, I understand this isn't enough as I notice that some state is being preserved accross users (state is preserved even after logout), and I was wondering what the best way to clear/reset all the state is.
Do I need to manually unmount all my components (I still don't quite understand how/when component unmounting happens), or is there an easy way I can achieve this using navigation or context tools?
I am using the Context API and React Navigation:
const Stack = createStackNavigator();
const Navigation = () => {
return (
<NavigationContainer>
<Stack.Navigator
headerMode="none"
>
<Stack.Screen
name="Register"
component={RegisterScreen}
/>
<Stack.Screen
name="Login"
component={LoginScreen}
/>
<Stack.Screen
name="Home"
component={HomeScreen}
/>
</Stack.Navigator>
</NavigationContainer>
);
};

It's hard to say because a lot of this is application specific, but generally speaking, you need to dedicate a single method which is capable destroying a user's session, and making sure that's persistent. Maybe it's logging out of Firebase, maybe it's clearing some cookies, maybe it's updating some other persistent state.
The point is, you write a single method which is responsible for performing all of the data actions to make sure that session doesn't exist any more. Importantly, it's not associated with any navigation or whatever. It's just the pure data operations.
Then you have this reusable function to log the user out that can be called from wherever. Applied to your example...
// Notice that things that can possibly fail are wrapped in dedicated try/catches.
// You don't want the failure to clear some state prevent your ability to clear others.
const doSomeBigGlobalSessionLogout = async () => {
const maybeErrors = [];
try {
await firebase.signOut(auth);
} catch (e) {
maybeErrors.push(e);
}
try {
await clearSomeOtherPersistentState();
} catch (e) {
maybeErrors.push(e);
}
/* do something with errors */
// for (error of maybeErrors) { ... }
// Still resolve successfully, because you won't want to lock the user from
// not being able to sign out.
return Promise.resolve();
};
// Here you can trust it will never lock the user out, and will always clear state as best as it can.
doSomeBigGlobalSessionLogout()
// You *will* get here.
.then(() => navigation.navigate("Login"));
Finally in terms of the UI, it would probably make sense to move the user first. Otherwise the user would see their state disappear from the screen before their eyes!
Promise.resolve()
.then(() => navigation.navigate("SomePlaceThatsPretty"));
.then(doSomeBigGlobalSessionLogout)
.then(() => navigation.navigate("Login"));

Related

Next.JS Abort fetching component for route: "/login"

I was developing a useUser Hook for per-page authentication. I have implemented the useUser hook normally and Redirecting works fine accordingly.
But I am getting the above error.
Abort fetching component for route: "/login"
How can I fix useUserHook to solve it??
//useUser.tsx
const useUser = ({ redirectTo, redirectIfFound }: IParams) => {
const { data, error } = useRequest("authed", isAuthed);
const user = data?.data;
const hasUser = user;
useEffect(() => {
if (!redirectTo) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !hasUser) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && hasUser)
) {
Router.push(redirectTo);
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
//index.tsx
const Home: NextPage = () => {
const user = useUser({ redirectTo: "/login" });
if (user === undefined || user === false) {
return <div>Loading...</div>;
}
return (
<div>
<Head>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div>Home</div>
</div>
);
};
UseRequest Hook returns true and false as return values.
tl;dr
Ensure that you only call router.push() once throughout all potential re-executions of useEffect with the help of state:
const [calledPush, setCalledPush] = useState(false); // <- add this state
// rest of your code [...]
useEffect(() => {
if (!redirectTo) return;
if (
(redirectTo && !redirectIfFound && !hasUser) ||
(redirectIfFound && hasUser)
) {
// check if we have previously called router.push() before redirecting
if (calledPush) {
return; // no need to call router.push() again
}
Router.push(redirectTo);
setCalledPush(true); // <-- toggle 'true' after first redirect
}
}, [redirectTo, redirectIfFound, hasUser]);
return error ? null : user;
};
Background
useEffect potentially gets called multiple times if you have more than one dependency (Also happens with React Strict Mode enabled, but in this case there seems to be no error), and (re-)calling router.push() multiple times within the same Next.js page in different places/throughout different re-renders seems to cause this error in some cases, as the redundant router.push() call(s) will have to be aborted, because the current page-component unmounts due to the successful, previously called router.push().
If we keep track of whether we have already called router.push via the calledPush state as in the code snippet above, we omit all redundant router.push() calls in potential useEffect re-executions, because for all subsequent useEffect executions the state value calledPush will already be updated to true as useEffect gets triggered after re-renders, hence after setCalledPush(true) takes effect.
In my case I have use rotuer.push("/") two times in a single file. That caused the error. Try using one. I think problem will be solved.
This error occurs because useEffect tries to update a component that has already been unmounted and this can introduce memory leaks in which your app uses more memory than it needs to. To prevent this, use the following approach:
useEffect(() => {
//first manually mount the effect
let mounted = true;
//check if component is currently mounted
if(mounted && ...code){
router.push('/index')
}
//cleanup side effects before unmounting
return()=>{mounted=false}
}, [router]);
};
In my current NextJS project, when I make reactStrictMode: false,, the value to false, then it seems like the re-rendering will be gone, and component will be only rendered once.
I don't like react strict mode very much ..

apollo-client: makeVar / useReactiveVar can be updated from another screen?

I use react-native with apollo-client3.
In my App.js file that starts my project, I configure if I log in go to LoggedInNav else LoggedOutNav.
<ApolloProvider client={client}>
<NavigationContainer>
<View onLayout={onLayoutRootView}></View>
{isLoggedIn ? <LoggedInNav /> : <LoggedoutNav />}
</NavigationContainer>
</ApolloProvider>
And the state whether I log in or not is tracked from useReactiveVar.
const isLoggedIn = useReactiveVar(isLoggedInVar);
In my LogIn.js file, I do log in and if it succeeds, I update my isLoggedInVar state made from makeVar of apollo/client from false to true.
export const isLoggedInVar = makeVar(false);
export const tokenVar = makeVar("");
export const logUserIn = async (token) => {
try {
await AsyncStorage.setItem("token", JSON.stringify(token));
isLoggedInVar(true);
tokenVar(token);
} catch (e) {
console.error(e);
}
};
So I update my state of makeVar in another screen, but can App.js listen to this update and navigate from <LoggedInNav/> to <LoggedOutNav/>?
I think App.js file is the bottom of all screens of my app, it should work.
But in my app project, I feel sometimes it might works but sometimes it throws Network error.
And in this case How can I make switch between two Navs according to Login state? Please help me.

How can I prevent going back in react-native-webview?

My app uses react-native-webview and set allowsBackForwardNavigationGestures to true.
I want to prevent going back on a specific page.
For example, after navigating to the home page from the login page, I want to make sure that I cannot go back.
I wonder if blocking back is possible in react-native-webview.
Please help.
I can't be sure but your question sure sounds like a React Navigation issue, particularly concerning Auth. Flow. Well, should that be the case, there's a pretty well written documentation on Auth Flow with good example(s) here
Hope that helps in solving your issue.
I think you could use onShouldStartLoadWithRequest and simply return false when request contains URL you do not want to allow.
From documentation:
<WebView
source={{ uri: 'https://reactnative.dev' }}
onShouldStartLoadWithRequest={(request) => {
// Only allow navigating within this website
return request.url.startsWith('https://reactnative.dev');
}}
/>
You can restrict back button from onNavigationStateChange
onNavigationStateChange={(navState) => {
if (navState.canGoBack) {
navigation.setParams({
headerLeftInfo: {
title: '',
onPress: () => //TODO:back button logic,
},
});
} else {
navigation.setParams({
headerLeftInfo: null,
});
}
}}
Reference: https://www.reactnativeschool.com/integrating-react-navigation-back-button-with-a-webview

My React Native app crashes during Login and Logout ( when context is changed )

I have a react native app and whenever I try to login or logout the app crashes .. I am not getting any error logs in React Native Debugger.
so the App Crashes and then when I reopen the app the changes have already taken effect ( i.e :- If I was trying to login I would be logged in when I reopen the app)
I have 2 files for Navigation
CofficNavigator.tsx
// This file has all the routing
// here BottomNavigator is a tab Navigator and AuthNavigator is a stack navigator
// Before the app crashing all the correct state is set but it just crashes
// ----------- Final Stack Navigator ----------------
const FinalStackNavigator = createStackNavigator();
export const FinalNavigator = (props: any) => {
const authContext = useContext(AuthContext);
const [signedIn, setSignedIn] = useState<boolean | null>(
authContext.isLoggedIn
);
useEffect(() => {
if (authContext.isLoggedIn !== signedIn && !authContext.isLoading) {
setSignedIn(authContext.isLoggedIn);
}
}, [authContext]);
return (
<FinalStackNavigator.Navigator>
{signedIn ? (
<>
<FinalStackNavigator.Screen
name="BottomNavigator"
component={BottomNavigator}
options={{ headerShown: false }}
/>
</>
) : (
<>
<FinalStackNavigator.Screen
name="AuthNavigator"
component={AuthNavigator}
/>
</>
)}
</FinalStackNavigator.Navigator>
);
};
AppNavigator.tsx
export const AppNavigator = (props: any) => {
return (
<NavigationContainer>
<FinalNavigator />
</NavigationContainer>
);
};
-------- Login Logic flow -------------
on success of login API I call this line
await authContext.changeCofficToken(result.data.login.token);
AuthContext.ts
const changeCofficToken = async (token: string) => {
await setItem("cofficToken", token);
setIsLoggedIn(true);
setCofficToken(token);
};
helper.ts
export const setItem = async (key: string, value: any) => {
await AsyncStorage.setItem(key, value);
};
App.tsx
return (
<AuthContextProvider>
<FilterContextProvider>
<SearchTermContextProvider>
<ShowVerticalListContextProvider>
<CouponContextProvider>
<ApolloProvider client={client}>
<AppNavigator />
</ApolloProvider>
</CouponContextProvider>
</ShowVerticalListContextProvider>
</SearchTermContextProvider>
</FilterContextProvider>
</AuthContextProvider>
);
------ End of Login Logic code flow -------
Error logs from my IOS Simulator
com.apple.CoreSimulator.SimDevice.957FA264-501C-44C3-80F3-9E8D1F600A1E[15500] (UIKitApplication:host.exp.Exponent[e036][rb-legacy][15601]): Service exited due to SIGABRT
assertion failed: 19F101 17F61: libxpc.dylib + 83746 [ED46009E-B942-37CC-95D4-82CF3FE90BD8]: 0x7d
Please Note :- I am using Expo if it matters
Video Link :- https://fisico-dhaval.s3.ap-south-1.amazonaws.com/Screen+Recording+2020-08-20+at+5.38.06+PM.mov
If you need any other code snippets then please do lemme know
Any help will be appreciated
Can you share the code where you setSignin. In case if you are saving and clearing token, make sure those libraries are supported and linked properly.
Service exited due to SIGABRT is a common issue in Native iOS development, it can be cause due to various events. While working on react-native, I have never experienced this error. But I'm adding some of my observation, may be that can help you,
This can be cause due to some buggy component, which are not compatible with Expo version. So, you need to add many console.log or may be comment some things and check is there any functionality or a component causing this error. A buggy component can be a culprit.
If you have pod installed, Make sure you check all your libraries.You likely have some issue with an installed external library. and make sure you run pod install and delete node_modules folder and then run npm install again (close the packager too)
You can also use sentry or any other crash reporting library to detect crashes, It will give you the exact line number.
Sometimes the crash reporting libraries won't work, the reason can be that the crash occurs before the js bundle is loaded and because of that, the library is not yet initialised and it can't detect the crash. If so you can install a native crash reporting library, You can use fabric for that, or even bugsnag.
These are few scenarios, I've observed via which you can debug or detect what's the real cause of this error.

Preloading assets in React-Native expo app never finishes

I'm writing a simple app with React Native and Expo.
This app has ~10 small to medium sized images that are used in different places within the app.
From what I read, unless I cache these images, they will be required to be downloaded from expo each time.
For this reason, I have noticed that they seem to load in really slowly when testing the app. Upon building and navigating through the app, I find that it takes a few seconds for my images to pop up even after the rest of the page has loaded.
I followed the setup as seen in the starting template.
Here is what my App.js looks like (I am using react-navigation so it varies from the sample file above):
export default class App extends React.Component {
state = {
isLoadingComplete: false
};
componentDidMount() {
StatusBar.setHidden(true);
}
render() {
_loadResourcesAsync = async () => {
return Promise.all([
Asset.loadAsync([
require("./assets/syria.png"),
require("./assets/lebanon.png"),
require("./assets/kenya.png"),
require("./assets/indonesia.png"),
require("./assets/somalia.png"),
require("./assets/india.png"),
require("./assets/america.png"),
require("./assets/albania.png"),
require("./assets/bosnia.png")
])
]);
};
_handleLoadingError = error => {
Alert.alert(error);
};
_handleFinishLoading = () => {
this.setState({ isLoadingComplete: true });
};
if (this.state.isLoadingComplete == false) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
} else {
return (
<AppContainer
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
);
}
}
}
I have excluded my react-navigation code for the sake of brevity.
When I run this, my app gets stuck at Downloading JavaScript bundle 100.00%.
It seems that the _handleFinishLoading never runs. At least, that's the only reason I can see for it to never finish loading.
Given the small amount of images, I don't know how this could take more than a second. Instead it sits at the splash screen forever.
Any ideas on what I might be doing wrong here?
Found the solution:
I made a simple error. The async functions (_loadResourcesAsync, _handleFinishLoading, etc) need to be outside the render method. Moving them below my render method inside of the app class caused this to work as expected.