Skip to content

Latest commit

 

History

History
650 lines (541 loc) · 19.2 KB

README.md

File metadata and controls

650 lines (541 loc) · 19.2 KB

ExNavigation

exponent support npm version

A route-centric, batteries-included navigation library for Exponent and React Native that works seamlessly on Android and iOS.

A few of our favorite features:

  • Android back button handling (it just works, no need to do anything)
  • Tab bar navigation
  • Drawer navigation
  • Sliding tab navigation
  • Optional blurred translucent backgrounds in navigation and tab bar on iOS
  • Alert bars
  • Declarative configuration co-located with your routes
  • Typed with Flow

Installation

  • npm i @exponent/ex-navigation babel-preset-react-native-stage-0 --save
  • Change your .babelrc (if you have one, if not, then create one):
{
  "presets": ["react-native-stage-0/decorator-support"]
}

Note: Comprehensive documentation is coming soon! For now, check out the example project in example/. This lib is very much a work in progress.

How to run the example project

or use this link in your mobile phone: https://getexponent.com/@community/ex-navigation-example

How is this different from what is built into React Native?

NavigationExperimental ships with React Native, it is powerful and flexible, and that comes at the cost of exposing some internals to the app developer. ExNavigation is built on top of NavigationExperimental with the aim of providing a more feature-rich out of the box experience.

A minimal navigation set up

To give you an idea of what the required pieces of are, the following includes only the minimal code necessary to get ExNavigation working.

import React from 'react';
import {
  AppRegistry,
  Text,
  View,
} from 'react-native';

/**
 * If you're using Exponent, uncomment the line below to import Exponent
 * BEFORE importing `@exponent/ex-navigation`. This sets the status bar
 * offsets properly.
 */
// import Exponent from 'exponent';

import {
  createRouter,
  NavigationProvider,
  StackNavigation,
} from '@exponent/ex-navigation';

/**
  * This is where we map route names to route components. Any React
  * component can be a route, it only needs to have a static `route`
  * property defined on it, as in HomeScreen below
  */
const Router = createRouter(() => ({
  home: () => HomeScreen,
}));

class App extends React.Component {
  render() {
    /**
      * NavigationProvider is only needed at the top level of the app,
      * similar to react-redux's Provider component. It passes down
      * navigation objects and functions through context to children.
      *
      * StackNavigation represents a single stack of screens, you can
      * think of a stack like a stack of playing cards, and each time
      * you add a screen it slides in on top. Stacks can contain
      * other stacks, for example if you have a tab bar, each of the
      * tabs has its own individual stack. This is where the playing
      * card analogy falls apart, but it's still useful when thinking
      * of individual stacks.
      */
    return (
      <NavigationProvider router={Router}>
        <StackNavigation initialRoute={Router.getRoute('home')} />
      </NavigationProvider>
    );
  }
}

class HomeScreen extends React.Component {
  /**
    * This is where we can define any route configuration for this
    * screen. For example, in addition to the navigationBar title we
    * could add backgroundColor.
    */
  static route = {
    navigationBar: {
      title: 'Home',
    }
  }

  render() {
    return (
      <View style={{alignItems: 'center', justifyContent: 'center', flex: 1}}>
        <Text>HomeScreen!</Text>
      </View>
    )
  }
}


AppRegistry.registerComponent('main', () => App);

Push and popping routes

 const Router = createRouter(() => ({
   home: () => HomeScreen,
+  about: () => AboutScreen,
 }));

 class HomeScreen extends React.Component {
    render() {
     return (
       <View style={{alignItems: 'center', justifyContent: 'center', flex: 1}}>
         <Text>HomeScreen!</Text>
+        <Text onPress={this._goToAbout}>
+          Push about route
+        </Text>
       </View>
     )
   }
+
+  _goToAbout = () => {
+    this.props.navigator.push(Router.getRoute('about'));
+  }
 }

+ class AboutScreen extends React.Component {
+  static route = {
+    navigationBar: {
+      title: 'About',
+    }
+  }
+
+  render() {
+    return (
+      <View style={{alignItems: 'center', justifyContent: 'center', flex: 1}}>
+        <Text>AboutScreen!</Text>
+        <Text onPress={this._goBackHome}>
+          Go back home
+        </Text>
+      </View>
+    )
+  }
+
+  _goBackHome = () => {
+    this.props.navigator.pop();
+  }
+}

In the above example you will see that we push and pop routes to and from the stack by calling those functions on the navigator prop. This is a prop that is passed into all components that you registered with the router. If you need to access the navigator on a component that is not a route, you can either pass it in manually from your route component use withNavigation as a decorator on the component:

import React from 'react';
import { Text } from 'react-native';
import { withNavigation } from '@exponent/ex-navigation';

@withNavigation
class BackButton extends React.Component {
  render() {
    return <Text onPress={this._goBack}>Go back</Text>
  }

  _goBack = () => {
    if (this.props.navigator.getCurrentIndex() > 0) {
      this.props.navigator.pop();
    }
  }
}

Passing params to a route

  class HomeScreen extends React.Component {

   _goToAbout = () => {
-    this.props.navigator.push(Router.getRoute('about'));
+    this.props.navigator.push(Router.getRoute('about', {name: 'Brent'}));
   }
 }

 class AboutScreen extends React.Component {
   static route = {
     navigationBar: {
-      title: 'About',
+      title(params) {
+        return `Greeting for ${params.name}`;
+      },
     }
   }

   render() {
     return (
       <View style={{alignItems: 'center', justifyContent: 'center', flex: 1}}>
-        <Text>AboutScreen!</Text>
+        <Text>AboutScreen! Hello {this.props.route.params.name}</Text>
         <Text onPress={this._goBackHome}>
           Go back home
         </Text>

Updating route params

Sometimes you don't have all of the data that you need to set the navigation bar title when you mount the route - for example, if you navigate to a user profile screen by user id and need to fetch the profile data before you know what the name is. In this case, one solution is to use the updateCurrentRouteParams function available on StackNavigation navigators.

 class AboutScreen extends React.Component {
   static route = {
     navigationBar: {
       title(params) {
-        return `Greeting for ${params.name}`;
+        if (typeof params.isCool === 'undefined') {
+          return '';
+        }
+
+        return params.isCool ? `Hey cool person!` : `zzz`;
       },
     }
   }

+  componentDidMount() {
+    setTimeout(() => {
+      this.props.navigator.updateCurrentRouteParams({
+        isCool: this.props.route.params.name === 'Brent'
+      })
+    }, 1000);
+  }
+

Make navigation bar buttons update based on route or app state

See the following example for details on how to connect your buttons to the navigator or Redux to perform actions: https://github.com/brentvatne/ex-navigation-conditional-buttons-example

StackNavigation actions

As you saw above, you can push and pop routes. The following is a full list of functions that can be called on StackNavigation navigators.

  • push: add a route to the top of the stack
  • pop: remove the route at the top of the stack
  • replace: replace the current route with a given route
  • showLocalAlert: show an alert bar with given text and styles
  • hideLocalAlert: hide an active alert bar
  • immediatelyResetStack: reset the current stack to the given stack
  • updateCurrentRouteParams: update route params as in the above example

Working with the navigation bar

The navigation bar configuration exposes a set of useful options that should allow you to do most things that you will want to do with it.

You specify the configuration for the navigationBar on the route component, or on a StackNavigation component.

On a route component

When you configure the navigationBar on a route component, the configuration only applies to that specific component. This is usually useful for specifying the title or components to render on the left or right of the title.

 @connect()
 class SignOutButton extends React.Component {
   render() {
      return (
        <TouchableOpacity onPress={this.props.dispatch(Actions.signOut())}>
          <Text>Sign out</Text>
        </TouchableOpacity>
      );
   }
 }

 class AboutScreen extends React.Component {
   static route = {
     navigationBar: {
       title: 'Title goes here',
       renderRight: (route, props) => <SignOutButton />
     }
   }

   // ...
 }

On StackNavigation

You can configure the defaultRouteConfig for all routes within a StackNavigation to save you needing to specify properties like the navigationBar backgroundColor and tintColor (color to use for the title and back button or drawer menu hamburger button).

class App extends React.Component {
  render() {
    return (
      <NavigationProvider router={Router}>
        <StackNavigation
          defaultRouteConfig={{
            navigationBar: {
              backgroundColor: '#000',
              tintColor: '#fff',
            }
          }}
          initialRoute={Router.getRoute('home')}
        />
      </NavigationProvider>
    );
  }
}

navigationBar properties

  • title - a string or a function that returns a string. The function is provided with the route params as the first argument.
  • titleStyle - Text.propTypes.style object to use for the title.
  • backgroundColor - the background color to use for the navigationBar.
  • tintColor - the color to use for the title text and back button or drawer button icons.
  • visible - boolean that indicates whether the navigationBar should be visible for this route.
  • translucent - iOS and Exponent only, use background blur on the navigationBar, like in the Apple Podcasts app, for example.
  • renderLeft - a function that should return a React component that will be rendered in the left position of the navigationBar.
  • renderTitle - a function that should return a React component that will be rendered in the title position of the navigationBar.
  • renderRight - a function that should return a React component that will be rendered in the right position of the navigationBar.
  • renderBackground - a function that should return a React component that will be rendered in the background of the navigationBar.

TabNavigation

A minimal example using tabs:

import {
  StackNavigation,
  TabNavigation,
  TabNavigationItem as TabItem,
} from '@exponent/ex-navigation';


// Treat the TabScreen route like any other route -- you may want to set
// it as the intiial route for a top-level StackNavigation
class TabScreen extends React.Component {
  static route = {
    navigationBar: {
      visible: false,
    }
  }

  render() {
    return (
      <TabNavigation
        id="main"
        navigatorUID="main"
        initialTab="home">
        <TabItem
          id="home"
          title="Home"
          selectedStyle={styles.selectedTab}
          renderIcon={(isSelected) => <Image source={require('./assets/images/home.png')} /> }>
          <StackNavigation
            id="home"
            navigatorUID="home"
            initialRoute={Router.getRoute('home')}
          />
        </TabItem>

        <TabItem
          id="posts"
          title="Posts"
          selectedStyle={styles.selectedTab}
          renderIcon={(isSelected) => <Image source={require('./assets/images/posts.png')} /> }>
          <StackNavigation
            id="posts"
            initialRoute={Router.getRoute('posts')}
          />
        </TabItem>

        <TabItem
          id="profile"
          title="Profile"
          selectedStyle={styles.selectedTab}
          renderIcon={(isSelected) => <Image source={require('./assets/images/profile.png')} /> }>
          <StackNavigation
            id="profile"
            initialRoute={Router.getRoute('profile')}
          />
        </TabItem>
      </TabNavigation>
    );
  }
}

See an example of TabNavigation in a real app here.

If you'd like to switch tabs programmatically (eg: a notification arrives and you want to jump to a notifications tab, or you tap on a button to open your profile but within another tab) you can use jumpToTab. For the code below to work, we need the navigatorUID prop to be set on TabNavigator, as with the example above.

<TouchableOpacity
  onPress={() => {
    this.props.navigation.performAction(({ tabs, stacks }) => {
      tabs('main').jumpToTab('profile');
      stacks('home').push(route);
    });
  }}
/>

DrawerNavigation

A minimal example using the DrawerNavigation:

import {
  StackNavigation,
  DrawerNavigation,
  DrawerNavigationItem,
} from '@exponent/ex-navigation';

// Treat the DrawerNavigationLayout route like any other route -- you may want to set
// it as the intiial route for a top-level StackNavigation

class DrawerNavigationLayout extends React.Component {
  static route = {
    navigationBar: {
      visible: false,
    }
  };

  render() {
    return (
      <DrawerNavigation
        id='main'
        initialItem='home'
        drawerWidth={300}
        renderHeader={this._renderHeader}
      >
        <DrawerNavigationItem
          id='home'
          selectedStyle={styles.selectedItemStyle}
          renderTitle={isSelected => this._renderTitle('Home', isSelected)}
        >
          <StackNavigation
            id='home'
            initialRoute={Router.getRoute('home')}
          />
        </DrawerNavigationItem>

        <DrawerNavigationItem
          id='about'
          selectedStyle={styles.selectedItemStyle}
          renderTitle={isSelected => this._renderTitle('About', isSelected)}
        >
          <StackNavigation
            id='about'
            initialRoute={Router.getRoute('about')}
          />
        </DrawerNavigationItem>

      </DrawerNavigation>
    );
  }

  _renderHeader = () => {
    return (
      <View style={styles.header}>
      </View>
    );
  };

  _renderTitle(text: string, isSelected: boolean) {
    return (
      <Text style={[styles.titleText, isSelected ? styles.selectedTitleText : {}]}>
        {text}
      </Text>
    );
  };
}

const styles = StyleSheet.create({
  header: {
    height: 20
  },

  selectedItemStyle: {
    backgroundColor: 'blue'
  },

  titleText: {
    fontWeight: 'bold'
  },

  selectedTitleText: {
    color: 'white'
  }
});

Integrate with your existing Redux store

Behind the scenes ExNavigation manages your navigation state using Redux in its own store. If you'd like to store the navigation state on your app's store, you can use the createStoreWithNavigation function when creating the store and then manually provide the NavigationContext, initialized with your app's store.

/* Your store definition, let's say state/Store.js */

import { createNavigationEnabledStore, NavigationReducer } from '@exponent/ex-navigation';
import { combineReducers, createStore } from 'redux';

const createStoreWithNavigation = createNavigationEnabledStore({
  createStore,
  navigationStateKey: 'navigation',
});

const store = createStoreWithNavigation(
  /* combineReducers and your normal create store things here! */
  combineReducers({
    navigation: NavigationReducer,
    // other reducers
  })
);

export default store;
/* Your routes, Router.js */

import { createRouter } from '@exponent/ex-navigation';
import HomeScreen from './HomeScreen';

export const Router = createRouter(() => ({
  home: () => HomeScreen,
}));
/* The top level of your app, often in main.js or index.[ios/android].js */

import {
  NavigationContext,
  NavigationProvider,
  StackNavigation,
} from '@exponent/ex-navigation';

import Store from './state/Store';
import Router from './Router';

const navigationContext = new NavigationContext({
  router: Router,
  store: Store,
})

return (
  <Provider store={Store}>
    <NavigationProvider context={navigationContext}>
      <StackNavigation yourUsualPropsHere />
    </NavigationProvider>
  </Provider>
)

Perform navigation actions from outside of a component

You might be using some Redux middleware like saga, thunk, promise, or effex (we recommend effex because we love async/await). Whatever you're using, you no longer have access to this.props.navigator and the like. What to do? Well as long as you include your navigation state inside of your Redux store, you can dispatch a NavigationAction to it -- after all, this is what this.props.navigator.push etc. do behind the scenes.

In the following example we call getState and dispatch directly on your store -- feel free to change this to whatever the equivalent is for your context (eg: if this was effex, dispatch and getState would be passed in to the goHome function).

import { NavigationActions } from '@exponent/ex-navigation'
import Store from '../state/Store';
import Router from './Router'

export default function goHome() {
  let navigatorUID = Store.getState().navigation.currentNavigatorUID;
  Store.dispatch(NavigationActions.push(navigatorUID, Router.getRoute('home')))
}