UNPKG

3.84 kBTypeScriptView Raw
1import * as React from 'react';
2import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
3import type {
4 SceneRendererProps,
5 EventEmitterProps,
6 NavigationState,
7 Route,
8} from './types';
9
10type Props<T extends Route> = SceneRendererProps &
11 EventEmitterProps & {
12 navigationState: NavigationState<T>;
13 lazy: boolean;
14 lazyPreloadDistance: number;
15 index: number;
16 children: (props: { loading: boolean }) => React.ReactNode;
17 style?: StyleProp<ViewStyle>;
18 };
19
20type State = {
21 loading: boolean;
22};
23
24export default class SceneView<T extends Route> extends React.Component<
25 Props<T>,
26 State
27> {
28 static getDerivedStateFromProps(props: Props<Route>, state: State) {
29 if (
30 state.loading &&
31 Math.abs(props.navigationState.index - props.index) <=
32 props.lazyPreloadDistance
33 ) {
34 // Always render the route when it becomes focused
35 return { loading: false };
36 }
37
38 return null;
39 }
40
41 state = {
42 loading:
43 Math.abs(this.props.navigationState.index - this.props.index) >
44 this.props.lazyPreloadDistance,
45 };
46
47 componentDidMount() {
48 if (this.props.lazy) {
49 // If lazy mode is enabled, listen to when we enter screens
50 this.unsubscribe = this.props.addEnterListener(this.handleEnter);
51 } else if (this.state.loading) {
52 // If lazy mode is not enabled, render the scene with a delay if not loaded already
53 // This improves the initial startup time as the scene is no longer blocking
54 this.timerHandler = setTimeout(
55 () => this.setState({ loading: false }),
56 0
57 );
58 }
59 }
60
61 componentDidUpdate(prevProps: Props<T>, prevState: State) {
62 if (
63 this.props.lazy !== prevProps.lazy ||
64 this.state.loading !== prevState.loading
65 ) {
66 // We only need the listener if the tab hasn't loaded yet and lazy is enabled
67 if (this.props.lazy && this.state.loading) {
68 this.unsubscribe?.();
69 this.unsubscribe = this.props.addEnterListener(this.handleEnter);
70 } else {
71 this.unsubscribe?.();
72 }
73 }
74 }
75
76 componentWillUnmount() {
77 this.unsubscribe?.();
78
79 if (this.timerHandler) {
80 clearTimeout(this.timerHandler);
81 this.timerHandler = undefined;
82 }
83 }
84
85 private timerHandler: NodeJS.Timeout | undefined;
86
87 private unsubscribe: (() => void) | null = null;
88
89 private handleEnter = (value: number) => {
90 const { index } = this.props;
91
92 // If we're entering the current route, we need to load it
93 if (value === index) {
94 this.setState((prevState) => {
95 if (prevState.loading) {
96 return { loading: false };
97 }
98
99 return null;
100 });
101 }
102 };
103
104 render() {
105 const { navigationState, index, layout, style } = this.props;
106 const { loading } = this.state;
107
108 const focused = navigationState.index === index;
109
110 return (
111 <View
112 accessibilityElementsHidden={!focused}
113 importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
114 style={[
115 styles.route,
116 // If we don't have the layout yet, make the focused screen fill the container
117 // This avoids delay before we are able to render pages side by side
118 layout.width
119 ? { width: layout.width }
120 : focused
121 ? StyleSheet.absoluteFill
122 : null,
123 style,
124 ]}
125 >
126 {
127 // Only render the route only if it's either focused or layout is available
128 // When layout is not available, we must not render unfocused routes
129 // so that the focused route can fill the screen
130 focused || layout.width ? this.props.children({ loading }) : null
131 }
132 </View>
133 );
134 }
135}
136
137const styles = StyleSheet.create({
138 route: {
139 flex: 1,
140 overflow: 'hidden',
141 },
142});