React Navigation V5, TabNavigation sharing props - react-native

In may Dashboard screen which is created using createMaterialTopTabNavigator, and has 5 screens.
I want to achieve that for example after doing some function in screen1, then i want screen2 to make a specific function, without rendering any screen again.
So my problem was how to make the tabs connected and know about each other.
I am looking to share state between screens or props.
That's my DashBoard Code (summarized).
function Switch0({navigation, route}) {return <Screen0 navigation = {navigation} route={route}/>}
function Switch1({navigation, route}) {return <Screen1 navigation = {navigation} route={route}/>}
function Switch2({navigation, route}) {return <Screen2 navigation = {navigation} route={route}/>}
function Switch3({navigation, route}) {return <Screen3 navigation = {navigation} route={route}/>}
function Switch4({navigation, route}) {return <Screen4 navigation = {navigation} route={route}/>}
export default class DashBoard1 extends React.Component {
render(){
return (
<Tab.Navigator>
<Tab.Screen name="MyTeams" component={Switch0} options={navigationOptions0} />
<Tab.Screen name="Matches" component={Switch1} options={navigationOptions1}/>
<Tab.Screen name="PlayerMarket" component={Switch2} options={navigationOptions2}/>
<Tab.Screen name="Courts" component={Switch3} options={navigationOptions3}/>
<Tab.Screen name="Tournaments" component={Switch4} options={navigationOptions4}/>
</Tab.Navigator>
);
}

I mean you should wrap you AppContainer with some state provider.
import { StoreProvider } from 'store';
<StoreProvider>
<AppContainer />
</StoreProvider>
This store provider can be made using redux. https://redux.js.org
Or if you are comfortable with hooks, you can try React context Api, something this and write your actions and reducers. https://reactjs.org/docs/context.html
import Reducer, { initialState } from './reducers';
import useActions from './actions';
export const Store = React.createContext();
export const StoreProvider = props => {
const { children } = props;
const [state, dispatch] = React.useReducer(Reducer, initialState);
const actions = useActions(state, dispatch);
const value = { state, dispatch, actions };
return <Store.Provider value={value}>{children}</Store.Provider>;
};

Related

Can an independent functional component re-render based on the state change of another?

I'm new to React Native, and my understanding is that functional components and hooks are the way to go. What I'm trying to do I've boiled down to the simplest case I can think of, to use as an example. (I am, by the way, writing in TypeScript.)
I have two Independent components. There is no parent-child relationship between the two. Take a look:
The two components are a login button on the navigation bar and a switch in the enclosed screen. How can I make the login button be enabled when the switch is ON and disabled when the switch is OFF?
The login button looks like this:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const handleClick = () => {
navigation.navigate('Away');
};
// I want the 'disabled' value to update based on the state of the switch.
return (
<Button title="Login"
color="white"
disabled={false}
onPress={handleClick} />
);
};
As you can see, right now I've simply hard-coded the disabled setting for the button. I'm thinking that will no doubt change to something dynamic.
The screen containing the switch looks like this:
const HomeScreen = () => {
const [isEnabled, setEnabled] = useState(false);
const toggleSwitch = () => setEnabled(value => !value);
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</SafeAreaView>
);
};
What's throwing me for a loop is that the HomeScreen and LoginButton are setup like this in the navigator stack. I can think of no way to have the one "know" about the other:
<MainStack.Screen name="Home"
component={HomeScreen}
options={{title: "Home", headerRight: LoginButton}} />
I need to get the login button component to re-render when the state of the switch changes, but I cannot seem to trigger that. I've tried to apply several different things, all involving hooks of some kind. I have to confess, I think I'm missing at least the big picture and probably some finer details too.
I'm open to any suggestion, but really I'm wondering what the simplest, best-practice (or thereabouts) solution is. Can this be done purely with functional components? Do I have to introduce a class somewhere? Is there a "notification" of sorts (I come from native iOS development). I'd appreciate some help. Thank you.
I figured out another way of tracking state, for this simple example, that doesn't involve using a reducer, which I'm including here for documentation purposes in hopes that it may help someone. It tracks very close to the accepted answer.
First, we create both a custom hook for the context, and a context provider:
// FILE: switch-context.tsx
import React, { SetStateAction } from 'react';
type SwitchStateTuple = [boolean, React.Dispatch<SetStateAction<boolean>>];
const SwitchContext = React.createContext<SwitchStateTuple>(null!);
const useSwitchContext = (): SwitchStateTuple => {
const context = React.useContext(SwitchContext);
if (!context) {
throw new Error(`useSwitch must be used within a SwitchProvider.`);
}
return context;
};
const SwitchContextProvider = (props: object) => {
const [isOn, setOn] = React.useState(false);
const [value, setValue] = React.useMemo(() => [isOn, setOn], [isOn]);
return (<SwitchContext.Provider value={[value, setValue]} {...props} />);
};
export { SwitchContextProvider, useSwitchContext };
Then, in the main file, after importing the SwitchContextProvider and useSwitchContext hook, wrap the app's content in the context provider:
const App = () => {
return (
<SwitchContextProvider>
<NavigationContainer>
{MainStackScreen()}
</NavigationContainer>
</SwitchContextProvider>
);
};
Use the custom hook in the Home screen:
const HomeScreen = () => {
const [isOn, setOn] = useSwitchContext();
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={setOn}
value={isOn}
/>
</SafeAreaView>
);
};
And in the Login button component:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const [isOn] = useSwitchContext();
const handleClick = () => {
navigation.navigate('Away');
};
return (
<Button title="Login"
color="white"
disabled={!isOn}
onPress={handleClick} />
);
};
I created the above by adapting an example I found here:
https://kentcdodds.com/blog/application-state-management-with-react
The whole project is now up on GitHub, as a reference:
https://github.com/software-mariodiana/hellonavigate
If you want to choose the context method, you need to create a component first that creates our context:
import React, { createContext, useReducer, Dispatch } from 'react';
type ActionType = {type: 'TOGGLE_STATE'};
// Your initial switch state
const initialState = false;
// We are creating a reducer to handle our actions
const SwitchStateReducer = (state = initialState, action: ActionType) => {
switch(action.type){
// In this case we only have one action to toggle state, but you can add more
case 'TOGGLE_STATE':
return !state;
// Return the current state if the action type is not correct
default:
return state;
}
}
// We are creating a context using React's Context API
// This should be exported because we are going to import this context in order to access the state
export const SwitchStateContext = createContext<[boolean, Dispatch<ActionType>]>(null as any);
// And now we are creating a Provider component to pass our reducer to the context
const SwitchStateProvider: React.FC = ({children}) => {
// We are initializing our reducer with useReducer hook
const reducer = useReducer(SwitchStateReducer, initialState);
return (
<SwitchStateContext.Provider value={reducer}>
{children}
</SwitchStateContext.Provider>
)
}
export default SwitchStateProvider;
Then you need to wrap your header, your home screen and all other components/pages in this component. Basically you need to wrap your whole app content with this component.
<SwitchStateProvider>
<AppContent />
</SwitchStateProvider>
Then you need to use this context in your home screen component:
const HomeScreen = () => {
// useContext returns an array with two elements if used with useReducer.
// These elements are: first element is your current state, second element is a function to dispatch actions
const [switchState, dispatchSwitch] = useContext(SwitchStateContext);
const toggleSwitch = () => {
// Here, TOGGLE_STATE is the action name we have set in our reducer
dispatchSwitch({type: 'TOGGLE_STATE'})
}
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={switchState}
/>
</SafeAreaView>
);
};
And finally you need to use this context in your button component:
// We are going to use only the state, so i'm not including the dispatch action here.
const [switchState] = useContext(SwitchStateContext);
<Button title="Login"
color="white"
disabled={!switchState}
onPress={handleClick} />
Crete a reducer.js :
import {CLEAR_VALUE_ACTION, SET_VALUE_ACTION} from '../action'
const initialAppState = {
value: '',
};
export const reducer = (state = initialAppState, action) => {
if (action.type === SET_VALUE_ACTION) {
state.value = action.data
}else if(action.type===CLEAR_VALUE_ACTION){
state.value = ''
}
return {...state};
};
Then action.js:
export const SET_VALUE_ACTION = 'SET_VALUE_ACTION';
export const CLEAR_VALUE_ACTION = 'CLEAR_VALUE_ACTION';
export function setValueAction(data) {
return {type: SET_VALUE_ACTION, data};
}
export function clearValueAction() {
return {type: CLEAR_VALUE_ACTION}
}
In your components :
...
import {connect} from 'react-redux';
...
function ComponentA({cartItems, dispatch}) {
}
const mapStateToProps = (state) => {
return {
value: state.someState,
};
};
export default connect(mapStateToProps)(ComponentA);
You can create more components and communicate between them, independently.

React native #5Navigation react testing library

I am new to React Native Testing Library. For my React native app I am using styled components and Typescript. I fetched the data and pass to my flatList. In Flat-list's render item I have created one global component where it display all the data which is wrap with one touchable container. When user will click the that touchable opacity it will go to single product details screen.
For testing the component I have created one mock container. And I wrap my touchable opacity component. I followed this article to create mocked navigator. I want to test the touchable opacity component and it navigate to the next screen. But I am getting error and it says:
The action 'NAVIGATE' with payload
{"name":"ProductDetails","params":{"product":{"__typename":"Product","id":"6414893391048","ean":"6414893391048","name":"T
shirt","brandName":"Denim","price":13.79 }} was not handled by any
navigator.
This my component
const navigation = useNavigation();
const onPress = () => {
trackProductView({
id: item.id ,
name: item.name,
});
navigation.navigate(Routes.ProductDetails, { product: item });
};
return (
<TouchableOpacity
accessible={true}
{...a11y({
role: 'menuitem',
label: item.name,
})}
onPress={onPress} // This is my onPress function
>
<ItemContainer>
<ProductTitleText ellipsizeMode={'tail'} numberOfLines={2}>
{item.name}
</ProductTitleText>
<QuantityModifierWrapper>
<QuantityModifier item={item!} />
</QuantityModifierWrapper>
</ItemContainer>
</TouchableOpacity>
);
This is my mocked container
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import 'react-native-gesture-handler';
import { MockedProvider } from '#apollo/client/testing';
type Props = {
screen: any;
params?: any;
};
const Stack = createStackNavigator();
const MockedNavigator = ({ screen, params = {} }: Props) => {
return (
<MockedProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name='MockedScreen'
component={screen}
initialParams={params}
/>
</Stack.Navigator>
</NavigationContainer>
</MockedProvider>
);
};
export default MockedNavigator;
This is my mocked screen
import React from 'react';
import { View } from 'react-native';
type Props = {
children: React.ReactNode;
};
const MockedScreen = ({ children }: Props) => {
return <View>{children}</View>;
};
export default MockedScreen;
This is my test suite where I am getting failed test
import React from 'react';
import { fireEvent, render, cleanup } from 'skm/utils/testing_utils';
import Touchablecomponent from './Touchable';
import MockedNavigator from './MockNav';
import MockedScreen from './Mockscreen';
describe('<Touchablecomponent/> ', () => {
test("render with invalid data", async () => {
const screenName = 'ProductDetails';
const component = (
<MockedNavigator
screen={() => (
<MockedScreen>
<ProductItemSmall item={mockData} />
</MockedScreen>
)}
// params={{data: mockData }}
/>
);
const { getByA11yRole, debug, toJSON } = render(component);
const item = getByA11yRole('menuitem');
console.log(fireEvent.press(item));
});
})

Navigation Error when navigating to Home page from Firebase registration

I'm following this guide: https://www.freecodecamp.org/news/react-native-firebase-tutorial/ in attempt to learn how to use firebase, and even though I've followed the code very closely, I'm receiving a NAVIGATION error:
The action 'NAVIGATE' with payload {"name":"Home","params":{"user":{"id":"AWSKEmmUsua5koR1V3x5bapc3Eq2","email":"tk#gmail.com","fullName":"t"}}} was not handled by any navigator.
Do you have a screen named 'Home'?
I do however, have a screen named Home. App.js:
import Home from './src/Home';
import Login from './src/Login/Login';
import Registration from './src/Registration/Registration';
const Stack = createStackNavigator();
export default function App() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
return (
<NavigationContainer>
<Stack.Navigator>
{ user ? (
<Stack.Screen name="Home">
{props => <Home {...props} extraData={user} />}
</Stack.Screen>
) : (
<>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Registration" component={Registration} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
When I use the Registration form to register a new user and Navigate to the Home page is when I get the error. Registration.js:
import { firebase } from '../firebase/config';
export default function Registration({ navigation }) {
const [fullName, setFullName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const onFooterLinkPress = () => {
navigation.navigate('Login');
}
const onRegisterPress = () => {
if (password !== confirmPassword) {
alert("Passwords do not match!");
return
}
// This works. However, navigation does not for some reason
firebase.auth()
.createUserWithEmailAndPassword(email, password)
.then((response) => {
const uid = response.user.uid
const data = {
id: uid,
email,
fullName
}
const usersRef = firebase.firestore().collection("users");
usersRef.doc(uid).set(data).then(() => {
// This is where the navigation error lies. It has nothing to do with the component
// This error happened even when I created a new plain Home component
navigation.navigate("Home", { user: data})
})
.catch((error) => alert(error))
})
.catch((error) => alert(error))
}
return (
....Input Forms
<TouchableOpacity
style={styles.loginButton}
onPress={() => onRegisterPress()}
>
<Text style={styles.buttonTitle}>Create Account</Text>
</TouchableOpacity>
I have used React Navigation before and haven't run into this issue. I am not using nested navigators and cannot see where the issue lies. Thank you for reading.
Adding to Göksel Pırnal answers:
At first, suppose there is no user. So We are in Registration Screen. At that stage, our navigator doesn’t even know whether there is any “Home” Screen. At this stage, our navigator only knows 2 screens: “Login” and “Registration” screens.
You need to notify our app.js whether anyone registered in the Registration screen or not. After that our app.js should change the value of 'user' in [user,setUser].
In your, App.js put this lines of code:
const [initializing,setInitializing]=useState(true)
useEffect(()=>{
const subscriber=firebase.auth().onAuthStateChanged((user)=>{
setUser(user)
setInitializing(false)
})
return subscriber
},[])
if (initializing) return null //Here you may use an Activity indicator
Then after rerendering our navigator will see the value of “user” has changed and it should navigate to the Home screen.
And guess what! You do not need to navigate manually from Registration Screen as you already put a condition in App.js ( in return () ).
You have a problem where you check the user value in App.js. After the registration is done, you did not assign the state in the App.js page and it will always be null. The Home page will not be added to the stack because the user value is null. That's why you got the error.
Solution: You need to notify App.js after registration.

React navigation. Render extra views on top of navigator

I'm trying to create a React Native app that renders an area with interchangeable screens and fixed view with some additional data/menu. I tried this solution form official React Navigation, but I can't make it work.
import React, { Component } from "react";
import {createStackNavigator} from 'react-navigation';
import { Text, View,} from 'react-native';
import Details from './DetailsScreen';
import MainScreen from './MainScreen';
const RootStack = createStackNavigator({
Main: {screen: MainScreen,
navigationOptions: {
header: null
}
},
Details: {
screen: Details,
},);
class App extends Component {
static router = {
...RootStack.router,
getStateForAction: (action, lastState) => {
return MyStack.router.getStateForAction(action, lastState);
}
};
render() {
const { navigation } = this.props;
return(
<View>
<Text>Foo</Text> //this is rendering
<RootStack navigation={navigation}/> //this is not
</View>);
}
}
export default App;
Article form link suggests that I can wrap object created with createStackNavigator() in a parent View, but it renders only when it is the only thing returned from render(), otherwise it seems to be ignored. Example from link claims that it is possible to "render additional things", but I can't make it work. Is it possible to do this way?
You can try this:
//wrapper.js
function wrapNavigator(Navigator, Wrapper, wrapperProps = {}) {
const WrappedComponent = props => (
<Wrapper { ...props, ...wrapperProps}>
<Navigator {...props} />
</Wrapper>
);
WrappedComponent.router = Navigator.router;
WrappedComponent.navigationOptions = Navigator.navigationOptions;
return WrappedComponent;
};
//app.js
const App = (props) => <View>
<Text>Some text...</Text>
{children}
</View>
export default wrapNavigator(Stack, App);
But this will not work if there are no parent navigator for App component.

Cannot listen for a key that isn't associated with a Redux Store - React Navigtion

I just upgraded my React Navigation to version 1.0.0. They have new ways to integrate the navigation and Redux. Here's my code
configureStore.js
export default (rootReducer, rootSaga) => {
const middleware = []
const enhancers = []
/* ------------- Analytics Middleware ------------- */
middleware.push(ScreenTracking)
const sagaMiddleware = createSagaMiddleware({ sagaMonitor })
middleware.push(sagaMiddleware)
const navMiddleware = createReactNavigationReduxMiddleware('root', state => state.nav)
middleware.push(navMiddleware)
/* ------------- Assemble Middleware ------------- */
enhancers.push(applyMiddleware(...middleware))
/* ------------- AutoRehydrate Enhancer ------------- */
// add the autoRehydrate enhancer
if (ReduxPersist.active) {
enhancers.push(autoRehydrate())
}
const store = createAppropriateStore(rootReducer, compose(...enhancers))
// kick off root saga
sagaMiddleware.run(rootSaga)
return store
}
ReduxNavigation.js
const addListener = createReduxBoundAddListener('root')
// here is our redux-aware our smart component
function ReduxNavigation (props) {
const { dispatch, nav } = props
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav,
uriPrefix: prefix,
addListener
})
return <AppNavigation navigation={navigation} />
}
const mapStateToProps = state => ({ nav: state.nav })
export default connect(mapStateToProps)(ReduxNavigation)
ReduxIndex.js
export default () => {
/* ------------- Assemble The Reducers ------------- */
const rootReducer = combineReducers({
//few reducers
})
return configureStore(rootReducer, rootSaga)
}
App.js
const store = createStore()
class App extends Component {
render () {
console.disableYellowBox = true
return (
<Provider store={store}>
<RootContainer />
</Provider>
)
}
}
export default App
And I got an error of
Cannot listen for a key that isn't associated with a Redux store. First call createReactNavigationReduxMiddleware so that we know when to trigger your listener
I hope someone can help me and please let me know if you needed more information
Thanks
It is clearly mentioned in the react-navigation docs that the Note: createReactNavigationReduxMiddleware must be run before createReduxBoundAddListener.
Whenever you do use the module after importing it, the listener is being called before the store is initialized.
So the simple fix is put the addListener in the ReduxNavigation function as
// here is our redux-aware our smart component
function ReduxNavigation (props) {
const addListener = createReduxBoundAddListener('root')
const { dispatch, nav } = props
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav,
uriPrefix: prefix,
addListener
})
return <AppNavigation navigation={navigation} />
}
const mapStateToProps = state => ({ nav: state.nav })
export default connect(mapStateToProps)(ReduxNavigation)
or you may make a wrapper class to the current class and bind the store to it as here
class RootContainer extends Component {
render () {
return (
<View style={{flex: 1, backgroundColor: '#fff'}}>
<StatusBar translucent barStyle='dark-content' backgroundColor='#fff' />
<ReduxNavigation/>
</View>
)
}
}
class App extends Component {
render () {
console.disableYellowBox = true
return (
<Provider store={store}>
<RootContainer />
</Provider>
)
}
}
I have made a sample starter kit for the same.Please checkout the link below
Sample Starter Kit
For those who struggle with it, be sure the import class in your App.js are first
import configureStore from '../Redux/configureStore'
(where you configure your Navigation Middleware)
and second or after:
import ReduxNavigation from '../Navigation/ReduxNavigation'
(where you call createReduxBoundAddListener )
Otherwise you'll keep having this message