1 | # React Native Tab View
|
2 |
|
3 | [![Build Status][build-badge]][build]
|
4 | [![Version][version-badge]][package]
|
5 | [![MIT License][license-badge]][license]
|
6 |
|
7 | A cross-platform Tab View component for React Native.
|
8 |
|
9 | - [Run the example app to see it in action](https://expo.io/@satya164/react-native-tab-view-demos).
|
10 | - Checkout the [example/](https://github.com/react-native-community/react-native-tab-view/tree/master/example) folder for source code.
|
11 |
|
12 | ## Features
|
13 |
|
14 | - Smooth animations and gestures
|
15 | - Scrollable tabs
|
16 | - Supports both top and bottom tab bars
|
17 | - Follows Material Design spec
|
18 | - Highly customizable
|
19 | - Fully typed with [Flow](https://flow.org/)
|
20 |
|
21 | ## Demo
|
22 |
|
23 | <a href="https://raw.githubusercontent.com/satya164/react-native-tab-view/master/demo/demo.mp4"><img src="https://raw.githubusercontent.com/satya164/react-native-tab-view/master/demo/demo.gif" width="360"></a>
|
24 |
|
25 | ## Installation
|
26 |
|
27 | Open a Terminal in the project root and run:
|
28 |
|
29 | ```sh
|
30 | yarn add react-native-tab-view
|
31 | ```
|
32 |
|
33 | If you are using Expo, you are done. Otherwise, continue to the next step.
|
34 |
|
35 | Install and link [`react-native-gesture-handler`](https://github.com/kmagiera/react-native-gesture-handler) and [`react-native-reanimated`](https://github.com/kmagiera/react-native-reanimated). To install and link them, run:
|
36 |
|
37 | ```sh
|
38 | yarn add react-native-reanimated react-native-gesture-handler
|
39 | react-native link react-native-reanimated
|
40 | react-native link react-native-gesture-handler
|
41 | ```
|
42 |
|
43 | **IMPORTANT:** There are additional steps required for `react-native-gesture-handler` on Android after running `react-native link react-native-gesture-handler`. Check the [this guide](https://kmagiera.github.io/react-native-gesture-handler/docs/getting-started.html) to complete the installation.
|
44 |
|
45 | ## Quick Start
|
46 |
|
47 | ```js
|
48 | import * as React from 'react';
|
49 | import { View, StyleSheet, Dimensions } from 'react-native';
|
50 | import { TabView, SceneMap } from 'react-native-tab-view';
|
51 |
|
52 | const FirstRoute = () => (
|
53 | <View style={[styles.scene, { backgroundColor: '#ff4081' }]} />
|
54 | );
|
55 | const SecondRoute = () => (
|
56 | <View style={[styles.scene, { backgroundColor: '#673ab7' }]} />
|
57 | );
|
58 |
|
59 | export default class TabViewExample extends React.Component {
|
60 | state = {
|
61 | index: 0,
|
62 | routes: [
|
63 | { key: 'first', title: 'First' },
|
64 | { key: 'second', title: 'Second' },
|
65 | ],
|
66 | };
|
67 |
|
68 | render() {
|
69 | return (
|
70 | <TabView
|
71 | navigationState={this.state}
|
72 | renderScene={SceneMap({
|
73 | first: FirstRoute,
|
74 | second: SecondRoute,
|
75 | })}
|
76 | onIndexChange={index => this.setState({ index })}
|
77 | initialLayout={{ width: Dimensions.get('window').width }}
|
78 | />
|
79 | );
|
80 | }
|
81 | }
|
82 |
|
83 | const styles = StyleSheet.create({
|
84 | scene: {
|
85 | flex: 1,
|
86 | },
|
87 | });
|
88 | ```
|
89 |
|
90 | [Try this example on Snack](https://snack.expo.io/@satya164/react-native-tab-view-quick-start)
|
91 |
|
92 | ## More examples on Snack
|
93 |
|
94 | - [Custom Tab Bar](https://snack.expo.io/@satya164/react-native-tab-view-custom-tabbar)
|
95 | - [Lazy Load](https://snack.expo.io/@satya164/react-native-tab-view-lazy-load)
|
96 |
|
97 | ## Integration with React Navigation
|
98 |
|
99 | React Navigation integration can be achieved by the [react-navigation-tabs](https://github.com/react-navigation/react-navigation-tabs) package. Note that while it's easier to use, it is not as flexible as using the library directly.
|
100 |
|
101 | ## API reference
|
102 |
|
103 | The package exports a `TabView` component which is the one you'd use to render the tab view, and a `TabBar` component which is the default tab bar implementation.
|
104 |
|
105 | ### `TabView`
|
106 |
|
107 | Container component responsible for rendering and managing tabs. Follows material design styles by default.
|
108 |
|
109 | Basic usage look like this:
|
110 |
|
111 | ```js
|
112 | <TabView
|
113 | navigationState={this.state}
|
114 | onIndexChange={index => this.setState({ index })}
|
115 | renderScene={SceneMap({
|
116 | first: FirstRoute,
|
117 | second: SecondRoute,
|
118 | })}
|
119 | />
|
120 | ```
|
121 |
|
122 | #### Props
|
123 |
|
124 | ##### `navigationState` (`required`)
|
125 |
|
126 | State for the tab view. The state should contain the following properties:
|
127 |
|
128 | - `index`: a number representing the index of the active route in the `routes` array
|
129 | - `routes`: an array containing a list of route objects used for rendering the tabs
|
130 |
|
131 | Each route object should contain the following properties:
|
132 |
|
133 | - `key`: a unique key to identify the route (required)
|
134 | - `title`: title for the route to display in the tab bar
|
135 | - `icon`: icon for the route to display in the tab bar
|
136 | - `accessibilityLabel`: accessibility label for the tab button
|
137 | - `testID`: test id for the tab button
|
138 |
|
139 | Example:
|
140 |
|
141 | ```js
|
142 | {
|
143 | index: 1,
|
144 | routes: [
|
145 | { key: 'music', title: 'Music' },
|
146 | { key: 'albums', title: 'Albums' },
|
147 | { key: 'recents', title: 'Recents' },
|
148 | { key: 'purchased', title: 'Purchased' },
|
149 | ]
|
150 | }
|
151 | ```
|
152 |
|
153 | `TabView` is a controlled component, which means the `index` needs to be updated via the `onIndexChange` callback.
|
154 |
|
155 | ##### `onIndexChange` (`required`)
|
156 |
|
157 | Callback which is called on tab change, receives the index of the new tab as argument.
|
158 | The navigation state needs to be updated when it's called, otherwise the change is dropped.
|
159 |
|
160 | ##### `renderScene` (`required`)
|
161 |
|
162 | Callback which returns a react element to render as the page for the tab. Receives an object containing the route as the argument:
|
163 |
|
164 | ```js
|
165 | renderScene = ({ route, jumpTo }) => {
|
166 | switch (route.key) {
|
167 | case 'music':
|
168 | return <MusicRoute jumpTo={jumpTo} />;
|
169 | case 'albums':
|
170 | return <AlbumsRoute jumpTo={jumpTo} />;
|
171 | }
|
172 | };
|
173 | ```
|
174 |
|
175 | You need to make sure that your individual routes implement a `shouldComponentUpdate` to improve the performance. To make it easier to specify the components, you can use the `SceneMap` helper.
|
176 |
|
177 | `SceneMap` takes an object with the mapping of `route.key` to React components and returns a function to use with `renderScene` prop.
|
178 |
|
179 | ```js
|
180 | import { SceneMap } from 'react-native-tab-view';
|
181 |
|
182 | ...
|
183 |
|
184 | renderScene = SceneMap({
|
185 | music: MusicRoute,
|
186 | albums: AlbumsRoute,
|
187 | });
|
188 | ```
|
189 |
|
190 | Specifying the components this way is easier and takes care of implementing a `shouldComponentUpdate` method.
|
191 |
|
192 | Each scene receives the following props:
|
193 |
|
194 | - `route`: the current route rendered by the component
|
195 | - `jumpTo`: method to jump to other tabs, takes a `route.key` as it's argument
|
196 |
|
197 | The `jumpTo` method can be used to navigate to other tabs programmatically:
|
198 |
|
199 | ```js
|
200 | this.props.jumpTo('albums');
|
201 | ```
|
202 |
|
203 | All the scenes rendered with `SceneMap` are optimized using `React.PureComponent` and don't re-render when parent's props or states change. If you need more control over how your scenes update (e.g. - triggering a re-render even if the `navigationState` didn't change), use `renderScene` directly instead of using `SceneMap`.
|
204 |
|
205 | **IMPORTANT:** **Do not** pass inline functions to `SceneMap`, for example, don't do the following:
|
206 |
|
207 | ```js
|
208 | SceneMap({
|
209 | first: () => <FirstRoute foo={this.props.foo} />,
|
210 | second: SecondRoute,
|
211 | });
|
212 | ```
|
213 |
|
214 | Always define your components elsewhere in the top level of the file. If you pass inline functions, it'll re-create the component every render, which will cause the entire route to unmount and remount every change. It's very bad for performance and will also cause any local state to be lost.
|
215 |
|
216 | ##### `renderTabBar`
|
217 |
|
218 | Callback which returns a custom React Element to use as the tab bar:
|
219 |
|
220 | ```js
|
221 | import { TabBar } from 'react-native-tab-view';
|
222 |
|
223 | ...
|
224 |
|
225 | renderTabBar = props => <TabBar {...props} />;
|
226 | ```
|
227 |
|
228 | If this is not specified, the default tab bar is rendered. You pass this props to customize the default tab bar, provide your own tab bar, or disable the tab bar completely.
|
229 |
|
230 | ```js
|
231 | renderTabBar = () => null;
|
232 | ```
|
233 |
|
234 | ##### `tabBarPosition`
|
235 |
|
236 | Position of the tab bar in the tab view. Possible values are `'top'` and `'bottom'`. Defaults to `'top'`.
|
237 |
|
238 | ##### `lazy`
|
239 |
|
240 | Boolean indicating whether to lazily render the scenes. By default all scenes are rendered to provide a smoother swipe experience. But you might want to defer the rendering of unfocused scenes until the user sees them. To enable lazy rendering, set `lazy` to `true`.
|
241 |
|
242 | When you enable `lazy`, the unfocused screens will usually take some time to render when they come into focus. You can use the `renderLazyPlaceholder` prop to customize what the user sees during this short period.
|
243 |
|
244 | ##### `renderLazyPlaceholder`
|
245 |
|
246 | Callback which returns a custom React Element to render for routes that haven't been rendered yet. Receives an object containing the route as the argument. The `lazy` prop also needs to be enabled.
|
247 |
|
248 | This view is usually only shown for a split second. Keep it lightweight.
|
249 |
|
250 | By default, this renders `null`.
|
251 |
|
252 | ##### `keyboardDismissMode`
|
253 |
|
254 | String indicating whether the keyboard gets dismissed in response to a drag gesture. Possible values are:
|
255 |
|
256 | - `'on-drag'` (default): the keyboard is dismissed when a drag begins.
|
257 | - `'none'`: drags do not dismiss the keyboard.
|
258 |
|
259 | ##### `swipeEnabled`
|
260 |
|
261 | Boolean indicating whether to enable swipe gestures. Swipe gestures are enabled by default. Passing `false` will disable swipe gestures, but the user can still switch tabs by pressing the tab bar.
|
262 |
|
263 | ##### `swipeDistanceThreshold`
|
264 |
|
265 | Minimum swipe distance which triggers a tab switch. By default, this is automatically determined based on the screen width.
|
266 |
|
267 | ##### `swipeVelocityThreshold`
|
268 |
|
269 | Minimum swipe velocity which triggers a tab switch. Defaults to `1200`.
|
270 |
|
271 | ##### `onSwipeStart`
|
272 |
|
273 | Callback which is called when the swipe gesture starts, i.e. the user touches the screen and moves it.
|
274 |
|
275 | ##### `onSwipeEnd`
|
276 |
|
277 | Callback which is called when the swipe gesture ends, i.e. the user lifts their finger from the screen after the swipe gesture.
|
278 |
|
279 | ##### `initialLayout`
|
280 |
|
281 | Object containing the initial height and width of the screens. Passing this will improve the initial rendering performance. For most apps, this is a good default:
|
282 |
|
283 | ```js
|
284 | { width: Dimensions.get('window').width }}
|
285 | ```
|
286 |
|
287 | ##### `sceneContainerStyle`
|
288 |
|
289 | Style to apply to the view wrapping each screen. You can pass this to override some default styles such as overflow clipping:
|
290 |
|
291 | ##### `style`
|
292 |
|
293 | Style to apply to the tab view container.
|
294 |
|
295 | ### `TabBar`
|
296 |
|
297 | Material design themed tab bar. To customize the tab bar, you'd need to use the `renderTabBar` prop of `TabView` to render the `TabBar` and pass additional props.
|
298 |
|
299 | For example, to customize the indicator color and the tab bar background color, you can pass `indicatorStyle` and `style` props to the `TabBar` respectively:
|
300 |
|
301 | ```js
|
302 | renderTabBar={props =>
|
303 | <TabBar
|
304 | {...props}
|
305 | indicatorStyle={{ backgroundColor: 'white' }}
|
306 | style={{ backgroundColor: 'pink' }}
|
307 | />
|
308 | }
|
309 | ```
|
310 |
|
311 | #### Props
|
312 |
|
313 | ##### `getLabelText`
|
314 |
|
315 | Function which takes an object with the current route and returns the label text for the tab. Uses `route.title` by default.
|
316 |
|
317 | ```js
|
318 | getLabelText={({ route }) => route.title}
|
319 | ```
|
320 |
|
321 | ##### `getAccessible`
|
322 |
|
323 | Function which takes an object with the current route and returns a boolean to indicate whether to mark a tab as `accessible`. Defaults to `true`.
|
324 |
|
325 | ##### `getAccessibilityLabel`
|
326 |
|
327 | Function which takes an object with the current route and returns a accessibility label for the tab button. Uses `route.accessibilityLabel` by default if specified, otherwise uses the route title.
|
328 |
|
329 | ```js
|
330 | getAccessibilityLabel={({ route }) => route.accessibilityLabel}
|
331 | ```
|
332 |
|
333 | ##### `testID`
|
334 |
|
335 | Function which takes an object with the current route and returns a test id for the tab button to locate this tab button in tests. Uses `route.testID` by default.
|
336 |
|
337 | ```js
|
338 | getTestID={({ route }) => route.testID}
|
339 | ```
|
340 |
|
341 | getAccessibilityLabel: (props: { route: T }) => string;
|
342 | Get accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
343 | Uses `route.accessibilityLabel` by default if specified, otherwise uses the route title.
|
344 |
|
345 | getTestID: (props: { route: T }) => string | undefined;
|
346 | Get the id to locate this tab button in tests, uses `route.testID` by default.
|
347 |
|
348 | ##### `renderIcon`
|
349 |
|
350 | Function which takes an object with the current route, focused status and color and returns a custom React Element to be used as a icon.
|
351 |
|
352 | ```js
|
353 | renderIcon={({ route, focused, color }) => (
|
354 | <Icon
|
355 | name={focused ? 'abums' : 'albums-outlined'}
|
356 | color={color}
|
357 | />
|
358 | )}
|
359 | ```
|
360 |
|
361 | ##### `renderLabel`
|
362 |
|
363 | Function which takes an object with the current route, focused status and color and returns a custom React Element to be used as a label.
|
364 |
|
365 | ```js
|
366 | renderLabel={({ route, focused, color }) => (
|
367 | <Text style={{ color, margin: 8 }}>
|
368 | {route.title}
|
369 | </Text>
|
370 | )}
|
371 | ```
|
372 |
|
373 | ##### `renderIndicator`
|
374 |
|
375 | Function which takes an object with the current route and returns a custom React Element to be used as a tab indicator.
|
376 |
|
377 | ##### `renderBadge`
|
378 |
|
379 | Function which takes an object with the current route and returns a custom React Element to be used as a badge.
|
380 |
|
381 | ##### `onTabPress`
|
382 |
|
383 | Function to execute on tab press. It receives the scene for the pressed tab, useful for things like scroll to top.
|
384 |
|
385 | ##### `onTabLongPress`
|
386 |
|
387 | Function to execute on tab long press, use for things like showing a menu with more options
|
388 |
|
389 | ##### `activeColor`
|
390 |
|
391 | Custom color for icon and label in the active tab.
|
392 |
|
393 | ##### `inactiveColor`
|
394 |
|
395 | Custom color for icon and label in the inactive tab.
|
396 |
|
397 | ##### `pressColor`
|
398 |
|
399 | Color for material ripple (Android >= 5.0 only).
|
400 |
|
401 | ##### `pressOpacity`
|
402 |
|
403 | Opacity for pressed tab (iOS and Android < 5.0 only).
|
404 |
|
405 | ##### `scrollEnabled`
|
406 |
|
407 | Boolean indicating whether to enable scrollable tabs.
|
408 |
|
409 | ##### `bounces`
|
410 |
|
411 | Boolean indicating whether the tab bar bounces when scrolling.
|
412 |
|
413 | ##### `tabStyle`
|
414 |
|
415 | Style to apply to the individual tab items in the tab bar.
|
416 |
|
417 | ##### `indicatorStyle`
|
418 |
|
419 | Style to apply to the active indicator.
|
420 |
|
421 | ##### `labelStyle`
|
422 |
|
423 | Style to apply to the tab item label.
|
424 |
|
425 | ##### `contentContainerStyle`
|
426 |
|
427 | Style to apply to the inner container for tabs.
|
428 |
|
429 | ##### `style`
|
430 |
|
431 | Style to apply to the tab bar container.
|
432 |
|
433 | ## Optimization Tips
|
434 |
|
435 | ### Avoid unnecessary re-renders
|
436 |
|
437 | The `renderScene` function is called every time the index changes. If your `renderScene` function is expensive, it's good idea move each route to a separate component if they don't depend on the index, and apply `shouldComponentUpdate` in your route components to prevent unnecessary re-renders.
|
438 |
|
439 | For example, instead of:
|
440 |
|
441 | ```js
|
442 | renderScene = ({ route }) => {
|
443 | switch (route.key) {
|
444 | case 'home':
|
445 | return (
|
446 | <View style={styles.page}>
|
447 | <Avatar />
|
448 | <NewsFeed />
|
449 | </View>
|
450 | );
|
451 | default:
|
452 | return null;
|
453 | }
|
454 | };
|
455 | ```
|
456 |
|
457 | Do the following:
|
458 |
|
459 | ```js
|
460 | renderScene = ({ route }) => {
|
461 | switch (route.key) {
|
462 | case 'home':
|
463 | return <HomeComponent />;
|
464 | default:
|
465 | return null;
|
466 | }
|
467 | };
|
468 | ```
|
469 |
|
470 | Where `<HomeComponent />` is a `PureComponent`:
|
471 |
|
472 | ```js
|
473 | export default class HomeComponent extends React.PureComponent {
|
474 | render() {
|
475 | return (
|
476 | <View style={styles.page}>
|
477 | <Avatar />
|
478 | <NewsFeed />
|
479 | </View>
|
480 | );
|
481 | }
|
482 | }
|
483 | ```
|
484 |
|
485 | ### Avoid one frame delay
|
486 |
|
487 | We need to measure the width of the container and hence need to wait before rendering some elements on the screen. If you know the initial width upfront, you can pass it in and we won't need to wait for measuring it. Most of the time, it's just the window width.
|
488 |
|
489 | For example, pass the following `initialLayout` to `TabView`:
|
490 |
|
491 | ```js
|
492 | const initialLayout = {
|
493 | height: 0,
|
494 | width: Dimensions.get('window').width,
|
495 | };
|
496 | ```
|
497 |
|
498 | The tab view will still react to changes in the dimension and adjust accordingly to accommodate things like orientation change.
|
499 |
|
500 | ### Optimize large number of routes
|
501 |
|
502 | If you've a large number of routes, especially images, it can slow the animation down a lot. You can instead render a limited number of routes.
|
503 |
|
504 | For example, do the following to render only 2 routes on each side:
|
505 |
|
506 | ```js
|
507 | renderScene = ({ route }) => {
|
508 | if (Math.abs(this.state.index - this.state.routes.indexOf(route)) > 2) {
|
509 | return <View />;
|
510 | }
|
511 |
|
512 | return <MySceneComponent route={route} />;
|
513 | };
|
514 | ```
|
515 |
|
516 | ### Avoid rendering TabView inside ScrollView
|
517 |
|
518 | Nesting the `TabView` inside a vertical `ScrollView` will disable the optimizations in the `FlatList` components rendered inside the `TabView`. So avoid doing it if possible.
|
519 |
|
520 | ## Contributing
|
521 |
|
522 | While developing, you can run the [example app](/example/README.md) to test your changes.
|
523 |
|
524 | Make sure your code passes Flow and ESLint. Run the following to verify:
|
525 |
|
526 | ```sh
|
527 | yarn flow
|
528 | yarn lint
|
529 | ```
|
530 |
|
531 | To fix formatting errors, run the following:
|
532 |
|
533 | ```sh
|
534 | yarn lint -- --fix
|
535 | ```
|
536 |
|
537 | Remember to add tests for your change if possible.
|
538 |
|
539 |
|
540 |
|
541 | [build-badge]: https://img.shields.io/circleci/project/github/react-native-community/react-native-tab-view/master.svg?style=flat-square
|
542 | [build]: https://circleci.com/gh/react-native-community/react-native-tab-view
|
543 | [version-badge]: https://img.shields.io/npm/v/react-native-tab-view.svg?style=flat-square
|
544 | [package]: https://www.npmjs.com/package/react-native-tab-view
|
545 | [license-badge]: https://img.shields.io/npm/l/react-native-tab-view.svg?style=flat-square
|
546 | [license]: https://opensource.org/licenses/MIT
|