Skip to content
This repository was archived by the owner on Nov 27, 2022. It is now read-only.

Commit 79a94b4

Browse files
authored
refactor: change components to function based (#1392)
This pull request introduces function based components. The goal is to make this library easier to maintain and easier to contribute to.
1 parent c8ce6b0 commit 79a94b4

File tree

11 files changed

+881
-808
lines changed

11 files changed

+881
-808
lines changed

example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"react-native": "0.69.5",
2323
"react-native-pager-view": "5.4.25",
2424
"react-native-safe-area-context": "4.3.1",
25-
"react-native-web": "~0.18.7"
25+
"react-native-web": "~0.18.7",
26+
"use-latest-callback": "^0.1.5"
2627
},
2728
"devDependencies": {
2829
"babel-plugin-module-resolver": "^4.1.0",

example/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11415,6 +11415,11 @@ url@^0.11.0:
1141511415
punycode "1.3.2"
1141611416
querystring "0.2.0"
1141711417

11418+
use-latest-callback@^0.1.5:
11419+
version "0.1.5"
11420+
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
11421+
integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==
11422+
1141811423
use-sync-external-store@^1.0.0:
1141911424
version "1.2.0"
1142011425
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,8 @@
9797
}
9898
]
9999
]
100+
},
101+
"dependencies": {
102+
"use-latest-callback": "^0.1.5"
100103
}
101104
}

src/PagerViewAdapter.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,13 @@ export default function PagerViewAdapter<T extends Route>({
129129
};
130130
}, []);
131131

132+
const memoizedPosition = React.useMemo(
133+
() => Animated.add(position, offset),
134+
[offset, position]
135+
);
136+
132137
return children({
133-
position: Animated.add(position, offset),
138+
position: memoizedPosition,
134139
addEnterListener,
135140
jumpTo,
136141
render: (children) => (

src/PanResponderAdapter.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,13 @@ export default function PanResponderAdapter<T extends Route>({
283283
I18nManager.isRTL ? -1 : 1
284284
);
285285

286+
const position = React.useMemo(
287+
() => (layout.width ? Animated.divide(panX, -layout.width) : null),
288+
[layout.width, panX]
289+
);
290+
286291
return children({
287-
position: layout.width
288-
? Animated.divide(panX, -layout.width)
289-
: new Animated.Value(index),
292+
position: position ?? new Animated.Value(index),
290293
addEnterListener,
291294
jumpTo,
292295
render: (children) => (

src/SceneMap.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import * as React from 'react';
22
import type { SceneRendererProps } from './types';
33

4-
class SceneComponent<
5-
T extends { component: React.ComponentType<any> }
6-
> extends React.PureComponent<T> {
7-
render() {
8-
const { component, ...rest } = this.props;
4+
type SceneProps = {
5+
route: any;
6+
} & Omit<SceneRendererProps, 'layout'>;
7+
8+
const SceneComponent = React.memo(
9+
<T extends { component: React.ComponentType<any> } & SceneProps>({
10+
component,
11+
...rest
12+
}: T) => {
913
return React.createElement(component, rest);
1014
}
11-
}
15+
);
1216

1317
export default function SceneMap<T extends any>(scenes: {
1418
[key: string]: React.ComponentType<T>;
1519
}) {
16-
return ({ route, jumpTo, position }: SceneRendererProps & { route: any }) => (
20+
return ({ route, jumpTo, position }: SceneProps) => (
1721
<SceneComponent
1822
key={route.key}
1923
component={scenes[route.key]}

src/SceneView.tsx

Lines changed: 70 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -17,121 +17,85 @@ type Props<T extends Route> = SceneRendererProps &
1717
style?: StyleProp<ViewStyle>;
1818
};
1919

20-
type State = {
21-
loading: boolean;
22-
};
23-
24-
export 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;
20+
export default function SceneView<T extends Route>({
21+
children,
22+
navigationState,
23+
lazy,
24+
layout,
25+
index,
26+
lazyPreloadDistance,
27+
addEnterListener,
28+
style,
29+
}: Props<T>) {
30+
const [isLoading, setIsLoading] = React.useState(
31+
Math.abs(navigationState.index - index) > lazyPreloadDistance
32+
);
33+
34+
if (
35+
isLoading &&
36+
Math.abs(navigationState.index - index) <= lazyPreloadDistance
37+
) {
38+
// Always render the route when it becomes focused
39+
setIsLoading(false);
3940
}
4041

41-
state = {
42-
loading:
43-
Math.abs(this.props.navigationState.index - this.props.index) >
44-
this.props.lazyPreloadDistance,
45-
};
42+
React.useEffect(() => {
43+
const handleEnter = (value: number) => {
44+
// If we're entering the current route, we need to load it
45+
if (value === index) {
46+
setIsLoading((prevState) => {
47+
if (prevState) {
48+
return false;
49+
}
50+
return prevState;
51+
});
52+
}
53+
};
54+
55+
let unsubscribe: (() => void) | undefined;
56+
let timer: NodeJS.Timeout;
4657

47-
componentDidMount() {
48-
if (this.props.lazy) {
58+
if (lazy && isLoading) {
4959
// 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) {
60+
unsubscribe = addEnterListener(handleEnter);
61+
} else if (isLoading) {
5262
// If lazy mode is not enabled, render the scene with a delay if not loaded already
5363
// 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-
);
64+
timer = setTimeout(() => setIsLoading(false), 0);
5865
}
59-
}
6066

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?.();
67+
return () => {
68+
unsubscribe?.();
69+
clearTimeout(timer);
70+
};
71+
}, [addEnterListener, index, isLoading, lazy]);
72+
73+
const focused = navigationState.index === index;
74+
75+
return (
76+
<View
77+
accessibilityElementsHidden={!focused}
78+
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
79+
style={[
80+
styles.route,
81+
// If we don't have the layout yet, make the focused screen fill the container
82+
// This avoids delay before we are able to render pages side by side
83+
layout.width
84+
? { width: layout.width }
85+
: focused
86+
? StyleSheet.absoluteFill
87+
: null,
88+
style,
89+
]}
90+
>
91+
{
92+
// Only render the route only if it's either focused or layout is available
93+
// When layout is not available, we must not render unfocused routes
94+
// so that the focused route can fill the screen
95+
focused || layout.width ? children({ loading: isLoading }) : null
7296
}
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-
}
97+
</View>
98+
);
13599
}
136100

137101
const styles = StyleSheet.create({

0 commit comments

Comments
 (0)