How to navigate with react navigation outside react component? - react-native

I'm building a code to check if access_token or refresh_token are valid. I'm using axios interceptors to check the response to generate new token.
How to use navigate(React Navigation) inside axios interceptors?
Error:
09:53:55.852 client_log FarmsList:React.FC -> error [Error: Invalid
hook call. Hooks can only be called inside of the body of a function
component. This could happen for one of the following reasons
axios.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const navigation = useNavigation()
const originalRequest = error.config
const accessToken = await getAccessToken()
const refreshToken = await getRefreshToken()
if (
error.response.status === 400 &&
originalRequest.url === connectTokenUrl &&
accessToken
) {
removeConnectToken()
navigation.navigate('SignIn')
return Promise.reject(error)
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
console.log('entrou 401')
if (!refreshToken) {
navigation.navigate('SignIn')
return Promise.reject(error)
}
const data = {
grant_type: 'refresh_token',
client_id: 'xxx',
refresh_token: refreshToken,
}
const formData = new FormData()
_.forEach(data, (value, key) => {
formData.append(key, value)
})
return axios({
method: 'post',
url: connectTokenUrl,
data: formData,
headers: {'Content-Type': 'multipart/form-data'},
}).then((response) => {
const {access_token, refresh_token} = response.data
connectToken(access_token, refresh_token)
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`
return axios(originalRequest)
})
}
return Promise.reject(error)
},
)

There are several ways to access the navigation props outside the navigation.
The useNavigation hook : this is used for scenarios where you access the navigation prop from functional components which are under the navigation container. Eg : A navigation button which is inside a screen.
The navigationRef : this is used for scenarios where you access the navigation outside the navigation, used for scenarios like redux middleware.
You should use the navgation ref for this scenario and perform your navigation actions. You can use the RootNavigation.js and call the navigation actions.
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
// RootNavigation.js
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}

where is your navigation param? if you can show me more of your code (your full component) will be helpful, its possible that you are calling some hook outside of your functional component

First you have to create a createNavigationContainerRef and a navigate function like this:
// RootNavigation.js
import { createNavigationContainerRef } from '#react-navigation/native';
export const navigationRef = createNavigationContainerRef()
export function navigate(name, params) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
Then add the newly created navigationRef to the NavigationContainer wrapper:
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
You can now import the navigate function to any .js file and use it. Hope this was useful. For further reference, refer this article.

Related

Context API dispatch not called with onEffect while using expo-splash-screen

When I am trying to use the dispatch function recieved with the useContext hook I cannot get the change the content of the data inside the context. It looks like as if the call wasn't even made, when I try to log something inside the conext's reducer it doesn't react. When I try to call it from other components, it works just fine.
Sorry if it's not clean enough, I'm not too used to ask around here, if there's anything else to clarify please tell me, and I'll add the necessary info, I just don't know at the moment what could help.
import { QueryClient, QueryClientProvider } from "react-query";
import LoginPage from "./src/pages/LoginPage";
import { UserDataContext, UserDataProvider } from "./src/contexts/UserData";
import { useState } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { useContext } from "react";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { useCallback } from "react";
import { UserData } from "./src/interfaces";
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();
export default function App() {
const [appReady, setAppReady] = useState<boolean>(false);
const { loggedInUser, dispatch } = useContext(UserDataContext);
useEffect(() => {
async function prepare() {
AsyncStorage.getItem("userData")
.then((result) => {
if (result !== null) {
console.log(loggedInUser);
const resultUser: UserData = JSON.parse(result);
dispatch({
type: "SET_LOGGED_IN_USER",
payload: resultUser,
});
new Promise((resolve) => setTimeout(resolve, 2000));
}
})
.catch((e) => console.log(e))
.finally(() => setAppReady(true));
}
if (!appReady) {
prepare();
}
}, []);
const onLayoutRootView = useCallback(async () => {
if (appReady) {
await SplashScreen.hideAsync();
}
}, [appReady]);
if (!appReady) {
return null;
}
return (
<>
<UserDataProvider>
<QueryClientProvider client={queryClient}>
<LoginPage onLayout={onLayoutRootView} />
</QueryClientProvider>
</UserDataProvider>
</>
);
}
I'm thinking I use the context hook too early on, when I check the type of the dispatch function here it says it's [Function dispatch], and where it works it's [Function bound dispatchReducerAction].
I think the problem might come from me trying to call useContext before the contextprovider could render, but even when I put the block with using the dispatch action in the onLayoutRootView part, it didn't work.

How to fetch data from Amplify's GraphQL API and store it in a React's context variable

I am using React native and I have a context variable post, it has an attribute called name and I have defined a function called onChange to set it.
import React, { useState } from "react";
const PostContext = React.createContext({
content: "",
onChange: (newPostContent: string) => {},
});
export const PostContextProvider = ({ children }) => {
const [content, setContent] = useState("");
const postChangeHandler = (newPostContent: string) => {
setContent(newPostContent);
};
return (
<PostContext.Provider
value={{ content, onChange: postChangeHandler }}
>
{children}
</PostContext.Provider>
);
};
export default PostContext;
Now I have a page on which I want to fetch a post from Amplify's GraphQL API and set its content to my context variable, so I can use it on other pages.
import React, { useEffect, useContext } from "react";
import { API, graphqlOperations} from "aws-amplify";
import PostContext from "./context/post-context";
const post = useContext(PostContext);
const fetchPost = async () => {
const {data: {getPost: { postContent },},} = await API.graphql(
graphqlOperation(`
query GetPost {
getPost(id: "${some post Id}") {
content
}
}
`)
);
post.onChange(postContent)
}
useEffect(()=>{
fetchPost()
}, [])
useEffect(()=>{
console.log(post.content)
}, [post])
What I expect is that in the async function, the execution is blocked until postContent (because of the await and then it's value is assigned to the context variable, or its update is schedualed (that's why I have also included a useEffect to console.log the value of post.content. But it is not updated and its value remains an empty screen. Can somebody help me with this? I am learning React native how this work, so a detailed answer that lets me know what I am doing wrong is appreciated.

Alternative for NavigationActions in react-native v6

I have problem, namely the navigation in this code doesn't work:
import AsyncStorage from "#react-native-async-storage/async-storage";
import createDataContext from "./createDataContext";
import trackerApi from "../api/tracker";
import { navigate } from "./navigationRef";
const authReducer = (state, action) => {
switch (action.type) {
case "add_error":
return { ...state, errorMessage: action.payload };
case "signin":
return { errorMessage: "", token: action.payload };
case "clear_error_message":
return { ...state, errorMessage: "" };
case "signout":
return { token: null, errorMessage: "" };
default:
return state;
}
};
const signup = (dispatch) => async ({ email, username, birth, gender, password }) => {
try {
const response = await trackerApi.post("/signup", { email, username, birth, gender, password });
await AsyncStorage.setItem("token", response.data.token);
dispatch({ type: "signin", payload: response.data.token });
console.log(response.data.token);
navigate("DrawerScreen");
} catch (err) {
dispatch({
type: "add_error",
payload: "Something went wrong with sign up",
});
}
};
export const { Provider, Context } = createDataContext(
authReducer,
{ signin, signout, signup, clearErrorMessage, tryLocalSignin },
{ token: null, errorMessage: "" }
);
"signup" function successfully sends my data to database in mongodb. But after this
The next file is created to help my navigation works. But "NavigationActions" was used in ReactNative v4. I need to change my code to work with RN v6. The following code is pasted below:
import { NavigationActions } from 'react-navigation';
let navigator;
export const setNavigator = nav => {
navigator = nav;
};
export const navigate = (routeName, params) => {
navigator.dispatch(
NavigationActions.navigate({
routeName,
params
})
);
};
Both files are referenced by each other.
To sum up I've tried the solution to use navigation.navigate("MyScreen"), but it doesnt work in signup function. The question is how to change the second file to work with RN6 or how to navigate successfully in this function without the second file?
First you have to import useNavigation
Like this:
import { useNavigation } from "#react-navigation/core";
Then you have to use it and save it in a variable like:
const navigation = useNavigation();
Now use onPress when press on that button to navigate:
onPress={() => navigation.navigate('MyScreen')};
This will navigate to the the other Screen.
Make sure you install every library you use in your project using npm or yarn.
You can get access to the root navigation object through a ref and pass it to the RootNavigation which we will later use to navigate.
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}
</NavigationContainer>
);
}
In the next step, we define RootNavigation, which is a simple module with functions that dispatch user-defined navigation actions.
// RootNavigation.js
import {createNavigationContainerRef} from '#react-navigation/native';
import {StackActions} from '#react-navigation/native';
export const navigationRef = createNavigationContainerRef();
// for navigate
export function navigate(name, params) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
// for replace
export function navigateReplace(name, param) {
if (navigationRef.isReady()) {
navigationRef.dispatch(
StackActions.replace(name, {
param,
}),
);
}
}
// any js module
import * as RootNavigation from './path/to/RootNavigation.js';
then you can navigate like this
RootNavigation.navigateReplace('ChatScreen', { userName: 'Lucy' });
or
RootNavigation.navigate('ChatScreen', { userName: 'Lucy' });
for more details, you can read the documentation
Navigating without the navigation prop

How to use navigation in this function from importing a hook within this function? any idea?

I am using a function to call an api and i have added navigation on 401 and i want to use navigation here. But as the hooks can be called from the component only. So anybody can tell me how can i add navigation here. So someone i can import here and use while passing in the function.
const GetApiRequestWithToken = async (url, params, headers) => {
return new Promise((resolve, reject) => {
axios.get(base_url_address + url, { headers: headers }).then(resp => {
if (resp.status == 401) {
UnAuthorizedLogout()
} else {
resolve(resp)
}
}).catch((error) => {
resolve(error.response)
});
})
}
So this is a function and how can i import navigation in this. from hooks or some other way.
As this is not allowing to import here
import { useNavigation } from '#react-navigation/native';
and i don't want that everytime, i call GetApiRequestWithToken then pass navigation to it.
You can use a navigation provider pattern to achieve this. You can create a utility file like the following:
// util/navigation.js
import React from 'react';
export const navigationRef = React.createRef();
export const navigate = (routeName, params) => {
navigationRef.current?.navigate(routeName, params);
};
This stores the navigator reference in a local variable. You can see that it requires the navigator object to be passed in by an external component. I would recommend calling this function in your top-level navigation stack component. You most likely already have a component that looks something like the below:
// NavigationContainer.js
import React from 'react';
import { createStackNavigator } from '#react-navigation/stack';
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from 'util/navigation'; // from util
import ExampleStack from './ExampleStack';
const Stack = createStackNavigator();
export default () => {
return (
<NavigationContainer ref={navigationRef}> {/* store ref */}
<Stack.Navigator
initialRouteName="Example"
...
>
<Stack.Screen
name="Example"
component={ExampleStack}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
You can see here that the component created by createNavigationContainer will pass the ref to your navigation provider util above.
Finally, you can use the NavigationContainer created in this second file inside your App.js.
// App.js
import NavigationContainer from './NavigationContainer';
export default () => (
<NavigationContainer />
);
Any other functions of the navigator can be added to navigationUtil.js now, for example goBack. You can now use the utility in your axios request like so:
// axios util
import { navigate } from './navigationUtil.js'; // new
const GetApiRequestWithToken = async (url, params, headers) => {
return new Promise((resolve, reject) => {
axios.get(base_url_address + url, { headers: headers }).then(resp => {
if (resp.status == 401) {
navigate('UnauthorizedLogoutScreen'); // new
UnAuthorizedLogout();
} else {
resolve(resp)
}
}).catch((error) => {
resolve(error.response)
});
})
}
I hope this is clear, feel free to ask if something has not been covered.
I think this can be done using a custom hook.
import React, { useEffect } from 'react';
import { useNavigation } from '#react-navigation/native';
import axios from 'axios';
export default function useFetchAPI(url, params, headers) {
const [result, setResult] = React.useState(null);
const navigation = useNavigation();
useEffect(() => {
axios
.get(base_url_address + url, { headers: headers })
.then((resp) => {
console.log('resp: ', resp);
if (resp.status == 401) {
// this will navigate to your UnAuthorizedLogout page
navigation.navigate('UnAuthorizedLogout');
} else {
// otherwise, set the response to result state
setResult(resp);
}
})
.catch((error) => {
setResult(error.response);
});
}, [url])
return result;
}

How to use useNavigation() Hook in Redux actions React Native

I want to navigate user from my redux actions. For example when they
click on login, they navigate from action
.
Two ways i have tried.
1.pass navigation prop from component to action. (it works fine.)
2. use useNavigation() hook in redux actions. (it is not working. (Hooks can only be called inside of the body of a function component)).
Here is my code
action.js
export const registerUser = (data) => {
const navigation = useNavigation()
return async dispatch => {
dispatch(authLoading());
try {
const res = await axios.post(
`${BASE_URL}/mobilesignup`,
data,
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
);
console.log(res);
dispatch(registerSuccess(res));
navigation.navigate('dashboard')
} catch (err) {
dispatch(authFailed(err));
}
};
};
This code is not working
Error (Hooks can only be called inside of the body of a function
component)
Can anybody help me how can i use useNavigation() in redux actions ?
Thanks
You will have to use the Navigation ref which is there for purposes like calling from the reducer
The idea is to create a navigation.js and set the reference of navigation container and use it.
Code would be like below. (A sample from documentation)
//App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
// RootNavigation.js
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
You can simply import the navigation js anywhere and call navigate
Documentation
https://reactnavigation.org/docs/navigating-without-navigation-prop/#handling-initialization
According to the documentation of react-navigation v6.x
Define your rootNavigation module as followed:
// RootNavigation.ts
import { createNavigationContainerRef } from "#react-navigation/native";
const navigationRef = createNavigationContainerRef();
export class RootNavigation {
static navigate(name: string, params: any = {}) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
}
}
static get ref():any {
return navigationRef;
}
}
Pass the reference to NavigationContainer located at the root of your App.
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={RootNavigation.ref}>{/* ... */}</NavigationContainer>
);
}
Then simply use it at an action creator
// any js module
// ...
RootNavigation.navigate('ChatScreen', { userName: 'Lucy' });