In a react-native child-component, I need to read both parent-props and navigation.
I use this code in parent to pass DIC-props to child, which works just fine:
...
<Stack.Screen name="SignIn>
{(props) => <SignIn {...props} DIC={DIC} />}
</Stack.Screen>
...
In Child comp. I get that prop (DIC) like this, so far all fine:
const SignIn = (props) => {
const { DIC } = props
...
}
But in Child I need now to get navigation from props too, but this does not work (navigation appears as an empty object)
const SignIn = (props, {navigation}) => {
const { DIC } = props
...
Can someone see what am I doing wrong? How can I get both specific props AND navigation? Thx!
I really recommend you using Typescript so that you can better understand what is happening under the hood. Anyway, as for your question, this should work for you:
const SignIn = ({ navigation, route }) => {
const DIC = route.params.DIC
...
}
you can get navigation like this:
const { DIC, navigation } = props;
navigation comes within props.
Related
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.
I am using setState in order to dynamically update an image's source when a button is pushed in React Native. However, I am also using the TypeWriter library which types out text with a special 'typewriter' animated effect.
When my setState is called to change the element and the page is rerendered, TypeWriter types the text out again. I don't want this. Is there a way to exclude my TypeWriter text from being rerendered?
Code snippet:
export const AccountScreen = ({ navigation }) => {
this.state = {
img1Src: require('../assets/img/token.png')
}
const [state, setState] = useState(this.state);
changeImgSrc = () =>{
setState({
img1Src: require('../assets/img/X2.png')
})
}
return (
<TypeWriter> //I don't want this to re-render on setState
<Text>My Account</Text>
</TypeWriter>
<Animatable.Image source={state.img1Src}/>
<Button onPress={this.changeImgSrc}>
Click me!
</Button>
etc...//
export const NewComponent = ({ navigation }) => {
const [state, setState] = useState({
img1Src: require('../assets/img/token.png')
});
changeImgSrc = () =>{
setState({
img1Src: require('../assets/img/X2.png')
})
}
return (
<Animatable.Image source={state.img1Src}/>
<Button onPress={this.changeImgSrc}>
Click me!
</Button>
etc...//
You can make a new component is called NewComponent. Then,
export const AccountScreen = ({ navigation }) => {
return (
<TypeWriter> //I don't want this to re-render on setState
<Text>My Account</Text>
</TypeWriter>
<NewComponent />
etc...//
Also, you cannot use this in the functional component. In addition, if you want to call a function in functional component, you must use it with useCallback.
I want to pass something by screenProps in React-navigation v5.x.x. I am one of the newcomers in react-native. Can anyone help me?
There's no screenProps in React Navigation 5. You can use React's Context feature instead to pass data down the tree without an extra API.
https://reactnavigation.org/docs/upgrading-from-4.x#global-props-with-screenprops
in my case I am passing my data like :
props.navigation.navigate('toScreen', {
resumeDetail: data,
})
and you can access it like :
detail = this.props.navigation.state.params.resumeDetail;
https://reactnavigation.org/docs/screen/#children
Render callback to return React Element to use for the screen:
<Stack.Screen name="Profile">
{(props) => <ProfileScreen {...props} />}
</Stack.Screen>
You can use this approach instead of the component prop if you need to pass additional props. Though we recommend using React context for passing data instead.
Note: By default, React Navigation applies optimizations to screen components to prevent unnecessary renders. Using a render callback removes those optimizations. So if you use a render callback, you'll need to ensure that you use React.memo or React.PureComponent for your screen components to avoid performance issues.
This is what I use at the moment:
// ...
export const ScanPage = React.memo(ScanComponent);
function useScreenWithProps(
PageComponent: React.FC<Props>,
props: Props
) {
// Take note of the double arrow,
// the value useMemo returns is a function that returns a component.
return useMemo(
() => (navigationProps: NavigationProps) => (
<PageComponent {...navigationProps} {...props} />
),
[PageComponent, props]
);
}
const Stack = createStackNavigator();
const Navigator: React.FC<Props> = (props) => {
const scan = useScreenWithProps(ScanPage, props);
const activate = useScreenWithProps(ActivatePage, props);
const calibrate = useScreenWithProps(CalibratePage, props);
const success = useScreenWithProps(SuccessPage, props);
const error = useScreenWithProps(ErrorPage, props);
return (
<Stack.Navigator>
<Stack.Screen name="Scan">{scan}</Stack.Screen>
<Stack.Screen name="Activate">{activate}</Stack.Screen>
<Stack.Screen name="Calibrate">{calibrate}</Stack.Screen>
<Stack.Screen name="Success">{success}</Stack.Screen>
<Stack.Screen name="Error">{error}</Stack.Screen>
</Stack.Navigator>
);
};
I've got a React Native app with React Navigation and I need to show or hide a tab based on Redux state. I'm using createBottomTabNavigator and have written this:
function createTabNavigator(props:TabNavigatorProps){
debugger;
const isClient = WHAT_TO_PUT_HERE??
const tabs = isClient
? {
//something
}
: {
//something else
}
return createBottomTabNavigator(
tabs,
{
defaultNavigationOptions: ({ navigation }) => ({
tabBarIcon: ... ,
tabBarLabel: ...,
}
tabBarComponent: (props) => customTabBar(props, isClient),
tabBarOptions: {
...
},
});
};
My createTabNavigator works the way as intended if I manually set isClient to true or false, though I need this from redux state.
I've also tried connecting it, and mapStateToProps get called with the correct state, however it doesn't call createTabNavigator again so state changes aren't updated to my view hierarchy.
How can I add/remove a tab (and actually re-render altogether as that tab also involves some custom styling/rendering of the whole bar) based on Redux state?
react-navigation does not provide any API to hide a tab from BottomTabNavigator.
However, it allows providing custom TabBarComponent. It also exposes BottomTabBar component which is the default TabBarComponent for BottomTabNavigator.
This BottomTabBar component takes a property getButtonComponent to get the component for rendering each tab. It is provided the tab details in argument and is expected to return component to be used for rendering that tab.
To achieve hiding of tabs dynamically, you provide your own function for getButtonComponent. Check the details of tab with your state to decide whether it should be visible or hidden. Return an 'empty component' if you want to hide it, else simply call the default.
/**** Custom TabBarComponent ****/
const BlackHole = (props) => []
let _TabBarComponent = (props) => {
const getButtonComponent = (arg) => {
const hide = props.hiddenTabs[arg.route.key];
if (hide) {
return BlackHole;
}
return props.getButtonComponent(arg)
}
return (<BottomTabBar {...props} getButtonComponent={getButtonComponent}/>);
};
const mapStateToProps = (state) => ({
hiddenTabs: state.hiddenTabs
});
const TabBarComponent = connect(mapStateToProps)(_TabBarComponent);
/**** Navigation ****/
const TabNavigator = createBottomTabNavigator({
Tab1: createStackNavigator({Screen1}),
Tab2: createStackNavigator({Screen2}),
Tab3: createStackNavigator({Screen3}),
}, {
tabBarComponent: props => (
<TabBarComponent {...props} style={{borderTopColor: '#605F60'}}/>
)
});
See this snack for POC: https://snack.expo.io/BJ80uiJUH
We had the same requirement but in react navigation we cannot have dynamic tab, as we have to define all the routes statically well in advance.
Check this answer, in which I have explained in detail about how we achieved it.
Also have mentioned other possible approach (which we have not done).
There is one more interesting article you might want to check on this.
Make sure you are not calling redux state inside a HOC Component ,
For state management ,you can try to render it inside a component , try this logic :
class TabIndex extends Component {
render() {
// coming from redux
let isClient= this.props.isClient;
return (<View>
{isClient?<TabIndexNavigator ifYouNeed={ isClient } />
:
null}
</View>)
}
}
module.exports = TabIndex
then import it normally in your main stackNavigator
const StackMain = StackNavigator(
{
TabIndex: {
screen: TabIndex
},
...
}
I'm trying to pass params into a new screen, and implemented it like mentioned here.
I have the following TouchableOpacity button.
<TouchableOpacity
onPress={() => {
this.props.navigation.navigate('SomeScreen', {
title: 'Title',
subTitle: 'Subtitle',
});
}}
>
On the other page (let's call it Somescreen), I have the following:
render() {
const { navigation } = this.props;
const title = navigation.getParam('title');
}
But title above is undefined:
{ params: undefined, routeName: "Somescreen", key: "id-xx" }
My rootStack:
const RootStack = createStackNavigator({
SomescreenA: { screen: SomescreenA },
SomescreenB: { screen: SomescreenB },
}, { headerMode: 'none' });
Why are my params undefined in a new screen?
If you face a situation where your target screen get undefined params, probably you have a nested navigation stack.
Here you have to pass params to the navigate method in this way:
navigation.navigate('Root', {
screen: 'Settings',
params: { user: 'jane' },
});
For more information read this page in the official docs:
https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator
In my specific case, I was calling a nested navigator, so I had to manage how send those params to their specific screen, so I did this:
Send params this way...the regular way:
navigation.navigate(
'OrderNavigator',
{itemSelected},
);
Then, from navigator stack I did this:
const OrderNavigator = ({route: {params}}) => {
return (
<Stack.Navigator initialRouteName="Order">
<Stack.Screen name="Order" component={Order} options={{headerShown: false}} initialParams={params} />
</Stack.Navigator>
);
};
And that's it. Then from the screen I got them like this:
const Order = ({route}) => {
const {itemSelected} = route.params;
const {first_name, last_name} = itemSelected;
return (...)
}
I've, unfortunately, encountered cases where navigate(route, params, ...) wouldn't pass the params object, just like you did.
As a workaround, I use the other variant - navigate({routeName, params, action, key}) that you can find here. It always works.
The accepted answer workaround did not work for me, so apparently if you use children to render your component (in screen options) and pass route as a prop, it works
if you are on react navigation v6^ use the useRoute hook to access the params object
const route = useRoute();
useRoute is a hook that gives access to the route object. It's useful when you cannot pass the route prop into the component directly, or don't want to pass it in case of a deeply nested child.
below is an implementation of this
import { useNavigation, useRoute } from '#react-navigation/native';
import { Pressable, Text } from 'react-native';
function Screen1() {
const navigation = useNavigation();
return (
<Pressable
onPress={() => {
navigation.navigate('Screen2', { caption: 'hey' });
}}
>
<Text> Go to Screen 2 </Text>
</Pressable>
);
}
function Screen2() {
const route = useRoute();
return <Text>{route.params.caption}</Text>;
}