I am new to react native and trying to create React Native context which will store array of objects. Context looks something like this:
import React, {useState, useCallback} from 'react';
export const NotificationContext = React.createContext({
notifications: [],
updateNotifications: () => {},
});
export default function NotificationContextProvider({children}) {
const [notifications, setNotifications] = useState([]);
const updateNotifications = n => {
notifications.push(n);
setNotifications(notifications);
};
const contextValue = {
notifications,
updateNotifications: useCallback(n => updateNotifications(n), []),
};
return (
<NotificationContext.Provider value={contextValue}>
{children}
</NotificationContext.Provider>
);
}
Now when I am trying to access the context, I am not getting the updated array value as desired.
var context = useContext(NotificationContext);
useEffect(() => {
(async () => {
console.log('Before', context);
console.log('Notification value', context.notifications);
context.updateNotifications([1]);
console.log('After', context);
})();
}, []);
I think you should pass dependencies variable context in useEffect
How can I call a callback function when I press on a tab in Tab.Navigator which is already selected?
The custom useTabDoublePressEffect hook:
import {EventArg, useNavigation, useRoute} from '#react-navigation/native';
import React from 'react';
export default function useTabDoublePressEffect(callback: Function) {
const navigation = useNavigation();
const route = useRoute();
React.useEffect(() => {
let current = navigation; // The screen might be inside another navigator such as stack nested in tabs
// We need to find the closest tab navigator and add the listener there
while (current && current.getState().type !== 'tab') {
current = current.getParent();
}
if (!current) {
return;
}
const unsubscribe = current.addListener(
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
// #ts-expect-error
'tabPress',
(e: EventArg<'tabPress', true>) => {
// We should scroll to top only when the screen is focused
const isFocused = navigation.isFocused(); // In a nested stack navigator, tab press resets the stack to first screen
// So we should scroll to top only when we are on first screen
const isFirst =
navigation === current ||
navigation.getState().routes[0].key === route.key;
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (isFocused && isFirst && !e.defaultPrevented) {
callback();
}
});
},
);
return unsubscribe;
}, [navigation, route.key]);
}
Basic example for how to use it:
import React, {useState} from 'react';
import {Text} from 'react-native';
import useTabDoublePressEffect from '../hooks/useTabDoublePressEffect';
export default function SomeComponent() {
const [text, setText] = useState('Hello');
useTabDoublePressEffect(() => setText('Bye'));
return <Text>{text}</Text>;
}
Code borrowed from:
https://github.com/react-navigation/react-navigation/blob/ac24e617af10c48b161d1aaa7dfc8c1c1218a3cd/packages/native/src/useScrollToTop.tsx
im using react native expo and push notification works fine when app is running or in background but with the app is close. it doesnt call the method to handle the notification. I need to redirect to detail page. I tried to use function compoents and class components, tried to migrade to legacy notification and the new one.
import React, {useState, useEffect, useRef} from 'react';
import {
Image, ScrollView,
StyleSheet, Text, TouchableOpacity, Platform,
View, Linking,
} from 'react-native';
import * as Notifications from "expo-notifications";
const HomeScreen = (props) => {
useEffect(() => {
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
const {request, date} = notification ||{}
const {content} = request ||{}
const {data} = content ||{}
const {annKey,type} = data ||{}
if(annKey) {
// navigation.navigate('Detail', {annKey, updateFeed: true, onSelect},)
}
});
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
const {notification} = response ||{}
console.log(notification);
const {request, date} = notification ||{}
const {content} = request ||{}
const {data} = content ||{}
const {annKey, type} = data ||{}
if(annKey){
navigation.navigate('Detail', {annKey, updateFeed: true, onSelect},)
}
});
return () => {
Notifications.removeNotificationSubscription(notificationListener);
Notifications.removeNotificationSubscription(responseListener);
};
}, []);
}
export default HomeScreen;
The problem is that the useEffect() get called too late after the app has finished initializing. Therefore the listener is not added before the system has abandoned the notification, and the handler not called.
Fortunately, since you are using the new expo-notifications library, it introduced the useLastNotificationResponse() React hook. It can replace the addNotificationResponseReceivedListener() listener and returns the last notification the user interacted with (i.e. tap). It can be safely used in a useEffect() hook.
You can find the documentation here : https://docs.expo.io/versions/latest/sdk/notifications/#uselastnotificationresponse-undefined--notificationresponse--null
Here is how to use it (it's better to implement it on your root component):
import * as Notifications from 'expo-notifications';
export default function App() {
const lastNotificationResponse = Notifications.useLastNotificationResponse();
React.useEffect(() => {
if (
lastNotificationResponse &&
lastNotificationResponse.notification.request.content.data['someDataToCheck'] &&
lastNotificationResponse.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
) {
// navigate to your desired screen
}
}, [lastNotificationResponse]);
return (
/*
* your app
*/
);
}
You have to add this to your app.json file:
"android": {
"useNextNotificationsApi": true,
},
I have a custom react hook 'useSample' which uses useNavigation and useNavigationParam
import { useContext } from 'react'
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'
import sampleContext from '../sampleContext'
import LoadingStateContext from '../LoadingState/Context'
const useSample = () => {
const sample = useContext(sampleContext)
const loading = useContext(LoadingStateContext)
const navigation = useNavigation()
const Mode = !!useNavigationParam('Mode')
const getSample = () => {
if (Mode) {
return sample.selectors.getSample(SAMPLE_ID)
}
const id = useNavigationParam('sample')
sample.selectors.getSample(id)
navigation.navigate(SAMPLE_MODE_ROUTE, { ...navigation.state.params}) // using navigation hook here
}
return { getSample }
}
export default useSample
I need to write unit tests for the above hook using jest and I tried the following
import { renderHook } from '#testing-library/react-hooks'
import sampleContext from '../../sampleContext'
import useSample from '../useSample'
describe('useSample', () => {
it('return sample data', () => {
const getSample = jest.fn()
const sampleContextValue = ({
selectors: {
getSample
}
})
const wrapper = ({ children }) => (
<sampleContext.Provider value={sampleContextValue}>
{children}
</sampleContext.Provider>
)
renderHook(() => useSample(), { wrapper })
})
})
I got the error
'react-navigation hooks require a navigation context but it couldn't be found. Make sure you didn't forget to create and render the react-navigation app container. If you need to access an optional navigation object, you can useContext(NavigationContext), which may return'
Any help would be appreciated!
versions I am using
"react-navigation-hooks": "^1.1.0"
"#testing-library/react-hooks":"^3.4.1"
"react": "^16.11.0"
You have to mock the react-navigation-hooks module.
In your test:
import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
jest.mock('react-navigation-hooks');
And it's up to you to add a custom implementation to the mock. If you want to do that you can check how to mock functions on jest documentation.
for me, soved it by usingenter code here useRoute():
For functional component:
import * as React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '#react-navigation/native';
function MyBackButton() {
const navigation = useNavigation();
return (
<Button
title="Back"
onPress={() => {
navigation.goBack();
}}
/>
);
}
For class component:
class MyText extends React.Component {
render() {
// Get it from props
const { route } = this.props;
}
}
// Wrap and export
export default function(props) {
const route = useRoute();
return <MyText {...props} route={route} />;
}
I have a couple of buttons that acts as routes. Everytime the route is changed, I want to make sure the button that is active changes.
Is there a way to listen to route changes in react router v4?
I use withRouter to get the location prop. When the component is updated because of a new route, I check if the value changed:
#withRouter
class App extends React.Component {
static propTypes = {
location: React.PropTypes.object.isRequired
}
// ...
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.onRouteChanged();
}
}
onRouteChanged() {
console.log("ROUTE CHANGED");
}
// ...
render(){
return <Switch>
<Route path="/" exact component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
<Route path="/success" component={SuccessPage} />
// ...
<Route component={NotFound} />
</Switch>
}
}
To expand on the above, you will need to get at the history object. If you are using BrowserRouter, you can import withRouter and wrap your component with a higher-order component (HoC) in order to have access via props to the history object's properties and functions.
import { withRouter } from 'react-router-dom';
const myComponent = ({ history }) => {
history.listen((location, action) => {
// location is an object like window.location
console.log(action, location.pathname, location.state)
});
return <div>...</div>;
};
export default withRouter(myComponent);
The only thing to be aware of is that withRouter and most other ways to access the history seem to pollute the props as they de-structure the object into it.
As others have said, this has been superseded by the hooks exposed by react router and it has a memory leak. If you are registering listeners in a functional component you should be doing so via useEffect and unregistering them in the return of that function.
v5.1 introduces the useful hook useLocation
https://reacttraining.com/blog/react-router-v5-1/#uselocation
import { Switch, useLocation } from 'react-router-dom'
function usePageViews() {
let location = useLocation()
useEffect(
() => {
ga.send(['pageview', location.pathname])
},
[location]
)
}
function App() {
usePageViews()
return <Switch>{/* your routes here */}</Switch>
}
You should to use history v4 lib.
Example from there
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
withRouter, history.listen, and useEffect (React Hooks) works quite nicely together:
import React, { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
const Component = ({ history }) => {
useEffect(() => history.listen(() => {
// do something on route change
// for my example, close a drawer
}), [])
//...
}
export default withRouter(Component)
The listener callback will fire any time a route is changed, and the return for history.listen is a shutdown handler that plays nicely with useEffect.
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
function MyApp() {
const location = useLocation();
useEffect(() => {
console.log('route has been changed');
...your code
},[location.pathname]);
}
with hooks
With hooks:
import { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
import { history as historyShape } from 'react-router-prop-types'
const DebugHistory = ({ history }) => {
useEffect(() => {
console.log('> Router', history.action, history.location)
}, [history.location.key])
return null
}
DebugHistory.propTypes = { history: historyShape }
export default withRouter(DebugHistory)
Import and render as <DebugHistory> component
import { useHistory } from 'react-router-dom';
const Scroll = () => {
const history = useHistory();
useEffect(() => {
window.scrollTo(0, 0);
}, [history.location.pathname]);
return null;
}
With react Hooks, I am using useEffect
import React from 'react'
const history = useHistory()
const queryString = require('query-string')
const parsed = queryString.parse(location.search)
const [search, setSearch] = useState(parsed.search ? parsed.search : '')
useEffect(() => {
const parsedSearch = parsed.search ? parsed.search : ''
if (parsedSearch !== search) {
// do some action! The route Changed!
}
}, [location.search])
in this example, Im scrolling up when the route change:
import React from 'react'
import { useLocation } from 'react-router-dom'
const ScrollToTop = () => {
const location = useLocation()
React.useEffect(() => {
window.scrollTo(0, 0)
}, [location.key])
return null
}
export default ScrollToTop
In some cases you might use render attribute instead of component, in this way:
class App extends React.Component {
constructor (props) {
super(props);
}
onRouteChange (pageId) {
console.log(pageId);
}
render () {
return <Switch>
<Route path="/" exact render={(props) => {
this.onRouteChange('home');
return <HomePage {...props} />;
}} />
<Route path="/checkout" exact render={(props) => {
this.onRouteChange('checkout');
return <CheckoutPage {...props} />;
}} />
</Switch>
}
}
Notice that if you change state in onRouteChange method, this could cause 'Maximum update depth exceeded' error.
For functional components try useEffect with props.location.
import React, {useEffect} from 'react';
const SampleComponent = (props) => {
useEffect(() => {
console.log(props.location);
}, [props.location]);
}
export default SampleComponent;
For React Router v6 & React Hooks,
You need to use useLocation instead of useHistory as it is deprecated
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
export default function Component() {
const history = useLocation();
useEffect(() => {
console.log('> Router', history.pathname)
}, [history.pathname]);
}
With the useEffect hook it's possible to detect route changes without adding a listener.
import React, { useEffect } from 'react';
import { Switch, Route, withRouter } from 'react-router-dom';
import Main from './Main';
import Blog from './Blog';
const App = ({history}) => {
useEffect( () => {
// When route changes, history.location.pathname changes as well
// And the code will execute after this line
}, [history.location.pathname]);
return (<Switch>
<Route exact path = '/' component = {Main}/>
<Route exact path = '/blog' component = {Blog}/>
</Switch>);
}
export default withRouter(App);
I just dealt with this problem, so I'll add my solution as a supplement on other answers given.
The problem here is that useEffect doesn't really work as you would want it to, since the call only gets triggered after the first render so there is an unwanted delay.
If you use some state manager like redux, chances are that you will get a flicker on the screen because of lingering state in the store.
What you really want is to use useLayoutEffect since this gets triggered immediately.
So I wrote a small utility function that I put in the same directory as my router:
export const callApis = (fn, path) => {
useLayoutEffect(() => {
fn();
}, [path]);
};
Which I call from within the component HOC like this:
callApis(() => getTopicById({topicId}), path);
path is the prop that gets passed in the match object when using withRouter.
I'm not really in favour of listening / unlistening manually on history.
That's just imo.