4

I'm making a project (react native, expo, react navigation 5) where I wanted to add a custom 'add' button to the bottom tabs, but since...

A navigator can only contain 'Screen' components as its direct children

...I needed to find a way to pass my custom component.

Seemed easy enough, I mean there are docs:

...but in looking at these and the questions from others I either only found muy complicado examples or examples of how to achieve this in earlier versions.

In the end I found a simple solution that so far works like a charm (fully grateful to any suggestions as to why this might be a terrible idea).

Figured I post my solution if anyone is in a similar pickle. See answer below.

Osa Gambas
  • 91
  • 1
  • 9

4 Answers4

5

Place the component outside the navigator, and position it above the tabs with css. Adjust the icons of the tabs to the left and right as seen in the example.

Like I said above, suggestions on how to achieve this in a different way warmly welcome, but haven't encountered any issues yet (knock on wood).

Here's what it looks like:

enter image description here

And here's the bunny:

import React from 'react';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import { Ionicons, MaterialIcons, FontAwesome } from '@expo/vector-icons';

import AddButton from '../../components/UI/AddButton';
import SpotlightProductsScreen from './SpotlightProductsScreen';
import ProductsScreen from './ProductsScreen';
import UserSpotlightScreen from './../user/UserSpotlightScreen';
import UserProductsScreen from './../user/UserProductsScreen';


const ProductsOverviewScreen = props => {

  const Tab = createMaterialBottomTabNavigator();

  return (
    <>
      <AddButton
        navigation={props.navigation}
        style={{
          position: 'absolute',
          zIndex: 99,
          bottom: 5,
          alignSelf: 'center',
          shadowColor: 'black',
          shadowOpacity: 0.15,
          shadowOffset: { width: 0, height: 2 },
          shadowRadius: 8,
          elevation: 3 //Because shadow only work on iOS, elevation is same thing but for android.
        }}
      />
      <Tab.Navigator
        initialRouteName="Spotlight"
        labeled={false}
        shifting={true}
        activeColor="#f0edf6"
        inactiveColor="#3e2465"
        barStyle={{ backgroundColor: 'rgba(127,63,191,.9)' }}
      >
        <Tab.Screen
          name="Spotlight"
          component={SpotlightProductsScreen}
          options={{
            tabBarIcon: ({ color }) => (
              <Ionicons
                name={
                  Platform.OS === 'android'
                    ? 'md-notifications'
                    : 'ios-notifications'
                }
                color={color}
                size={27}
                style={{
                  marginLeft: -35
                }}
              />
            )
          }}
        />
        <Tab.Screen
          name="Förråd"
          component={ProductsScreen}
          options={{
            tabBarIcon: ({ color }) => (
              <MaterialIcons
                name={'file-download'}
                color={color}
                size={27}
                style={{
                  marginLeft: -70
                }}
              />
            )
          }}
        />
        <Tab.Screen
          name="Mitt Förråd"
          component={UserProductsScreen}
          options={{
            tabBarIcon: ({ color }) => (
              <MaterialIcons
                name={'file-upload'}
                color={color}
                size={30}
                style={{
                  marginRight: -70
                }}
              />
            )
          }}
        />
        <Tab.Screen
          name="Min Sida"
          component={UserSpotlightScreen}
          options={{
            tabBarBadge: 4,
            tabBarIcon: ({ color }) => (
              <FontAwesome
                name={'user'}
                color={color}
                size={30}
                style={{
                  marginRight: -35
                }}
              />
            )
          }}
        />
      </Tab.Navigator>
    </>
  );
};

export default ProductsOverviewScreen;
Osa Gambas
  • 91
  • 1
  • 9
  • it worked. I managed to make my Button appear in bottom Middle of my tabs. But there is an Issue. made my button position absolute with bottom 25. now my half button is non clickable and half is clickable. Outside is non clickable. – Najam Us Saqib Jun 03 '21 at 14:15
  • any solution worked for you or you come up with for the non clickable part? – sanister Jan 07 '22 at 07:00
3

You can try this:

<Tab.Screen 
        name = "Button" 
        component={ScanStack} 
        options={{
        tabBarButton:()=>
        <View style={{position:'relative',bottom:35,alignItems:'center', justifyContent:'space-around',height:85}}>
          <Icon 
            name="barcode-scan"
            type = "material-community" 
            reverse
            color={'yellow'}
            reverseColor='black'
            containerStyle={{padding:0,margin:0,elevation:5}}
            onPress={()=>console.log('Hi')}
            size={30}/>
          <Text>Scan</Text>
        </View>
        }}/>

In the component it is necessary to use a valid react-component, I tried to use component={()=>null} but a warn appeared in the console

Result

0

I had the same problem, I needed to add a custom component, not related with a screen, to the tab navigator and everything I tried it failed. In my case I was trying with createMaterialTopTabNavigator.

Documentation of React Navigation 5 is a little rough and there isn't a lot of examples of React Navigation 5, but after many attempts I could create a custom component and style it myself so I can mix the tabs created by the routes and the custom buttons embedded in the tab navigator.

import * as React from 'react';
import { View } from 'react-native'
import {
  NavigationHelpersContext,
  useNavigationBuilder,
  TabRouter,
  TabActions,
  createNavigatorFactory,
} from '@react-navigation/native';
import styled from 'styled-components'
import Colors from '../constants/Colors';

const customTabNavigator = ({ 
  initialRouteName, 
  children, 
  screenOptions, 
  tabContainerStyle, 
  contentStyle, 
  leftIcon,
  rightIcon 
}) => {
  const { state, navigation, descriptors } = useNavigationBuilder(TabRouter, {
    children,
    screenOptions,
    initialRouteName,
  });

  return (
    <NavigationHelpersContext.Provider value={navigation}>
      <OuterWrapper style={tabContainerStyle}>
        { leftIcon }
        <TabWrapper>
          {state.routes.map((route, i) => {
            return (
              <Tab
                key={route.key}
                onPress={() => {
                  const event = navigation.emit({
                    type: 'tabPress',
                    target: route.key,
                    canPreventDefault: true,
                  });

                  if (!event.defaultPrevented) {
                    navigation.dispatch({
                      ...TabActions.jumpTo(route.name),
                      target: state.key,
                    });
                  }
                }}
                style={descriptors[route.key].options.tabStyle}
              >
                { descriptors[route.key].options.label ?? <Label active={state.index === i}>{descriptors[route.key].options.title || route.name}</Label> }
              </Tab>
            )
          })}
        </TabWrapper>
        { rightIcon }
      </OuterWrapper>
      <View style={[{ flex: 1 }, contentStyle]}>
        {descriptors[state.routes[state.index].key].render()}
      </View>
    </NavigationHelpersContext.Provider>
  );
}

const OuterWrapper = styled.View`
  height: 55px;
  flex-direction: row;
  justify-content: space-between;
  background-color: ${Colors.grey1};
`
const TabWrapper = styled.View`
  flex: 1;
  flex-direction: row;
  justify-content: space-evenly;
`
const Tab = styled.TouchableOpacity`
  padding: 0 24px;
  justify-content: center;
  height: 100%;
`
const Label = styled.Text`
  font-family: Futura-Medium;
  font-size: 26px;
  color: ${({ active }) => active ? Colors.grey6 : Colors.grey3};
`

export default createNavigatorFactory(customTabNavigator)
import customTabNavigator from './customTabNavigator'
import * as React from 'react';
import { View, Image } from 'react-native'

import {
  ProjectsScreen,
  RenderScreen,
  EventsScreen,
  CameraScreen
} from '../screens';

import Colors from '../constants/Colors'

import logo from '../assets/images/icon.png'
import { Ionicons } from '@expo/vector-icons';
import { TouchableOpacity } from 'react-native-gesture-handler';

const TopTab = customTabNavigator();
const INITIAL_ROUTE_NAME = 'Home';

export default function MainNavigator({ navigation, route }) {

  navigation.setOptions({ headerTitle: getHeaderTitle(route) });
  return (
      <TopTab.Navigator
        initialRouteName={INITIAL_ROUTE_NAME}
        leftIcon={(
          <TouchableOpacity style={{ height: "100%", justifyContent: "center" }} onPress={() => alert("Whatever")}>
            <Image source={logo} style={{ resizeMode: "center", width: 70, height: 40 }} />
          </TouchableOpacity>
        )}
      >
        <TopTab.Screen
          name="Home"
          component={ProjectsScreen}
          options={{
            title: 'Proyectos',
          }}
        />
        <TopTab.Screen
          name="Preview"
          component={EventsScreen}
          options={{
            title: 'Eventos',
          }}
        />
        <TopTab.Screen
          name="Render"
          component={RenderScreen}
          options={{
            title: 'Mi cuenta',
          }}
        />
        <TopTab.Screen
          name="Camera"
          component={CameraScreen}
          options={{
            title: "Camera",
            label: (
              <View style={{ width: 36, height: 32, backgroundColor: Colors.grey3, borderRadius: 3, alignItems: "center", justifyContent: "center" }}>
                <Ionicons name="md-camera" style={{ color: Colors.grey5 }} size={25} />
              </View>
            ),
            tabStyle: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", flex: 1  }
          }}
        />
      </TopTab.Navigator>
  );
}

function getHeaderTitle(route) {
  const routeName = route.state?.routes[route.state.index]?.name ?? INITIAL_ROUTE_NAME;

  switch (routeName) {
    case 'Home':
      return 'Montar vídeo';
    case 'Preview':
      return 'Previsualizar vídeo';
    case 'Render':
      return 'Renderizar';
    case 'Gallery':
      return 'Galería'
    case 'Camera':
      return 'Camera'
  }
}

To the example of https://reactnavigation.org/docs/custom-navigators I added a different styling and two new props, leftIcon and rightIcon. This props receive a component for render it to the corresponding side of the tab wrapper. And this components can be TouchableWhatevers with a custom onPress not related with a screen :P

I hope it helps, I almost throw myself through the window until I made it work, haha

Al Hill
  • 479
  • 2
  • 6
0

step-1:- In bottomTabNav.js to component-prop give a screen which returns nothing

import React from 'react'
const AddMoreScreen = () => {
  return  null
}
export default AddMoreScreen
//I created AddMoreScreen.js component 

step-2:- As mentioned in step-1, I am giving a screen that renders nothing to component prop, later in options prop I will tap tabBarButton object & returns a custom button to it

  ...
 <Tab.Screen
    name={"Add"}
    component={AddMoreScreen}
    options={{
      tabBarButton: ()=> <AddMoreModal />
    }}
  />
  ...

step-3:- and finally our center button code, in my case, if I press button a modal has to appear from the bottom, just change the below codes for your requirements. AddMoreModal.js

export default function AddMoreModal() {
const [modalVisible, setModalVisible] = useState(false);

return (
<View style={{ marginTop: 15, marginLeft: 10, marginRight: 10, }}>
  <TouchableOpacity
    onPress={() => {
      setModalVisible(true);
    }}
  >
    <View style={styles.buttonStyle}>
      <Image
        source={icons.quickAddOutlined}
        style={styles.bottomTabImage}
      />
      <Text style={styles.bottomTabText}>Add</Text>
    </View>
  </TouchableOpacity>

  <View style={styles.container}>
    <Modal
      backdropOpacity={0.3}
      isVisible={modalVisible}
      onBackdropPress={() => setModalVisible(false)}
      style={styles.contentView}
    >
      <View style={styles.content}>
        <Text style={styles.contentTitle}>Hi !</Text>
        <Text>Welcome to CRAZY MIDDLE BUTTON! forgotten by react-native navigation</Text>
      </View>
    </Modal>
  </View>
</View>
);
}


const styles = StyleSheet.create({
content: {
backgroundColor: "white",
padding: 22,
justifyContent: "center",
alignItems: "center",
borderTopRightRadius: 17,
borderTopLeftRadius: 17,
},
contentTitle: {
fontSize: 20,
marginBottom: 12,
},
contentView: {
justifyContent: "flex-end",
margin: 0,
},
buttonStyle: {
marginBottom: 0,
alignItems:'center',
justifyContent:'center'
},
bottomTabImage: {
width: 25,
height: 25,
},
bottomTabText: {
marginTop: 4,
fontSize: FONTS.body3.fontSize,
fontFamily: FONTS.body3.fontFamily,
color: COLORS.fontGray90,
},
});

step-4:- enter image description here

abhish
  • 243
  • 4
  • 8