Skip to content

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 committed Oct 11, 2022
1 parent 2ca852c commit 9c960a3
Show file tree
Hide file tree
Showing 11 changed files with 881 additions and 808 deletions.
3 changes: 2 additions & 1 deletion packages/react-native-tab-view/example/package.json
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 packages/react-native-tab-view/example/yarn.lock
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 packages/react-native-tab-view/package.json
Expand Up @@ -97,5 +97,8 @@
}
]
]
},
"dependencies": {
"use-latest-callback": "^0.1.5"
}
}
7 changes: 6 additions & 1 deletion packages/react-native-tab-view/src/PagerViewAdapter.tsx
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 packages/react-native-tab-view/src/PanResponderAdapter.tsx
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 packages/react-native-tab-view/src/SceneMap.tsx
@@ -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 packages/react-native-tab-view/src/SceneView.tsx
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

0 comments on commit 9c960a3

Please sign in to comment.