Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic number of screens in TabNavigator #1872

Closed
danielang opened this issue Jun 14, 2017 · 19 comments
Closed

Dynamic number of screens in TabNavigator #1872

danielang opened this issue Jun 14, 2017 · 19 comments

Comments

@danielang
Copy link

Situation

I currently write a news reader app for a magazine, which publishes the content in English and German under different categories. The number of categories per language is different. The categories are stored in as an array per language.

CATEGORIES_EN = [
    {
        selector: '*',
        blog: BLOG_EN,
        id: `${BLOG_EN}_*`,
    },
    {
        selector: 'Politics',
        blog: BLOG_EN,
        id: `${BLOG_EN}_Politics`,
    },
    // ... 8 more
];

CATEGORIES_DE = [
    {
        selector: '*',
        blog: BLOG_DE,
        id: `${BLOG_DE}_*`,
    },
    {
        selector: 'Politik',
        blog: BLOG_DE,
        id: `${BLOG_DE}_Politik`,
    },
    // ... 9 more
];

The screen component is always the same, but must receive the selector and the blog somehow.

Question

How can I change the number of screens, when the the language has changed?
How can I assign the categories to the screen component?

Environment

software version
react-navigation 1.0.0-beta.11
react-native 0.45.0
node 6.10.3
npm 4.0.5
@matthamil
Copy link
Member

Check out some of the solutions to a similar problem in #717

@danielang
Copy link
Author

Thank you @matthamil for your hint!

This is how i got it to work:

tabs(categories) {
    return categories.reduce((routes, category) => {
        routes[category.get('id')] = this.tab(category);

        return routes;
    }, {});
}

tab(category) {
    const { t } = this.props;

    const screen = this.getTabForCategory(category);

    return {
        screen: screen,
        navigationOptions: {
            title: t(category.get('selector')),
        }
    }        
}

getTabForCategory (category){
    return () => (<ArticlesList category={category} />);
}

render() {
    const { categories } = this.props;

    const Tabs = TabNavigator(this.tabs(categories), {
        tabBarComponent: TabBarTop,
        tabBarPosition: 'top',
        tabBarOptions: {
            scrollEnabled: true,
            upperCaseLabel: false,            
        }
    });
    
    return (<Tabs />);
}

If anybody has suggestions for improvement, please share it :)
I'll close this issue now.

@oximer
Copy link

oximer commented Nov 20, 2017

@danielang I tried your solution, however I'm losing your screen state.
When the category changes, a new TabNavigator is created. Thus, I lost the screen state.

It's possible to update the currentTabNavigator, adding a new tab on it?

@danielang
Copy link
Author

No, this is not possible. When you change the numbers of tabs, this always requires a complete rerender of the TabNavigator. In my app this only occurs when the user has changed the language.
I've used redux to persist my state.

@petrogad
Copy link

petrogad commented Jan 5, 2018

@danielang Thanks for the post above; I'm attempting to nest a dynamically created tab navigator similar to how you're creating the above snippet.

I'm curious if you've worked through this at all.

I attempted the following:
I assume your class is called CustomTab, I import CustomTab to my parent; container then assign the PartentContainer.router = CustomTab.router pass in CustomTab the navigation param and lastly pass in that navigation to the <Tabs> element.

This still produces the forEach undefined error making me think something hasn't initiated yet. Any ideas on how best to do this?

@petrogad
Copy link

petrogad commented Jan 5, 2018

@AnthonyLamot
Copy link

@danielang thanks for your very useful example!

I do encounter an issue with that approach. I keep my navigation state in a Redux store. Previously I had navigation being passed down as a prop to the screens, from where I could dispatch() etc. To do this I followed the instructions here: https://reactnavigation.org/docs/redux-integration.html.

However, it seems that, by introducing an extra component to "wrap" (in my case) a DrawerNavigator, the navigation prop being passed down is no longer functional. Now, when I try to do things like this.props.navigation.dispatch() that simply doesn't work anymore, so it seems the wrong (?) navigation prop is passed down.

Is there a workaround for that?

One thing I tried was to replicate the Redux integration with React Navigation, not only at the top level of my app, but also at the level of this "wrapper" component, i.e. something like this:

      <DrawerWrapper navigation={addNavigationHelpers({
        dispatch: this.props.dispatch,
        state: this.props.nav,
        addListener,
      })} />

(again, taken from https://reactnavigation.org/docs/redux-integration.html)

However that caused errors I simply couldn't interpret, so I doubt it is the right approach.

@nicolasconnault
Copy link

@AnthonyLamot the link you shared at the end of your post makes it quite clear that storing your navigation state in your redux store is a bad idea :)

@AnthonyLamot
Copy link

@nicolasconnault thanks. I see that that article has been updated in the meantime. I'm guessing that the React Navigation guys got tired of all the Redux related questions. ;-)

That might cause one hell of a refactor though...

@nicolasconnault
Copy link

nicolasconnault commented Jul 25, 2018 via email

@tanveerbyn
Copy link

can anyone make Snack example for it coz m getting confused....

@nicolasconnault
Copy link

can anyone make Snack example for it coz m getting confused....

I'm working on one now, there's a little bug that's preventing it from running, because I had to strip about 75% of my code to only keep what's relevant, but you should get the idea of it.

https://snack.expo.io/@nicolasconnault/example-of-dynamic-tabs-with-react-navigation-v2

@tanveerbyn
Copy link

tanveerbyn commented Sep 16, 2018

can anyone make Snack example for it coz m getting confused....

I'm working on one now, there's a little bug that's preventing it from running, because I had to strip about 75% of my code to only keep what's relevant, but you should get the idea of it.

https://snack.expo.io/@nicolasconnault/example-of-dynamic-tabs-with-react-navigation-v2

thanks its working now.. but how do I use different State for every screen...

@devpascoe
Copy link

Anyone got this working with React Navigation 3?

@nicolasconnault
Copy link

nicolasconnault commented Dec 21, 2018

@tanveerbyn You can pass different params to each screen, if that's what you mean. You can then use the params using this.props.navigation.getParam('paramName')

@devpascoe I just upgraded to React Navigation 3 and this works just fine, there are very few changes to be made. I've updated the Snack above with comments to show the extra code required for this.

@devpascoe
Copy link

Ideally I want to wrap the bottom tab bar in a component so I can render dynamically setting an array of components for the tab’s tabitems based on an external api’s result.
I’ve followed a bunch of examples close to this but just can’t quite nail it. One issue being the navigation prop or screenProps not being passed (even though I set it).

Would love an updated example code if anyone is feeling generous (it is Christmas after all) of laying out a bottomTabBar with a set of tabs defined after a setTimeout etc to demo the async result of tabs to load.

@nicolasconnault
Copy link

One issue being the navigation prop or screenProps not being passed (even though I set it).

Yes I've had that issue too. To overcome this, I pass the navigation object from the parent component to each tab, and call it topNavigation. Then each tab screen uses this instead of this.props.navigation.

I've updated my Snack to show an example of this. Check Customers.js and components/CustomersTab.js

@devpascoe
Copy link

I ended up writing something like this to only load the component once the tab configuration was fetched from an api and saved into a global variable and used state to only show the component once ready.

  fetchTabs = async () => {
    const fetchUrl = `urltoapi/menu`
    const response = await fetch(fetchUrl)
    global.tabs = await response.json()
    AppScreen = require('./screens/AppScreen').default
    this.setState({ loadedTabs: true })
  }

...and in AppScreen its default returns the createBottomTabNavigator with the tabs object being the result of the following tabs() function

const tabs = () => {
  const tabItems = {}
  global.tabs.forEach(tab => {
    if (tab.screen == 'Feed') {
      tabItems['Feed'] = FeedStack
    } else if (tab.screen == 'Chat') {
      tabItems['Chat'] = ChatStack
    } else if (tab.screen == 'Profile') {
      tabItems['Profile'] = MyProfileStack
    } else if (tab.screen == 'Contact') {
      tabItems['Contact'] = ContactScreen
    }
  })
  return tabItems
}

So i'm basically doing whatever i can to delay loading and then showing the createBottomTabNavigator until i have prepared the tabs object for it to use since it needs that result returned synchronously.

@robinvw1
Copy link

@nicolasconnault I was looking at your Snack, but it is showing an error on startup. Can you update it maybe? That would be gladly appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants