I am trying to navigate to a certain screen whenever I click on an Expo Push Notification. The screen that I want to navigate to is rather deep into the NavigationContainer.
However, the issue that I am facing now is being unable to even navigate to anywhere except having the app restart on its own. I'm running all the testing on a real device.
I'm using Expo to work on this school project.
I have only managed to find this question in SO and Expo Forums (duplicate) useful.
This is my application Navigation structure:
-Navigation Structure-
AppNavigator
DrawerNavigator
MainNavigator
TabsNavigator
StackNavigator
StackNavigator
TabsNavigator
ScreenA (Want to navigate to)
ScreenB (Want to navigate to)
StackNavigator
ScreenA
ScreenB
StackNavigator
ScreenA
AuthNavigator
RegisterNavigator
ScreenA
There is a useNotifications hook created and I called it in the main App Navigator where the NavigationContainer resides in.
import React, { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import navigation from '../navigation/RootNavigation';
const useNotifications = () => {
const notiResponseListener = React.createRef();
useEffect(() => {
notiResponseListener.current =
Notifications.addNotificationResponseReceivedListener(res => {
console.log(res.notification.request.content.data);
console.log('addNotificationResponseReceivedListener');
navigation.navigate(
('DrawerNavigator', { screen: 'ChangePassword' }),
{}
);
});
return () =>
Notifications.removeNotificationSubscription(notiResponseListener);
}, []);
};
export default useNotifications;
There is a ref added to the NavigationContainer.
import { navigationRef } from '../navigation/RootNavigation';
import useNotifications from '../hooks/useNotifications';
const App = createStackNavigator();
const AppNavigator = () => {
useNotifications();
return (
<NavigationContainer ref={navigationRef}>
<App.Navigator headerMode='none'>
...
</App.Navigator>
</NavigationContainer>
);
};
And lastly, the file that contains the ref used in the NavigationContainer.
import React from 'react';
export const navigationRef = React.createRef();
const navigate = (name, params) => {
console.log('entered navigating'); // does not print
navigationRef.current?.navigate(name, params);
};
export default {
navigate
};
I have searced high and low but I can't seem to find out what's wrong. Looked at the documentation for Expo and React Navigation but I'm not sure what's going on. It's my first time working on Push Notifications and such a case.
I appreciate any help, thank you
We have fixed the problem with the usage of useLastNotificationResponse.
const [notification, setNotification] = useState(false);
const notificationListener = useRef();
const responseListener = useRef();
//add this
const lastNotificationResponse =
Notifications.useLastNotificationResponse();
useEffect(() => {
if (lastNotificationResponse) {
//console.log(lastNotificationResponse);
//get the route
const route = JSON.stringify(
lastNotificationResponse.notification.request.content.data.route
);
//use some function to return the correct screen by route
getFullPath(JSON.parse(route));
}
}, [lastNotificationResponse]);
Based on your routes, navigate to correct screen
getFullPath:
import { navigationRef } from "./rootNavigation";
import routes from "./routes";
export function getFullPath(route) {
switch (route) {
case "HomeScreen":
return navigationRef.current?.navigate(routes.HOME);
case "Account":
return navigationRef.current?.navigate(routes.ACCOUNT, {
screen: routes.ACCOUNTSCREEN,
});
default:
return;
}
}
Related
I want to get the url of image from firebase storage in my react app, but I am getting this error. code is working fine on localhost but giving error whenever I deploy it .
import React from 'react';
import { useSelector } from 'react-redux';
import Home from '../../Components/organisms/Home/Home';
import { getStorage, ref, getDownloadURL} from "firebase/storage";
import {
loginUserInfoSelector,
userLoginObjectSelector,
} from "../../Redux/Login/selectors";
import { useState } from 'react';
import { useEffect } from 'react';
const HomeScreen = () => {
const loginUserData = useSelector(loginUserInfoSelector);
const [profileurl, setprofileurl] = useState('')
const storage = getStorage();
const fileRef = ref(storage, loginUserData?.profileUrl);
getDownloadURL(fileRef).then((URL) => {
console.log(URL)
setprofileurl(URL)
}).catch((error) => {
console.log(error)
});
return (
<div>
<Home newprofile={profileurl} />
</div>
)
}
export default HomeScreen
It looks like loginUserData?.profileUrl is not returning a value, likely because loginUserData is null.
You'll probably only want to try and load the profile picture when the loginUserData is not null, and maybe even route to a different screen when there is no signed in user..
In a React project, I've created a popup modal which will be displayed when any user tries to do any changes in input field and navigate to other screen. It doesn't work as expected, hence gone through many posts to find the solution but, no luck. Please refer to code below:
useBlock.js
import {useContext, useEffect} from 'react';
import { UNSAFE_NavigationContext as NavigationContext} from 'react-router-dom';
const useBlocker = (blocker, when = true) => {
const navigator = useContext(NavigationContext).navigator
useEffect(() => {
if (!when)
return;
const unblock = navigator.block((tx) => { <-- This line is creating an issue
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
export default useBlocker
useCallbackPrompt.js
import { useCallback, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import useBlocker from './useBlocker';
const useCallbackPrompt = (when) => {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState(false);
const [lastLocation, setLastLocation] = useState(null);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);
const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);
const handleBlockedNavigation = useCallback((nextLocation) => {
if (!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname) {
setShowPrompt(true);
setLastLocation(nextLocation);
return false;
}
return true;
}, [confirmedNavigation]);
const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);
useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname);
}
}, [confirmedNavigation, lastLocation]);
useBlocker(handleBlockedNavigation, when);
return [showPrompt, confirmNavigation, cancelNavigation];
}
export default useCallbackPrompt
So above are the 2 files which I'm using. In useBlocker.js file that particular line is actually causing the root issue. Please refer to the image below
I'm using "react-router-dom": "^6.3.0", Is this causing any issue? Any suggestions or modifications are highly appreciated.
I wasn't able to reproduce the issue using react-router-dom#6.3.0, but I could when bumping to react-router-dom#6.4.0. I suspect with a dependency specified as ^6.3.0 you've actually a more current version actually installed. If you like you can check the installed version by running npm list react-router-dom and verify for yourself.
It seems the navigation context has a mildly breaking change between v6.3.0 and v6.4.0. The v6.3.0 version is a history object (source) while the v6.4.0 is a new navigation context object where navigator is a simpler interface (source).
Solution 1 - Revert to previous version
You could revert back to 6.3.0 though by running npm i -s react-router-dom#6.3.0 to install that exact version. Double-check your package.json file to ensure the entry is "react-router-dom": "6.3.0".
Solution 2 - Use the "real" history object
If you wanted to move forward with the newer RRD versions then an alternative I'd suggest is to use the history#5 history object directly instead of trying to use the react-router#6 navigator. RRDv6 was only ever exporting a subset of the history methods anyway.
Add history#5 as a project dependency.
IMPORTANT: You will want to check what version react-router-dom is using and match if you can.
Create and export a custom history object. createBrowserHistory for a BrowserRouter, createHashHistory for a HashRouter, etc.
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
Import your custom history object and the history router from RRD.
import { unstable_HistoryRouter as Router } from "react-router-dom";
import history from './history';
...
<Router history={history}>
<App />
</Router>
Import your custom history object to use in your custom hooks.
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import history from "./history"; // <-- import
const useBlocker = (blocker, when = true) => {
useEffect(() => {
if (!when) return;
const unblock = history.block((tx) => { // <-- use history
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
}
};
blocker(autoUnblockingTx);
});
return unblock;
}, [blocker, when]);
};
useCallbackPrompt is untouched.
const useCallbackPrompt = (when) => {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState(false);
const [lastLocation, setLastLocation] = useState(null);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);
const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);
const handleBlockedNavigation = useCallback(
(nextLocation) => {
if (
!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname
) {
setShowPrompt(true);
setLastLocation(nextLocation);
return false;
}
return true;
},
[confirmedNavigation]
);
const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);
useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname);
}
}, [confirmedNavigation, lastLocation]);
useBlocker(handleBlockedNavigation, when);
return [showPrompt, confirmNavigation, cancelNavigation];
};
Demo
From v6.4.0 navigator.block is removed. You can find a workaround here: https://gist.github.com/MarksCode/64e438c82b0b2a1161e01c88ca0d0355.
Also, relevant discussion going on here. https://github.com/remix-run/react-router/issues/8139#issuecomment-1262630360
I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.
I am new to react and recently i got into this problem and i dont know how to solve it.
it says Too many re-renders. React limits the number of renders to prevent an infinite loop. Hows it infinte loop? is it beacuase of on("value")?
import React from "react";
import fire from "./firebase";
import firebase from "firebase"
import { useState } from "react"
const UserPage = ({ match }) => {
const [user, setUser] = useState(null)
const { params: { userId } } = match;
var userName;
console.log(userId)
firebase.database().ref("users/"+userId+"/praivate/login credentials").on("value", (snapshot)=>{
setUser(snapshot.val().userName)
})
return(
<>
<h1>Hey {user}</h1>
</>
)
}
export default UserPage
Plz help me to fix it, thank you.
You should do your Firebase staff inside a lifecyle method.As your working with functionnal components you can use the useEffect hook:
import React from "react";
import fire from "./firebase";
import firebase from "firebase"
import { useState } from "react"
const UserPage = ({ match }) => {
const [user, setUser] = useState(null)
const { params: { userId } } = match;
useEffect(()=>{
//Put your Firebase staff here
},[])
return(
<>
<h1>Hey {user}</h1>
</>
)
}
export default UserPage
I dont know what you're trying to achieve, but inside you <h1>{user}</h1> i think that {user} is an object so if you want to access a specific attribute you can do something like <h1>{user.attributeName}</h1>.
I hope that it helped
I am new to react-native and I am confused about routing / navigation. Basically, I've got 4 screens which are Login, Register, Home and Links. The Register and Login are already set. I used stackNavigator in order for the user to click back whether they want to register or not. But when I click login, I want to redirect the user to my Home screen which has a tab menu or container. My current output for this code is that when I do register, I can go back to the login by pressing the text I made. Can someone show me or provide me some link on how to do the bottom tab when I successfully logged in?
My current output is here
https://imgur.com/a/9lsHCe6
I utilized onPress={()=>{navigation.navigate('Route')}} functionality from my components to switch screen.
Here is some of my code:
AppNavigation.js
import { createStackNavigator } from 'react-navigation-stack'
import Register from '../screens/RegisterScreen'
import Login from '../screens/LoginScreen'
const AppNavigation = createStackNavigator(
{
Login: { screen: Login},
Register: { screen: Register },
},
{
initialRouteName: 'Login',
headerMode: 'none'
}
)
export default AppNavigation
AuthNavigation.js
import { createStackNavigator } from 'react-navigation-stack'
import Login from '../screens/LoginScreen'
const AuthNavigation = createStackNavigator(
{
Login: { screen: Login },
},
{
initialRouteName: 'Login',
}
)
export default AuthNavigation
index.js (inside from ./navigation/index.js)
import { createSwitchNavigator, createAppContainer } from 'react-navigation'
import AuthNavigation from './AppNavigation'
import AppNavigation from './AppNavigation'
const SwitchNavigator = createSwitchNavigator(
{
Auth: AuthNavigation,
App: AppNavigation
},
{
initialRouteName: 'Auth',
}
)
const AppContainer = createAppContainer(SwitchNavigator)
export default AppContainer
App.js
export default function App() {
return (
<AppContainer />
);
}
}
My current directory
I followed the auth pattern here
https://heartbeat.fritz.ai/how-authentication-flow-works-in-react-native-apps-using-react-navigation-4-x-a30bb4d9e5d6
try this syntax on onPress
onPress={()=> this.props.navigation.navigate('Register')}
your ques: how to do the bottom tab when I successfully logged in?
create Home Component
When your login is successfully then
if(login is successfully){
this.props.navigation.navigate('Home');
}
Use the following syntax to be able to navigate between the components you want.
if(login == true){
this.props.navigation.navigate('Home');
// You can use this.props.navigation.push('Home'); as well
}
Here is a good reference to navigation and routing in general using React Native.
The way you can do this in React Navigation v5 and v6 are as followed. Setup a global auth provider with createContext. Then use that in a switch statement to determine whether you should use the Auth stack or the main navigation stack. The main navigation stack (AppStack in this case) would be a bottom tab navigator nested inside a stack navigator with the initialRouteName set to 'Home' (or whatever your home screen is called in the stack nav).
The Provider can look something like this
export const AuthUserProvider = ({ children }) => {
const [user, setUser] = useState(null);
return (
<AuthUserContext.Provider value={{ user, setUser }}>
{children}
</AuthUserContext.Provider>
);
};
And the Nested stack navigators with switch statement should look something like this:
export default function Routes() {
const Root = createStackNavigator();
const { user, setUser } = useContext(AuthUserContext);
//....authentication hooks
return (
<NavigationContainer>
<Root.Navigator
{user ?
(<Root.Screen name='AppStack' component={AppStack} />)
:
(<Root.Screen name='AuthStack' component={AuthStack} />)
}
<Root.Navigator>
<NavigationContainer>
)