Closed kennethstarkrl closed 8 months ago
I was able to come up with a solution. There's likely a better way to do this and needs error handling, but it's working for my needs. I added a slug param to the Tab Screen options and set the name as the title, which is unique for each tab. Then modified expo-router/src/useScreens.tsx
to basically inject duplicates on the fly of the child found based on the one slug file, and use the slug param for the route instead of the name.
So far it seems to work with out issues for me, and with complexity of using nested navigators both a drawer and tabs. I'm sure someone can make this better. It's a starting point at least.
tabs/_layout.tsx
export default function TabsLayout(){
const [tabs,setTabs] = useState([])
useEffect(()=>{
//fetch whatever data for each tab
setTabs([{id:1,title:'tab1'},{id:2,title:'tab2'},{id:3,title:'tab3'}]);
},[]);
return(
<Tabs>
{tabs.map((tab,index)=>{
return(
<Tabs.Screen
key={tab.id}
name={tab.title}
options={{href:`tabs/${tab.id}`,title:tab.title,slug:'[tabs]'}}
/>
)
})}
</Tabs>
)
}
expo-router/src/useScreens.tsx
const ordered = order
.map(({ name, redirect, initialParams, listeners, options },index) => {
if (!entries.length) {
console.warn(
`[Layout children]: Too many screens defined. Route "${name}" is extraneous.`
);
return null;
}
let matchIndex = entries.findIndex((child) => {return(child.route === name || child.route === options?.slug)});
if (matchIndex === -1) {
console.warn(
`[Layout children]: No route named "${name}" exists in nested children:`,
children.map(({ route }) => route)
);
let dynamicChild = {...children[children.findIndex((child)=>{return(child.route === options?.slug)})],route:name? name : `dynamicScreen${index}`};
entries.push(dynamicChild);
matchIndex = entries.findIndex((child) => {return(child.route === name || child.route === options?.slug)});
}
if(matchIndex >= 0) {
// Get match and remove from entries
const match = entries[matchIndex];
entries.splice(matchIndex, 1);
// Ensure to return null after removing from entries.
if (redirect) {
if (typeof redirect === "string") {
throw new Error(
`Redirecting to a specific route is not supported yet.`
);
}
return null;
}
return { route: match, props: { initialParams, listeners, options } };
}
})
.filter(Boolean) as {
route: RouteNode;
props: Partial<ScreenProps>;
}[];
Is the below issue similar or different?
https://github.com/EvanBacon/expo-router-layouts-example/issues/4
hey mate, did you make any progress on this ? facing a similar issue
@EvanBacon Thanks for this awesome library. Is there any specific reason to restrict this kind of implementation as it's working fine with react-navigation. I'm trying to migrate my old expo app to use expo-router and this is a blocker for me.
Same issue here - wondering if I'm missing something or anyone has figured out a workaround! - would like to have:
[user]
route passing the params to give it the dynamicism inside the container
{aggregatedTabData.map((tabData) => (
<Tabs.Screen
key={tabData.href}
// Name of the dynamic route.
name={"[user]"}
options={{
title: tabData.name,
// Ensure the tab always links to the same href.
href: {
pathname: "/[user]",
params: {
user: tabData.href,
},
},
}}
/>
))}
same issue, trying to make dynamic tabs from api. ->app -->(tabs) ---> layout.tsx -> fetch api and return tabs with [tab.name] --->[tab.name] (coming from api) ---->index.tsx
does someone find any working solution?
I am not sure this use case is supported. You can add a Tab.Screen for a corresponding file in the same directory as the _layout
file.
Same issue here, trying to build a tab navigator only on one route, and link to that route with a search param, them use that param to generate the tabs.
User sees mural of notes -> user clicks to a note and navigates to /note with id param -> user sees a tab navigation with a screen to edit that note and a screen to view the rendered markdown version of that note.
All works fine except tabs dont show.
└── app/
└── index.js
└── note/
└── edit/
└── [id].js
└── view/
└── [id].js
└── newnote.js
└── __layout.js /<-- Tabs (that dont work)
└── _layout.js
Any progress on this one? I also need this feature
This repo is not maintained you need to check expo/expo
Same issue here - wondering if I'm missing something or anyone has figured out a workaround! - would like to have:
- Dynamic tabs based on a small subset of fetched data
- Display a name from the data as the Title through options for each
- Have the href navigate to the dynamic
[user]
route passing the params to give it the dynamicism inside the container{aggregatedTabData.map((tabData) => ( <Tabs.Screen key={tabData.href} // Name of the dynamic route. name={"[user]"} options={{ title: tabData.name, // Ensure the tab always links to the same href. href: { pathname: "/[user]", params: { user: tabData.href, }, }, }} /> ))}
I have the same problem but i am using the material-top-tab
layout, which has no href
prop...
All of my tabs are fetch from server such like this:
allCustomerTypes.forEach((value, index) => {
tabList.push(
<TopTab.Screen
name={value.text}
component={CustomerListScreen}
options={{title: value.text}}
initialParams={{ customerTypeList: [value.code] }}
key={index.toString()}
/>,
);
});
It works but the URL gets really weird: http://localhost:8081/xxx?customerTypeList=yyy&screen=zzz¶ms=%5Bobject+Object%5D
I have the same problem but i am using the
material-top-tab
layout, which has nohref
prop... All of my tabs are fetch from server such like this:allCustomerTypes.forEach((value, index) => { tabList.push( <TopTab.Screen name={value.text} component={CustomerListScreen} options={{title: value.text}} initialParams={{ customerTypeList: [value.code] }} key={index.toString()} />, ); });
It works but the URL gets really weird: http://localhost:8081/xxx?customerTypeList=yyy&screen=zzz¶ms=%5Bobject+Object%5D
I am using material-top-bar too. I didn't know about initialParams
, thanks for sharing that! However, while this works we still have to manually create each "{value.text}.jsx/tsx" file, right? This would be fine and we wouldn't even need the "href" prop if we could use a dynamic name="[tab]"
but it doesn't allow tabs with "same name" :/
This repo is in maintenance mode and is only used for critical issues for v2. Its not being actively monitored for issues / support.
Expo Router uses the React Navigation Tabs navigator, which as people have noted only allows tabs with unique names. We are aware of this restriction but it is not high on the roadmap (there are bigger bugs/features).
The work around is to create your own custom navigator or implement a <Tabs />
component using <Slot />
There is an example here: https://github.com/expo/expo/issues/27377#issuecomment-1977613490
@ansmlc Do not need create "{value.text}.jsx/tsx" files. In my case, Just a CustomerListScreen
, it's the common data list screen.
And... BTW, Looks like the expo router's roadmap does not listen to the community👀
@ansmlc Do not need create "{value.text}.jsx/tsx" files. In my case, Just a
CustomerListScreen
, it's the common data list screen. And... BTW, Looks like the expo router's roadmap does not listen to the community👀
@likeSo
Not sure if this helps you but I'm using the example custom "tabBar" from Material Top Tabs docs. This way you can navigate without needed to use the "href" prop at all.
For me, I can't use the component prop like component={CustomerListScreen}
. It just says the component prop doesn't exist. Probably because of the way I set up the Top Tab Navigator to integrate with Expo Router and Typescript (code snippet). If I don't have a "file.tsx/jsx" for each tab than the Tabs don't work at all. I am confused as to why you're using Expo Router if you don't use file-based routing.
import { withLayoutContext } from 'expo-router';
const { Navigator } = createMaterialTopTabNavigator();
export const MaterialTopTabs = withLayoutContext<
MaterialTopTabNavigationOptions,
typeof Navigator,
TabNavigationState<ParamListBase>,
MaterialTopTabNavigationEventMap
>(Navigator);
And yeah, expo-router seems to bring more problems than it solves. It's ridiculous they consider this an "edge case".
And yeah, expo-router seems to bring more problems than it solves. It's ridiculous they consider this an "edge case".
imo this issue defeats the whole purpose of having slug pages.
I created a patch for router v3.4.9. Clone the repo and replace contents of node_modules/expo-router. https://github.com/kennethstarkrl/expo-router-3.4.9-ds-patch
This repo is in maintenance mode and is only used for critical issues for v2. Its not being actively monitored for issues / support.
Expo Router uses the React Navigation Tabs navigator, which as people have noted only allows tabs with unique names. We are aware of this restriction but it is not high on the roadmap (there are bigger bugs/features).
The work around is to create your own custom navigator or implement a
<Tabs />
component using<Slot />
There is an example here: expo/expo#27377 (comment)
This example wasn't great, but the one below actually is very close to a standard tab bar navigator. I massaged it a bit and it is behaving like a standard tab navigator but allows reusing routes for each tab.
https://github.com/EvanBacon/expo-router-layouts-example/blob/main/app/slot/_layout.tsx
import { Link, Navigator, Slot } from "expo-router";
import { View, Text, StyleSheet, Pressable, ViewStyle } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
export const unstable_settings = {
initialRouteName: "index",
};
export default function Layout() {
return (
// You can wrap the navigator with any custom views.
<View style={{ flex: 1 }}>
{/* The custom navigator context must wrap the CustomTabBar so it has access to the Slot state. */}
<Navigator>
{/* A custom UI for our navigator. */}
<CustomTabBar />
{/* The selected contents render here. */}
<Slot />
</Navigator>
</View>
);
}
function CustomTabBar() {
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: "#191A20",
paddingVertical: 24,
borderBottomColor: "#D8D8D8",
borderBottomWidth: 1,
}}
>
<Link href="/slot" style={[styles.link]}>
My Website
</Link>
<View
style={{
flexDirection: "row",
}}
>
{/* Purposefully kept verbose, this can be automated with a for loop. */}
<TabLink
// `name` is used to determine if the tab is selected.
name="index"
// `href` is used to determine the route to navigate to.
href="/slot"
>
{({ focused }) => (
<Text style={[styles.link, { opacity: focused ? 1 : 0.5 }]}>
First
</Text>
)}
</TabLink>
<TabLink name="second" href="/slot/second">
{({ focused }) => (
<Text style={[styles.link, { opacity: focused ? 1 : 0.5 }]}>
Second
</Text>
)}
</TabLink>
</View>
</View>
);
}
// Utilities...
function useIsTabSelected(name: string): boolean {
const { state } = Navigator.useContext();
const current = state.routes.find((route, i) => state.index === i);
return current.name === name;
}
function TabLink({
children,
name,
href,
style,
}: {
children?: any;
name: string;
href: string;
style?: ViewStyle;
}) {
const focused = useIsTabSelected(name);
return (
<Link href={href} asChild style={style}>
<Pressable>{(props) => children({ ...props, focused })}</Pressable>
</Link>
);
}
const styles = StyleSheet.create({
link: {
fontSize: 24,
color: "#E7E9E6",
fontWeight: "bold",
paddingHorizontal: 24,
},
});
Is anyone found a solution for this?
Which package manager are you using? (Yarn is recommended)
npm
Summary
What I am trying to do is to use a slug and render different tabs depending on a users specific configuration. It seems in React Navigation, the name property in Tabs.Screen must be unique, and so it doesn't play nice with slugs and throws the following error: Screen names must be unique: [tab],[tab],[tab]
The goal is to use one slug file for multiple tabs.
Minimal reproducible example
app ->(home) -->tabs --->_layout.tsx --->[tab].tsx
_layout.tsx