How to test a custom react hook which uses useNavigation from 'react-navigation-hooks' using Jest? - react-native

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} />;
}

Related

how to create a hook with navigation properties for useContext react-native

Thank you very much in advance
I have a native reagent application that is in the following order of components:
app.tsx:
import React from 'react';
import { Routes } from './src/routes';
import { AppProvider } from './src/hooks';
export default function App() {
return (
<AppProvider>
<Routes />
</AppProvider>
);
}
I just needed to use the navigation properties inside a hooks:
hook/index.tsx
import React, { ReactNode, useContext } from 'react';
import {
NavigationContainer,
NavigationContext,
} from '#react-navigation/native';
import { AuthProvider } from './auth';
import { CommonProvider } from './common';
interface AppProviderProps {
children: ReactNode;
}
function AppProvider({ children }: AppProviderProps) {
return (
<CommonProvider>
<AuthProvider>{children}</AuthProvider>
</CommonProvider>
</NavigationProvider>
);
}
export { AppProvider };
hook example:
hook/CommonProvider.tsx:
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { Dispatch, SetStateAction } from 'react';
type CommonContextData = {
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
};
interface CommonProviderProps {
children: ReactNode;
}
const CommonContext = createContext<CommonContextData>({} as CommonContextData);
function CommonProvider({ children }: CommonProviderProps) {
const [isLoading, setIsLoading] = useState<boolean>(false);
//const {navigate} = useNavigation()//here I could use the navigation methods ???????
return (
<CommonContext.Provider value={{ isLoading, setIsLoading }}>
{children}
</CommonContext.Provider>
);
}
function useCommon(): CommonContextData {
const context = useContext(CommonContext);
return context;
}
export { CommonProvider, useCommon };
how would I do the following implementation?
I believe you need to wrap the Root component with the NavigationContainer. Once done, you can use the useNavigation hook in any child component.
For instance inside the CommonProvider you can use the hook useEffect in that way.
const navigation = useNavigation();
useEffect(()=>{
navigation.navigate('YourNextScreenName')
}, [navigation])
I managed to solve it as follows:
persist a file of
routes/RootNavigation.ts
import { createNavigationContainerRef } from '#react-navigation/native';
export const navigationRef = createNavigationContainerRef();
export function navigate(name: string, params: any) {
if (navigationRef.isReady()) {
navigationRef.navigate(name,params);
}
}
in my case what contains the centralization of routes in the file add the
navigationRef, no NavigationContainer:
routes/index.tsx
...
<NavigationContainer linking={linking} independent ref={navigationRef}>
...
using in file any hook:
...
function handleMovePage() {
// navigation.navigate('SignIn');
RootNavigation.navigate('SelectArea', { userName: 'Lucy' });
}
...
reference:
https://reactnavigation.org/docs/navigation-context/

Use redux action the dispatch is not working

I have combined my react redux.
Here is my App.js
import React from 'react';
import ReduxThunk from 'redux-thunk';
import { Provider } from 'react-redux';
import { compose, createStore, applyMiddleware } from 'redux';
import reducers from './src/reducers';
import AppContainer from './src/navigator'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const App: () => React$Node = () => {
const store = createStore(reducers, {}, composeEnhancers(applyMiddleware(ReduxThunk)));
return (
<Provider store={store}>
<AppContainer />
</Provider>
);
};
export default App;
src/reducers/index.js
import { combineReducers } from 'redux';
import LoginReducer from './LoginReducer';
export default combineReducers({
LoginRedux: LoginReducer
});
If I use my action login(), I can see login action start, but I can't see dispatch start
import React from 'react';
import {
Text,
View,
TouchableOpacity,
} from 'react-native';
import { connect } from 'react-redux';
import { login } from '../actions';
const LoginScreen = ({ navigation }) => {
// console.log('see my test value', testValue)
return (
<View>
<TouchableOpacity
onPress={() => {
login();
}
}>
<View>
<Text>LOGIN</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
const mapStateToProps = (state) => {
const { testValue } = state.LoginRedux;
console.log('mapStateToProps testValue =>', testValue);
return { testValue };
};
export default connect(mapStateToProps, { login })(LoginScreen);
If I console.log(dispatch), it will show dispatch is not defined.
import { LOGIN } from './types';
export const login = () => {
console.log('login action start')
return (dispatch) => {
console.log('dispatch start');
// console.log(dispatch);
dispatch({ type: LOGIN, testValue: 'I am test' });
};
};
src/reducers/LoginReducer.js
import { LOGIN } from '../actions/types';
const INITIAL_STATE = {
testValue: ''
};
export default (state = INITIAL_STATE, action) => {
console.log('reducer =>', action); // I can't see the console.log
switch (action.type) {
case LOGIN:
return {
...state,
testValue: action.testValue
};
default:
return state;
}
};
I have no idea why my action dispatch is not working. Do I set something wrong ?
Any help would be appreciated.
According to Zaki Obeid help, I update like this:
the action code:
export const login = () => {
console.log('login !');
return { type: LOGIN };
};
the function component code:
import { login } from '../../actions';
export const SettingScreen = ({ navigation, login }) => {
// return view code
}
const mapDispatchToProps = dispatch => ({
// you will use this to pass it to the props of your component
login: () => dispatch(login),
});
connect(null, mapDispatchToProps)(SettingScreen);
In LoginScreen component
you will need to add mapDispatchToProps
const mapDispatchToProps = dispatch => ({
// you will use this to pass it to the props of your component
login: () => dispatch(login()),
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
Then
you will need to destructure from the props as:
const LoginScreen = ({ navigation, login }) => {
// your code
}
In actions.js
the way you use dispatch here requires a library redux-thunk and it's used for async calls.
and the normal action should do the job for you:
export const login = () => ({
type: LOGIN,
testValue: 'I am test'
})
I hope this is useful and will solve your problem,
Have a good day.
In a react-redux app, you obtain the dispatch function either from getting a hold of the store object directly (store.dispatch), or via the react-redux connect function, which will provide dispatch as an argument to a function you write and then later hook up to a component
import { connect } from 'react-redux';
const mapStateToProps = ...
const mapDispatchToProps = (dispatch) => {
return {
someHandle: () => dispatch(myActionCreator())
}
}
export const connect(mapStateToProps, mapDispatchToProps)(MyComponent)
You can't just call dispatch out of thin air -- it's not a global function.
It seems you are using the login function directly. you will have to use the props. Just change the name for confusing and use through props.
import { combineReducers } from 'redux';
import LoginReducer from './LoginReducer';
export default combineReducers({
LoginRedux: LoginReducer
});
If I use my action login(), I can see login action start, but I can't see dispatch start
import React from 'react';
import {
Text,
View,
TouchableOpacity,
} from 'react-native';
import { connect } from 'react-redux';
import { login } from '../actions';
const LoginScreen = ({ navigation, userLogin }) => {
// console.log('see my test value', testValue)
return (
<View>
<TouchableOpacity
onPress={() => {
userLogin();
}
}>
<View>
<Text>LOGIN</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
const mapStateToProps = (state) => {
const { testValue } = state.LoginRedux;
console.log('mapStateToProps testValue =>', testValue);
return { testValue };
};
export default connect(mapStateToProps, { userLogin:login })(LoginScreen);

Error in react-native with expo: Could not find "store" in the context of "Connect(App)"

I'm building my first react native app and I encountered a problem to connect to redux store (I also do not have much experience with redux yet). I am using expo.
The error is:
Invariant Violation: Could not find "store" in the context of "Connect(App)". Either wrap the root component in a , or pass a custom React context provider to and the corresponding React context consumer to Connect(App) in connect options.
This error is located at:
in Connect(App) (at withExpoRoot.js:22)
(...)
Here is my code:
Could you please help?
// App.js
import React, { Component } from "react";
import AppStackNav from "./navigators/AppStackNav";
import { Provider, connect } from 'react-redux';
import { createStore } from 'redux';
import guestsReducer from "./reducers/GuestsReducer";
const store = createStore(guestsReducer);
class App extends Component {
constructor(props) {
super(props);
}
addGuest = (index) => {
// ...
}
render() {
return (
<Provider store={store}>
<AppStackNav
screenProps={{
currentGuests: this.state.currentGuests,
possibleGuests: this.state.possibleGuests,
addGuest: this.addGuest
}}
/>
</Provider>
)
}
}
const mapStateToProps = state => {
return {
currentGuests: this.state.current,
possibleGuests: this.state.possible,
addGuest: this.addGuest
};
}
export default connect(mapStateToProps)(App);
// GuestsReducer.js
import { combineReducers } from 'redux';
const INITIAL_STATE = {
current: 10,
possible: [
'Guest1',
'Guest2',
'Guest3',
'Guest4',
],
};
const guestsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
default:
return state
}
};
export default combineReducers({
guests: guestsReducer,
});
// AppStackNav.js
import { createStackNavigator, createAppContainer } from "react-navigation";
import Home from "../screens/Home";
import Dashboard from "../screens/Dashboard";
import Project from "../screens/Project";
import Placeholder from "../screens/Placeholder";
const AppStackNav = createStackNavigator({
// ...
});
export default createAppContainer(AppStackNav);
First Issue
const mapStateToProps = ({ guests }) => {
return {
currentGuests: guests.current,
possibleGuests: guests.possible
};
}
Second Issue
You wire redux store to your upper level component which is the App component ... and then use connect and mapStateToProps to access redux store in the children of this upper level component (App) ... I mean you connect your store via mapStateToProps to your AppStackNav component not the App component
const AppStackNav = ({ currentGuests, possibleGuests }) => {
const Stack = createStackNavigator({...});
return <Stack />;
};
const mapStateToProps = ({ guests }) => {
return {
currentGuests: guests.current,
possibleGuests: guests.possible
};
}
// react-navigation v2 is needed for this to work:
export default connect(mapStateToProps)(AppStackNav);
App.js
class App extends Component {
constructor(props) {
super(props);
}
addGuest = (index) => {
// ...
}
render() {
return (
<Provider store={store}>
<AppStackNav />
</Provider>
)
}
}
export default App;
you can't use 'this' keyword outside the class as It wont be able to understand the context for that particular method.
you need to simply remove this keyword from mapStateToProps
like this:
const mapStateToProps = state => {
return {
currentGuests: state.current,
possibleGuests: state.possible
};
}

Async call with react native and redux , thunk

I have been following this tutorial to integrate redux into my react native app.
https://github.com/jlebensold/peckish
On my Home view, I'm not able to call the functions from my action folder.
One difference is that I'm using react-navigation in my app. Wonder if I need to integrate redux with react navigation to be able to use redux for all data?
Below is the full implementation code I have been doing.
On the Home screen, I call the fetchSite function on ComponentDidMount to launch an async call with axios. But I can't even access to this function.
Sorry for this long post but I can't figure out how to make this work so quite difficult to make a shorter code sample to explain the structure of my app.
Let me know if any question.
index.ios.js
import React from 'react'
import { AppRegistry } from 'react-native'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose} from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import reducer from './app/reducers'
import AppContainer from './app/index'
// middleware that logs actions
const loggerMiddleware = createLogger({ predicate: (getState, action) => __DEV__ });
function configureStore(initialState) {
const enhancer = compose(
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware,
),
);
return createStore(reducer, initialState, enhancer);
}
const store = configureStore({});
const App = () => (
<Provider store={store}>
<AppContainer />
</Provider>
);
AppRegistry.registerComponent('Appero', () => App;
reducers/index.js
import { combineReducers } from 'redux';
import * as sitesReducer from './sites'
export default combineReducers(Object.assign(
sitesReducer,
));
reducers/sites.js
import createReducer from '../lib/createReducer'
import * as types from '../actions/types'
export const searchedSites = createReducer({}, {
[types.SET_SEARCHED_SITES](state, action) {
let newState = {};
action.sites.forEach( (site) => {
let id = site.id;
newState[id] = Object.assign({}, site, { id });
});
return newState;
},
});
../lib/createReducer
export default function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
../actions/types
export const SET_SEARCHED_SITES = 'SET_SEARCHED_SITES';
AppContainer in ./app/index
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ActionCreators } from './actions';
console.log(ActionCreators); //Properly gathered the functions from the actions folder
import { Root } from './config/router';
window.store = require('react-native-simple-store');
window.axios = require('axios');
class App extends Component {
render() {
return (
<Root />
)
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(ActionCreators, dispatch);
}
export default connect(mapDispatchToProps)(App);
ActionCreators in './actions';
import * as SiteActions from './sites'
export const ActionCreators = Object.assign({},
SiteActions,
);
Actions in './actions/sites'
import * as types from './types' //See above
export function fetchSites(token) {
return (dispatch, getState) => {
let instance = axios.create({
baseURL: url + 'api/',
timeout: 10000,
headers: {'Accept' : 'application/json', 'Authorization' : 'Bearer ' + token}
});
instance.get('/sites?page=1')
.then(response => {
console.log(response.data.data);
dispatch(setSearchedSites({sites: response.data.data}));
}).catch(error => {
console.log(error);
});
}
}
export function setSearchedSites({ sites }) {
return {
type: types.SET_SEARCHED_SITES,
sites,
}
}
Root file for navigation based on react-navigation
I made it as simple as possible for this example.
import React from 'react';
import {StackNavigator} from 'react-navigation';
import Home from '../screens/Home';
export const Root = StackNavigator({
Home: {
screen: Home,
}
});
And finally my Home screen
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {Text, View} from 'react-native';
class Home extends Component {
componentDidMount()
{
let token = "12345678" //Just for this example
this.props.fetchSites(token).then( (response) => {
console.log(response);
});
}
render() {
return (
<View>
<Text>This is the Home view</text>
</View>
);
}
}
function mapStateToProps(state) {
return {
searchedSites: state.searchedSites
};
}
export default connect(mapStateToProps)(Home);
To use action methods you need to connect in home screen like this
import { fetchSites } from '<your-path>'
// your Home's other code.
const mapDispatchToProps = (dispatch) => {
return{
fetchSites:dispatch(fetchSites())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Home);
after that you can use fetchSites as this.props.fetchSites whenever you want.

How to listen to route changes in react router v4?

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.