How to update DrawerLabel text (via navigationOptions) on DrawerOpen/Close event - react-native

I have define drawerLabel in navigationOptions as follows :
export default class Something extends React.Component {
static navigationOptions = {
drawerIcon: () => (
<Image source={drawerImage} style={styles.imageStyle} />
),
drawerLabel: ()=>{
if(~some condition~){
return 'New Label'
}else if(~some other condition~){
return 'Another Label'
}else{
return 'label'
}
},
};
This updates the label perfectly when any drawerItem is pressed in the drawer.
(apparently opening ANY drawer item updates the navigationOptions of all routes)
But I want this drawerLabel to be updated on drawer.Open() call as well.
or when left-header is pressed on any screen (triggers drawer.Open() )
So, how can I achieve this behaviour on DrawerOpen/Close ?
I am able to achieve this on pressing any drawerItem

Pass navigation prop to your navigationOptions.
static navigationOptions = ({ navigation }) => ({
drawerLabel: ()=>{ // maybe you also need to pass navigation to this function through the parentheses (navigation)
if(~some condition~){
return navigation.state.params.labels.first
}else if(~some other condition~){
return navigation.state.params.labels.second
}else{
return navigation.state.params.labels.third
}
},
});
Obviously, you need to create the labels:
navigation.setParams({ labels: { first: 'labelOne', second: 'labelTwo', third: 'labelThree' }})
before you can access them.
If you don't understand how to add the labels to the params - don't hesitate asking.
You can also use screenProps, if you have static labels. In this case you will need to define these props in your root-navigator. After that you will be able to access them in navigationOptions if you pass them alongside with navigation:
static navigationOptions = ({ navigation, screenProps }) => ({

I managed to achieve the desired output as follows (im afraid it might cause performance issues in the long run) :
I created a custom component and forced it to re-render every second using :
componentDidMount() {
this.interval = setInterval(() => this.setState({ time: Date.now() }), 1000); }
And used that component in the navigationOption's drawerLabel :
drawerLabel: ()=> {
return <CustomComponent />
}
Now it does not need a click event on either any drawerItem or in drawerToggle call. (label gets updated even if the drawer is left open and no click is fired at all)
Following is the CustomComponent's render code :
if(~some condition~){
return <Text> New Label </Text>
}else if(~some other condition~){
return <Text> Another Label </Text>
}else{
return <Text>label</Text>
}
Now the if/else condition gets checked every second and a Text component is rendered as a label accordingly.
Even though I managed to achieve what I required, I would like to know if this affects the performance of the application?
(if so should I optimize this somehow? ... async maybe)

Related

Show a tab bar item or not based on Redux state in React Native

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
},
...
}

How to change navigation header button style based on a state property in React Native using React Navigation?

I'm using react navigation in my RN app and trying to implement a form to submit some information. I'm using a button at the right header and want to style the button with different color to indicate whether the form is legal (e.g., white for a legal form and transparant for having important inputs left blank).
I use this.state.submitDisabled to indicate its legality and define the right header in componentDidMount() and pass the navigation param to the header to render in navigationOptions:
this.props.navigation.setParams({
headerRight: (
<MaterialHeaderButtons>
<Item title="submit" iconName="check"
color={this.state.submitDisabled ? colors.white_disabled : colors.white}
onPress={() => {
if (!this.state.submitDisabled) {
this.postEvent();
}
}}/>
</MaterialHeaderButtons>
),
});
However, the color change statement based on the value of this.state.submitDisabled did not work. I've checked its value, when this.state.submitDisabled is changed, the color of the button does not change. It seems that the color is determined when set as navigation params as above and will not change since then.
How can I achieve the effect of what I described?
when ever you change value of state also change navigation param too.see example
export class Example extends Component {
static navigationOptions = ({ navigation }) => {
const showModal = get(navigation, 'state.params.showModal');
return {
headerTitle: <Header
showModal={showModal}
backIcon />,
headerStyle: HeaderStyle,
headerLeft: null,
};
};
constructor(props) {
super(props);
this.state = {
showModal: false,
};
}
componentDidMount = () => {
this.props.navigation.setParams({
showModal: this.state.showModal,
});
}
handleModal=(value)=>{
this.setState({showModal:value});
this.props.navigation.setParams({
showModal: this.state.showModal,
});
}
render() {
return null;
}
}
In your implementation this.state.submitDisabled is not bound to the screen. Try the following:
static navigationOptions = ({ navigation }) => {
headerRight: (
<MaterialHeaderButtons>
<Item
title="submit"
iconName="check"
color={navigation.getParam('submitDisabled') ? colors.white_disabled : colors.white}
onPress={navigation.getParam('handlePress')}
/>
</MaterialHeaderButtons>
),
})
componentWillMount() {
this.props.navigation.setParams({
submitDisabled: this.state.submitDisabled,
handlePress: () => {
if (!this.state.submitDisabled) {
this.postEvent();
}
}
});
}

How to pass a component's function to a component inside of navigationoptions

so I've been dealing with this problems for days now and have been googling a lot and trying to implement a solution. Most solutions suggest using
this.props.navigation.setParams({function: this.function.bind(this)) inside of componentDidMount and then doing something like this in navigationOptions ->
static navigationOptions = {
header: ({navigation}) =>
<Button onPress = { () => navigation.state.params.function}/> }
This works for buttons with onPress but this method doesn't seem to work when it's just a 'props' to a Custom Component.
This is my code
static navigationOptions = {
header: ({navigation}) =>
<CustomCar refreshCar = {navigation.state.params.refreshCarSearch}
/>
transitionConfig: () => ({
transitionSpec: {
duration: 0,
timing: Animated.timing,
easing: Easing.step0,
},
}),
};
constructor(props) {
super(props)
this.state = {
Car: ' '
}
this.refreshCarSearch = this.refreshCarSearch.bind(this);
}
componentDidMount() {
this.props.navigation.setParams({ refreshCarSearch: this.refreshCarSearch });
}
refreshCarSearch(e){
this.setState({
Car: e
});
}
I always get navigation.state.params.refreshCarSearch is undefined. I've tried a lot of variations of this method but to no avail. This method only works if I replace the component with a button with onPress, but I need to pass this function over. Basically, I need to have my CustomCar component (which must reside at the top of the screen, so in navigationOptions) pass a value back to this Component so refreshCarSearch can happen. The only way I've read how to do this is with Redux or this case where the parent component passes the refreshCarSearch function to the 'child' component. The component existing in navigationOptions makes it much more difficult, is this possible?
If you want this customer Car component at the top of the screen you should call this component at the top in render function and then you will get your refreshCarSarch this.props.navigation.state.params.refreshCarSearch
eg:
render(){
return (
<CustomCar refreshCar = {()=>this.props.navigation.state.params.refreshCarSearch()}
/>
)
}
Consider a parent component, you are sending a function called refreshCarSearch on props which doesn't work in your case.
<ParentComponent refreshCarSearch={this.refreshCarSearch}/>
So, you have to send refreshCarSearch like this
<ParentComponent screenProps={{refreshCarSearch:this.refreshCarSearch}}/>
In navigationOptions of your child component you can access it like this
static navigationOptions = ({navigation, screenProps}) {
header: () =>
<Button onPress = { () => screenProps && screenProps.refreshCarSearch}/>
}
One more thing to add here is I'm not sure whether params on header returns navigation object but the one above returns the navigation and screenProps object(navigationOptions static method params).
I hope this helps you.

React Native Handle Headerback with Redux

I have integrated Redux to my RN Project. I can navigate between screens with buttons, but when I want to go back with the HeaderBackButton it says: "undefined is not a function"
Github-Repo: https://github.com/bayraktarhasan/React-Navigation-Redux-Globalization-Example.git
My Feed Component:
class Feed extends Component {
static navigationOptions = {
title: 'Hello Ahmet',
headerLeft: <HeaderBackButton onPress={this.goBack()} />,
}
constructor(props) {
super(props);
this.goBack = this.goBack.bind(this);
}
goBack = () => {
this.props.navBack();
}
render() {
return (
<View style={styles.container}>
<Text>{I18n.t('feedComponent')}</Text>
<Button
title={I18n.t('back')}
onPress={this.goBack}
/>
</View>
);
}
}
export default connect(null, { navBack, navToProfile })(Feed);
Reducer:
import { NAVIGATE_BACK, NAVIGATE_PROFILE, NAVIGATE_FEED } from '../Actions/types';
const firstAction = AppNavigator.router.getActionForPathAndParams('Main');
const initialNavState = AppNavigator.router.getStateForAction(
firstAction
);
function nav(state = initialNavState, action) {
console.log(action.type);
let nextState;
switch (action.type) {
case NAVIGATE_BACK:
nextState = AppNavigator.router.getStateForAction(
NavigationActions.back(),
state
);
break;
This is just a shot in the dark, but goBack isn't really defined. Maybe try:
const goBack = () => {
this.props.navBack();
}
This is very ironic, because I was having the same problem yesterday. Here's the post How to properly assign a url as a prop to action creator
It might not be very clear from the above link what might need to be done in your case. But I'll try to explain what I've discovered (I'm not an expert in any way). Your code would work on a text input or a date picker or something of that sort. Like this:
<TextInput
label="Model"
placeholder="4020"
value={this.props.model}
onChangeText={value => this.props.entryUpdate({ prop: 'model', value })}
/>
Where the text input has a couple of props already. Here, you assign value to
this.props.<whatever you want the specific object/prop to be called>
But with a button, you have to assign an actual prop name/variable to pass to your action creator. Here you don't have an actual prop. So, it comes back as undefined. So, you have to declare a variable name for that prop and assign it to this.props just before:
this.props.navBack();
So maybe it would be:
const goBack = () => {
const { back } = this.props
this.props.navBack({ back });
}
And in your case, you might need to specify a specific value for the prop { back }. Otherwise, it'll tell you that your second argument is undefined as well.
Here's a cool debugger that helps you see if your props are actually being passed to the action creator:
https://github.com/jhen0409/react-native-debugger
Hope that helps more than it confuses. Good luck!

Where to initialize data loading with react-navigation

I'm using react-navigation and here is my structure :
The root stack navigator :
export const Root = StackNavigator({
Index: {
screen: Index,
navigationOptions: ({ navigation }) => ({
}),
},
Cart: {
screen: Cart,
navigationOptions: ({ navigation }) => ({
title: 'Votre panier',
drawerLabel: 'Cart',
drawerIcon: ({ tintColor }) => <Icon theme={{ iconFamily: 'FontAwesome' }} size={26} name="shopping-basket" color={tintColor} />
}),
},
...
My structure looks like this :
StackNavigator (Root)
DrawerNavigator (Index)
TabNavigator
MyPage
MyPage (same page formatted with different datas)
...
So my question is, where do I load my data, initialize my application ? I need somewhere called once, called before the others pages.
The first page displayed in my application is the MyPage page. But as you can see, because of the TabNavigator, if I put my functions inside, it will be called many times.
Some will says in the splashscreen, but I'm using the main splashscreen component and I don't have many controls over it.
I thought about my App.js where we create the provider, but I don't think this is a good idea ?
const MyApp = () => {
//TODO We're loading the data here, I don't know if it's the good decision
ApplicationManager.loadData(store);
SplashScreen.hide();
return (
<Provider store={store}>
<Root/>
</Provider>
);
};
What is the good way to do it ?
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return <SplashScreen />
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
TabNavigator by default renders/loads all its child components at the same time, but if you set property lazy: true components will render only if you navigate. Which means your functions will not be called many times.
const Tabs = TabNavigator(
{
MyPage : {
screen: MyPage
},
MyPage2 : {
screen: MyPage,
}
}
},
{
lazy: true
}
);
If you use this structure and call fetching data inside of MyPage you can add logic in componentWillReceiveProps that will check is data already in store and/or is it changed before fetching new data. Calling your fetch functions from MyPage gives you the ability to pull fresh data on every page/screen visit or do "pull to refresh" if you need one.
You could also pull initial data in splashscreen time, I would just not recommend pulling all your app data, data for all screens, at that time since you probably don't need it all at once. You can do something like:
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return null
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
class Root extends Component {
componentDidMount() {
SplashScreen.hide();
}
...
}
You should do it in App.js or where you initialize your StackNavigator. If I were you, I would put a loading screen, which would get replaced by the StackNavigator structure once the data is ready.
I wouldn't do it in the App because you lose control. Sadly I haven't used react-navigation or redux but I see that the TabNavigator has a tabBarOnPress method, which I would use to trigger the loading. You can load every page data on demand.
https://reactnavigation.org/docs/navigators/tab#tabBarOnPress