Skip to content

Commit 56157ff

Browse files
committed
chore(example): overhaul example to allow more comfortable feature testing (#28)
## Description There are many many features to test & keeping them in single file was just too much. Now everything is nicely structured. Additionally I needed "real infrastructure" to see how the API could be used - now `BottomTabsContainer` component serves this purpose. Also see description of software-mansion/react-native-screens-labs@066bafc ## Checklist - [ ] Ensured that CI passes
1 parent 3d85d59 commit 56157ff

File tree

13 files changed

+345
-234
lines changed

13 files changed

+345
-234
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react';
2+
import ConfigWrapperContext from './ConfigWrapperContext';
3+
import { BottomTabs, BottomTabsScreen } from 'react-native-screens';
4+
import { Colors } from '../../shared/styling/Colors';
5+
import type { BottomTabsScreenProps } from 'react-native-screens/components/BottomTabsScreen';
6+
import type { NativeSyntheticEvent } from 'react-native';
7+
import type { NativeFocusChangeEvent } from 'react-native-screens/fabric/BottomTabsNativeComponent';
8+
9+
export interface TabConfiguration {
10+
tabScreenProps: BottomTabsScreenProps;
11+
contentViewRenderFn?: (selectNextTab?: () => void) => React.ReactNode;
12+
}
13+
14+
export interface BottomTabsContainerProps {
15+
tabConfigs: TabConfiguration[];
16+
}
17+
18+
export function BottomTabsContainer(props: BottomTabsContainerProps) {
19+
// Currently assumes controlled bottom tabs
20+
console.info('BottomTabsContainer render');
21+
22+
const totalTabCount = props.tabConfigs.length;
23+
24+
const [focusedTabKey, setFocusedTabKey] = React.useState<string>(() => {
25+
console.log('BottomTabsContainer focusedStateKey initial state computed');
26+
27+
if (props.tabConfigs.length === 0) {
28+
throw new Error('There must be at least one tab defined');
29+
}
30+
31+
const maybeUserRequestedFocusedTab = props.tabConfigs.find(
32+
tabConfig => tabConfig.tabScreenProps.isFocused === true,
33+
)?.tabScreenProps.tabKey;
34+
35+
if (maybeUserRequestedFocusedTab != null) {
36+
return maybeUserRequestedFocusedTab;
37+
}
38+
39+
// Default to first tab
40+
return props.tabConfigs[0].tabScreenProps.tabKey;
41+
});
42+
43+
const selectNextTab = React.useCallback(() => {
44+
setFocusedTabKey(currentTabKey => {
45+
const tabNumberAsString = currentTabKey.slice(3);
46+
const tabNumber = Number.parseInt(tabNumberAsString, 10);
47+
const tabIndex = tabNumber - 1; // tabs are numbered starting from 1
48+
const nextTabIndex = (tabIndex + 1) % totalTabCount;
49+
const nextTabKey = `Tab${nextTabIndex + 1}`;
50+
return nextTabKey;
51+
});
52+
}, [totalTabCount]);
53+
54+
const configWrapper = React.useContext(ConfigWrapperContext);
55+
56+
const onNativeFocusChangeCallback = React.useCallback(
57+
(event: NativeSyntheticEvent<NativeFocusChangeEvent>) => {
58+
const tabKey = event.nativeEvent.tabKey;
59+
60+
// Use `startTransition` only if the state is controlled in JS
61+
// const transitionFn = !configWrapper.config.controlledBottomTabs
62+
// ? startTransition
63+
// : (callback: () => void) => {
64+
// callback();
65+
// };
66+
67+
// Please note that the `useTransition` hook can not be used here,
68+
// because it intruduces additional renders, which lead
69+
// to blank screens / placeholders being visible (on slower render)
70+
// for a few frames!
71+
const transitionFn = React.startTransition;
72+
// const transitionFn = (callback: () => void) => {
73+
// callback();
74+
// };
75+
76+
transitionFn(() => {
77+
console.info(`Starting transition to ${tabKey}`);
78+
setFocusedTabKey(tabKey);
79+
});
80+
},
81+
[],
82+
);
83+
84+
return (
85+
<BottomTabs
86+
onNativeFocusChange={onNativeFocusChangeCallback}
87+
tabBarBackgroundColor={Colors.NavyLight100}
88+
experimentalControlNavigationStateInJS={
89+
configWrapper.config.controlledBottomTabs
90+
}>
91+
{props.tabConfigs.map(tabConfig => {
92+
const tabKey = tabConfig.tabScreenProps.tabKey;
93+
const isFocused = tabConfig.tabScreenProps.tabKey === focusedTabKey;
94+
console.info(
95+
`BottomTabsContainer map to component -> ${tabKey} ${
96+
isFocused ? '(focused)' : ''
97+
}`,
98+
);
99+
return (
100+
<BottomTabsScreen
101+
key={tabKey}
102+
{...tabConfig.tabScreenProps}
103+
isFocused={isFocused} // notice that the value passed by user is overriden here!
104+
>
105+
{tabConfig.contentViewRenderFn?.(selectNextTab)}
106+
</BottomTabsScreen>
107+
);
108+
})}
109+
</BottomTabs>
110+
);
111+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import type { Dispatch, SetStateAction } from 'react';
3+
import { featureFlags } from 'react-native-screens';
4+
5+
export const DEFAULT_GLOBAL_CONFIGURATION = {
6+
heavyTabRender: false,
7+
controlledBottomTabs: featureFlags.experiment.controlledBottomTabs,
8+
} as const;
9+
10+
export interface Configuration {
11+
heavyTabRender: boolean;
12+
controlledBottomTabs: boolean;
13+
}
14+
15+
export interface ConfigWrapper {
16+
config: Configuration;
17+
setConfig?: Dispatch<SetStateAction<Configuration>>;
18+
}
19+
20+
const ConfigWrapperContext = React.createContext<ConfigWrapper>({
21+
config: DEFAULT_GLOBAL_CONFIGURATION,
22+
});
23+
24+
export default ConfigWrapperContext;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import { type ViewProps, View } from 'react-native';
3+
4+
export interface LayoutViewProps extends ViewProps {}
5+
6+
export function LayoutView(props: LayoutViewProps) {
7+
const { children, style, ...rest } = props;
8+
return (
9+
<View
10+
style={[
11+
{
12+
flex: 1,
13+
width: '100%',
14+
height: '100%',
15+
justifyContent: 'center',
16+
alignItems: 'center',
17+
},
18+
style,
19+
]}
20+
{...rest}>
21+
{children}
22+
</View>
23+
);
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import { Text } from 'react-native';
3+
import ConfigWrapperContext from '../ConfigWrapperContext';
4+
5+
export interface TabConfigurationSummaryProps {
6+
tabKey: string;
7+
}
8+
9+
export function TabConfigurationSummary(props: TabConfigurationSummaryProps) {
10+
const configWrapper = React.useContext(ConfigWrapperContext);
11+
12+
return (
13+
<>
14+
<Text>tabKey: {props.tabKey}</Text>
15+
<Text>
16+
heavyTabRender: {configWrapper.config.heavyTabRender ? 'true' : 'false'}
17+
</Text>
18+
<Text>
19+
controlledBottomTabs:{' '}
20+
{configWrapper.config.controlledBottomTabs ? 'true' : 'false'}
21+
</Text>
22+
</>
23+
);
24+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { type ViewProps, View, Text, Button } from 'react-native';
3+
import ConfigWrapperContext from '../ConfigWrapperContext';
4+
import { someExtensiveComputation } from '../utils';
5+
6+
export interface TabContentViewProps extends ViewProps {
7+
selectNextTab?: (() => void) | undefined;
8+
tabKey: string;
9+
message?: string;
10+
}
11+
12+
export function TabContentView(props: TabContentViewProps) {
13+
const { selectNextTab, tabKey, message, ...viewProps } = props;
14+
15+
const configWrapper = React.useContext(ConfigWrapperContext);
16+
17+
console.log(
18+
`TabContentView render with config: ${JSON.stringify(
19+
configWrapper.config,
20+
)}`,
21+
);
22+
23+
if (configWrapper.config.heavyTabRender) {
24+
someExtensiveComputation();
25+
}
26+
27+
return (
28+
<View {...viewProps}>
29+
{message !== undefined && <Text>{message}</Text>}
30+
<Text>tabKey: {tabKey}</Text>
31+
<Text>
32+
heavyTabRender: {configWrapper.config.heavyTabRender ? 'true' : 'false'}
33+
</Text>
34+
<Text>
35+
controlledBottomTabs:{' '}
36+
{configWrapper.config.controlledBottomTabs ? 'true' : 'false'}
37+
</Text>
38+
<Button title="Next tab" onPress={selectNextTab} />
39+
<Button
40+
title="Toggle heavy render"
41+
onPress={() => {
42+
configWrapper.setConfig?.(prev => {
43+
return {
44+
...prev,
45+
heavyTabRender: !prev.heavyTabRender,
46+
};
47+
});
48+
}}
49+
/>
50+
</View>
51+
);
52+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import { View } from 'react-native';
3+
import { Colors } from '../../../shared/styling/Colors';
4+
5+
export function TabPlaceholder() {
6+
console.info('TabPlaceholder render');
7+
return (
8+
<View
9+
style={[
10+
{
11+
flex: 1,
12+
width: '100%',
13+
height: '100%',
14+
justifyContent: 'center',
15+
alignItems: 'center',
16+
backgroundColor: Colors.Navy,
17+
},
18+
]}
19+
/>
20+
);
21+
}

0 commit comments

Comments
 (0)