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

Commit

Permalink
refactor: change components to function based (#1392)
Browse files Browse the repository at this point in the history
This pull request introduces function based components. The goal is to make this library easier to maintain and easier to contribute to.
  • Loading branch information
okwasniewski authored Oct 11, 2022
1 parent c8ce6b0 commit 79a94b4
Show file tree
Hide file tree
Showing 11 changed files with 881 additions and 808 deletions.
3 changes: 2 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"react-native": "0.69.5",
"react-native-pager-view": "5.4.25",
"react-native-safe-area-context": "4.3.1",
"react-native-web": "~0.18.7"
"react-native-web": "~0.18.7",
"use-latest-callback": "^0.1.5"
},
"devDependencies": {
"babel-plugin-module-resolver": "^4.1.0",
Expand Down
5 changes: 5 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11415,6 +11415,11 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"

use-latest-callback@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==

use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,8 @@
}
]
]
},
"dependencies": {
"use-latest-callback": "^0.1.5"
}
}
7 changes: 6 additions & 1 deletion src/PagerViewAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,13 @@ export default function PagerViewAdapter<T extends Route>({
};
}, []);

const memoizedPosition = React.useMemo(
() => Animated.add(position, offset),
[offset, position]
);

return children({
position: Animated.add(position, offset),
position: memoizedPosition,
addEnterListener,
jumpTo,
render: (children) => (
Expand Down
9 changes: 6 additions & 3 deletions src/PanResponderAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,13 @@ export default function PanResponderAdapter<T extends Route>({
I18nManager.isRTL ? -1 : 1
);

const position = React.useMemo(
() => (layout.width ? Animated.divide(panX, -layout.width) : null),
[layout.width, panX]
);

return children({
position: layout.width
? Animated.divide(panX, -layout.width)
: new Animated.Value(index),
position: position ?? new Animated.Value(index),
addEnterListener,
jumpTo,
render: (children) => (
Expand Down
18 changes: 11 additions & 7 deletions src/SceneMap.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import * as React from 'react';
import type { SceneRendererProps } from './types';

class SceneComponent<
T extends { component: React.ComponentType<any> }
> extends React.PureComponent<T> {
render() {
const { component, ...rest } = this.props;
type SceneProps = {
route: any;
} & Omit<SceneRendererProps, 'layout'>;

const SceneComponent = React.memo(
<T extends { component: React.ComponentType<any> } & SceneProps>({
component,
...rest
}: T) => {
return React.createElement(component, rest);
}
}
);

export default function SceneMap<T extends any>(scenes: {
[key: string]: React.ComponentType<T>;
}) {
return ({ route, jumpTo, position }: SceneRendererProps & { route: any }) => (
return ({ route, jumpTo, position }: SceneProps) => (
<SceneComponent
key={route.key}
component={scenes[route.key]}
Expand Down
176 changes: 70 additions & 106 deletions src/SceneView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,121 +17,85 @@ type Props<T extends Route> = SceneRendererProps &
style?: StyleProp<ViewStyle>;
};

type State = {
loading: boolean;
};

export default class SceneView<T extends Route> extends React.Component<
Props<T>,
State
> {
static getDerivedStateFromProps(props: Props<Route>, state: State) {
if (
state.loading &&
Math.abs(props.navigationState.index - props.index) <=
props.lazyPreloadDistance
) {
// Always render the route when it becomes focused
return { loading: false };
}

return null;
export default function SceneView<T extends Route>({
children,
navigationState,
lazy,
layout,
index,
lazyPreloadDistance,
addEnterListener,
style,
}: Props<T>) {
const [isLoading, setIsLoading] = React.useState(
Math.abs(navigationState.index - index) > lazyPreloadDistance
);

if (
isLoading &&
Math.abs(navigationState.index - index) <= lazyPreloadDistance
) {
// Always render the route when it becomes focused
setIsLoading(false);
}

state = {
loading:
Math.abs(this.props.navigationState.index - this.props.index) >
this.props.lazyPreloadDistance,
};
React.useEffect(() => {
const handleEnter = (value: number) => {
// If we're entering the current route, we need to load it
if (value === index) {
setIsLoading((prevState) => {
if (prevState) {
return false;
}
return prevState;
});
}
};

let unsubscribe: (() => void) | undefined;
let timer: NodeJS.Timeout;

componentDidMount() {
if (this.props.lazy) {
if (lazy && isLoading) {
// If lazy mode is enabled, listen to when we enter screens
this.unsubscribe = this.props.addEnterListener(this.handleEnter);
} else if (this.state.loading) {
unsubscribe = addEnterListener(handleEnter);
} else if (isLoading) {
// If lazy mode is not enabled, render the scene with a delay if not loaded already
// This improves the initial startup time as the scene is no longer blocking
this.timerHandler = setTimeout(
() => this.setState({ loading: false }),
0
);
timer = setTimeout(() => setIsLoading(false), 0);
}
}

componentDidUpdate(prevProps: Props<T>, prevState: State) {
if (
this.props.lazy !== prevProps.lazy ||
this.state.loading !== prevState.loading
) {
// We only need the listener if the tab hasn't loaded yet and lazy is enabled
if (this.props.lazy && this.state.loading) {
this.unsubscribe?.();
this.unsubscribe = this.props.addEnterListener(this.handleEnter);
} else {
this.unsubscribe?.();
return () => {
unsubscribe?.();
clearTimeout(timer);
};
}, [addEnterListener, index, isLoading, lazy]);

const focused = navigationState.index === index;

return (
<View
accessibilityElementsHidden={!focused}
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
style={[
styles.route,
// If we don't have the layout yet, make the focused screen fill the container
// This avoids delay before we are able to render pages side by side
layout.width
? { width: layout.width }
: focused
? StyleSheet.absoluteFill
: null,
style,
]}
>
{
// Only render the route only if it's either focused or layout is available
// When layout is not available, we must not render unfocused routes
// so that the focused route can fill the screen
focused || layout.width ? children({ loading: isLoading }) : null
}
}
}

componentWillUnmount() {
this.unsubscribe?.();

if (this.timerHandler) {
clearTimeout(this.timerHandler);
this.timerHandler = undefined;
}
}

private timerHandler: NodeJS.Timeout | undefined;

private unsubscribe: (() => void) | null = null;

private handleEnter = (value: number) => {
const { index } = this.props;

// If we're entering the current route, we need to load it
if (value === index) {
this.setState((prevState) => {
if (prevState.loading) {
return { loading: false };
}

return null;
});
}
};

render() {
const { navigationState, index, layout, style } = this.props;
const { loading } = this.state;

const focused = navigationState.index === index;

return (
<View
accessibilityElementsHidden={!focused}
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
style={[
styles.route,
// If we don't have the layout yet, make the focused screen fill the container
// This avoids delay before we are able to render pages side by side
layout.width
? { width: layout.width }
: focused
? StyleSheet.absoluteFill
: null,
style,
]}
>
{
// Only render the route only if it's either focused or layout is available
// When layout is not available, we must not render unfocused routes
// so that the focused route can fill the screen
focused || layout.width ? this.props.children({ loading }) : null
}
</View>
);
}
</View>
);
}

const styles = StyleSheet.create({
Expand Down
Loading

0 comments on commit 79a94b4

Please sign in to comment.