mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-06 22:39:41 +08:00
feat: add Link component as useLinkTo hook for navigating to links
The `Link` component can be used to navigate to URLs. On web, it'll use an `a` tag for proper accessibility. On React Native, it'll use a `Text`.
Example:
```js
<Link to="/feed/hot">Go to 🔥</Link>
```
Sometimes we might want more complex styling and more control over the behaviour, or navigate to a URL programmatically. The `useLinkTo` hook can be used for that.
Example:
```js
function LinkButton({ to, ...rest }) {
const linkTo = useLinkTo();
return (
<Button
{...rest}
href={to}
onPress={(e) => {
e.preventDefault();
linkTo(to);
}}
/>
);
}
```
This commit is contained in:
committed by
Satyajit Sahoo
parent
2697355ab2
commit
2573b5beaa
140
example/src/Screens/LinkComponent.tsx
Normal file
140
example/src/Screens/LinkComponent.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import {
|
||||
Link,
|
||||
RouteProp,
|
||||
ParamListBase,
|
||||
useLinkTo,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
|
||||
type SimpleStackParams = {
|
||||
Article: { author: string };
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const LinkButton = ({
|
||||
to,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof Button> & { to: string }) => {
|
||||
const linkTo = useLinkTo();
|
||||
|
||||
return <Button onPress={() => linkTo(to)} {...rest} />;
|
||||
};
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Link
|
||||
to="/link-component/Album"
|
||||
style={[styles.button, { padding: 8 }]}
|
||||
>
|
||||
Go to /link-component/Album
|
||||
</Link>
|
||||
<LinkButton
|
||||
to="/link-component/Album"
|
||||
mode="contained"
|
||||
style={styles.button}
|
||||
>
|
||||
Go to /link-component/Album
|
||||
</LinkButton>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Link
|
||||
to="/link-component/Article?author=Babel"
|
||||
style={[styles.button, { padding: 8 }]}
|
||||
>
|
||||
Go to /link-component/Article
|
||||
</Link>
|
||||
<LinkButton
|
||||
to="/link-component/Article?author=Babel"
|
||||
mode="contained"
|
||||
style={styles.button}
|
||||
>
|
||||
Go to /link-component/Article
|
||||
</LinkButton>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator {...rest}>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params.author}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
options={{ title: 'Album' }}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
Appbar,
|
||||
List,
|
||||
Divider,
|
||||
Text,
|
||||
} from 'react-native-paper';
|
||||
import {
|
||||
InitialState,
|
||||
useLinking,
|
||||
NavigationContainerRef,
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
@@ -55,6 +55,7 @@ import DynamicTabs from './Screens/DynamicTabs';
|
||||
import AuthFlow from './Screens/AuthFlow';
|
||||
import CompatAPI from './Screens/CompatAPI';
|
||||
import MasterDetail from './Screens/MasterDetail';
|
||||
import LinkComponent from './Screens/LinkComponent';
|
||||
|
||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||
|
||||
@@ -113,6 +114,10 @@ const SCREENS = {
|
||||
title: 'Compat Layer',
|
||||
component: CompatAPI,
|
||||
},
|
||||
LinkComponent: {
|
||||
title: '<Link />',
|
||||
component: LinkComponent,
|
||||
},
|
||||
};
|
||||
|
||||
const Drawer = createDrawerNavigator<RootDrawerParamList>();
|
||||
@@ -126,34 +131,6 @@ Asset.loadAsync(StackAssets);
|
||||
export default function App() {
|
||||
const containerRef = React.useRef<NavigationContainerRef>(null);
|
||||
|
||||
// To test deep linking on, run the following in the Terminal:
|
||||
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
|
||||
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
|
||||
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
||||
const { getInitialState } = useLinking(containerRef, {
|
||||
prefixes: LinkingPrefixes,
|
||||
config: {
|
||||
Root: {
|
||||
path: '',
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
acc[name] = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ Home: '' }
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [theme, setTheme] = React.useState(DefaultTheme);
|
||||
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
@@ -164,12 +141,13 @@ export default function App() {
|
||||
React.useEffect(() => {
|
||||
const restoreState = async () => {
|
||||
try {
|
||||
let state = await getInitialState();
|
||||
let state;
|
||||
|
||||
if (Platform.OS !== 'web' && state === undefined) {
|
||||
const savedState = await AsyncStorage.getItem(
|
||||
NAVIGATION_PERSISTENCE_KEY
|
||||
);
|
||||
|
||||
state = savedState ? JSON.parse(savedState) : undefined;
|
||||
}
|
||||
|
||||
@@ -190,7 +168,7 @@ export default function App() {
|
||||
};
|
||||
|
||||
restoreState();
|
||||
}, [getInitialState]);
|
||||
}, []);
|
||||
|
||||
const paperTheme = React.useMemo(() => {
|
||||
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
|
||||
@@ -239,6 +217,34 @@ export default function App() {
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
linking={{
|
||||
// To test deep linking on, run the following in the Terminal:
|
||||
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
|
||||
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
|
||||
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
|
||||
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
|
||||
prefixes: LinkingPrefixes,
|
||||
config: {
|
||||
Root: {
|
||||
path: '',
|
||||
initialRouteName: 'Home',
|
||||
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
|
||||
(acc, name) => {
|
||||
// Convert screen names such as SimpleStack to kebab case (simple-stack)
|
||||
acc[name] = name
|
||||
.replace(/([A-Z]+)/g, '-$1')
|
||||
.replace(/^-/, '')
|
||||
.toLowerCase();
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ Home: '' }
|
||||
),
|
||||
},
|
||||
},
|
||||
}}
|
||||
fallback={<Text>Loading…</Text>}
|
||||
>
|
||||
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||
<Drawer.Screen
|
||||
|
||||
48
packages/native/src/Link.tsx
Normal file
48
packages/native/src/Link.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { Text, TextProps, GestureResponderEvent, Platform } from 'react-native';
|
||||
import useLinkTo from './useLinkTo';
|
||||
|
||||
type Props = {
|
||||
to: string;
|
||||
target?: string;
|
||||
} & (TextProps & { children: React.ReactNode });
|
||||
|
||||
export default function Link({ to, children, ...rest }: Props) {
|
||||
const linkTo = useLinkTo();
|
||||
|
||||
const onPress = (e: GestureResponderEvent | undefined) => {
|
||||
if ('onPress' in rest) {
|
||||
rest.onPress?.(e as GestureResponderEvent);
|
||||
}
|
||||
|
||||
const event = (e?.nativeEvent as any) as
|
||||
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||
| undefined;
|
||||
|
||||
if (Platform.OS !== 'web' || !event) {
|
||||
linkTo(to);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
!event.defaultPrevented && // onPress prevented default
|
||||
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) && // ignore clicks with modifier keys
|
||||
(event.button == null || event.button === 0) && // ignore everything but left clicks
|
||||
(rest.target == null || rest.target === '_self') // let browser handle "target=_blank" etc.
|
||||
) {
|
||||
event.preventDefault();
|
||||
linkTo(to);
|
||||
}
|
||||
};
|
||||
|
||||
const props = {
|
||||
href: to,
|
||||
onPress,
|
||||
accessibilityRole: 'link' as const,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return <Text {...props}>{children}</Text>;
|
||||
}
|
||||
12
packages/native/src/LinkingContext.tsx
Normal file
12
packages/native/src/LinkingContext.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { LinkingOptions } from './types';
|
||||
|
||||
const LinkingContext = React.createContext<() => LinkingOptions | undefined>(
|
||||
() => {
|
||||
throw new Error(
|
||||
"Couldn't find a linking context. Have you wrapped your app with 'NavigationContainer'?"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default LinkingContext;
|
||||
@@ -6,38 +6,76 @@ import {
|
||||
} from '@react-navigation/core';
|
||||
import ThemeProvider from './theming/ThemeProvider';
|
||||
import DefaultTheme from './theming/DefaultTheme';
|
||||
import LinkingContext from './LinkingContext';
|
||||
import useThenable from './useThenable';
|
||||
import useLinking from './useLinking';
|
||||
import useBackButton from './useBackButton';
|
||||
import { Theme } from './types';
|
||||
import { Theme, LinkingOptions } from './types';
|
||||
|
||||
type Props = NavigationContainerProps & {
|
||||
theme?: Theme;
|
||||
linking?: LinkingOptions;
|
||||
fallback?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Container component which holds the navigation state
|
||||
* designed for mobile apps.
|
||||
* Container component which holds the navigation state designed for React Native apps.
|
||||
* This should be rendered at the root wrapping the whole app.
|
||||
*
|
||||
* @param props.initialState Initial state object for the navigation tree.
|
||||
* @param props.initialState Initial state object for the navigation tree. When deep link handling is enabled, this will be ignored if there's an incoming link.
|
||||
* @param props.onStateChange Callback which is called with the latest navigation state when it changes.
|
||||
* @param props.theme Theme object for the navigators.
|
||||
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
|
||||
* @param props.fallback Fallback component to render until we have finished getting initial state when linking is enabled. Defaults to `null`.
|
||||
* @param props.children Child elements to render the content.
|
||||
* @param props.ref Ref object which refers to the navigation object containing helper methods.
|
||||
*/
|
||||
const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||
{ theme = DefaultTheme, ...rest }: Props,
|
||||
{ theme = DefaultTheme, linking, fallback = null, ...rest }: Props,
|
||||
ref?: React.Ref<NavigationContainerRef | null>
|
||||
) {
|
||||
const isLinkingEnabled = linking ? linking.enabled !== false : false;
|
||||
|
||||
const refContainer = React.useRef<NavigationContainerRef>(null);
|
||||
|
||||
useBackButton(refContainer);
|
||||
|
||||
const { getInitialState } = useLinking(refContainer, {
|
||||
enabled: isLinkingEnabled,
|
||||
prefixes: [],
|
||||
...linking,
|
||||
});
|
||||
|
||||
const [isReady, initialState = rest.initialState] = useThenable(
|
||||
getInitialState
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => refContainer.current);
|
||||
|
||||
const linkingOptionsRef = React.useRef(linking);
|
||||
|
||||
React.useEffect(() => {
|
||||
linkingOptionsRef.current = linking;
|
||||
});
|
||||
|
||||
const linkingContext = React.useCallback(() => linkingOptionsRef.current, []);
|
||||
|
||||
if (!isReady) {
|
||||
// This is temporary until we have Suspense for data-fetching
|
||||
// Then the fallback will be handled by a parent `Suspense` component
|
||||
return fallback as React.ReactElement;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider value={theme}>
|
||||
<BaseNavigationContainer {...rest} ref={refContainer} />
|
||||
</ThemeProvider>
|
||||
<LinkingContext.Provider value={linkingContext}>
|
||||
<ThemeProvider value={theme}>
|
||||
<BaseNavigationContainer
|
||||
{...rest}
|
||||
initialState={initialState}
|
||||
ref={refContainer}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</LinkingContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default function () {
|
||||
throw new Error(
|
||||
"'NavigationNativeContainer' has been renamed to 'NavigationContainer"
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
export * from '@react-navigation/core';
|
||||
|
||||
export { default as NavigationContainer } from './NavigationContainer';
|
||||
export { default as NavigationNativeContainer } from './NavigationNativeContainer';
|
||||
|
||||
export { default as useBackButton } from './useBackButton';
|
||||
export { default as useLinking } from './useLinking';
|
||||
export { default as useScrollToTop } from './useScrollToTop';
|
||||
|
||||
export { default as DefaultTheme } from './theming/DefaultTheme';
|
||||
export { default as DarkTheme } from './theming/DarkTheme';
|
||||
export { default as ThemeProvider } from './theming/ThemeProvider';
|
||||
export { default as useTheme } from './theming/useTheme';
|
||||
|
||||
export { default as Link } from './Link';
|
||||
export { default as useLinking } from './useLinking';
|
||||
export { default as useLinkTo } from './useLinkTo';
|
||||
|
||||
@@ -15,6 +15,11 @@ export type Theme = {
|
||||
};
|
||||
|
||||
export type LinkingOptions = {
|
||||
/**
|
||||
* Whether deep link handling should be enabled.
|
||||
* Defaults to true.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* The prefixes are stripped from the URL before parsing them.
|
||||
* Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`)
|
||||
|
||||
49
packages/native/src/useLinkTo.tsx
Normal file
49
packages/native/src/useLinkTo.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useNavigation,
|
||||
getStateFromPath,
|
||||
getActionFromState,
|
||||
} from '@react-navigation/core';
|
||||
import LinkingContext from './LinkingContext';
|
||||
|
||||
export default function useLinkTo() {
|
||||
const navigation = useNavigation();
|
||||
const getOptions = React.useContext(LinkingContext);
|
||||
|
||||
const linkTo = React.useCallback(
|
||||
(path: string) => {
|
||||
if (!path.startsWith('/')) {
|
||||
throw new Error(`The path must start with '/' (${path}).`);
|
||||
}
|
||||
|
||||
const options = getOptions();
|
||||
|
||||
const state = options?.getStateFromPath
|
||||
? options.getStateFromPath(path, options.config)
|
||||
: getStateFromPath(path, options?.config);
|
||||
|
||||
if (state) {
|
||||
let root = navigation;
|
||||
let current;
|
||||
|
||||
// Traverse up to get the root navigation
|
||||
while ((current = root.dangerouslyGetParent())) {
|
||||
root = current;
|
||||
}
|
||||
|
||||
const action = getActionFromState(state);
|
||||
|
||||
if (action !== undefined) {
|
||||
root.dispatch(action);
|
||||
} else {
|
||||
root.reset(state);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to parse the path to a navigation state.');
|
||||
}
|
||||
},
|
||||
[getOptions, navigation]
|
||||
);
|
||||
|
||||
return linkTo;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ let isUsingLinking = false;
|
||||
export default function useLinking(
|
||||
ref: React.RefObject<NavigationContainerRef>,
|
||||
{
|
||||
enabled,
|
||||
prefixes,
|
||||
config,
|
||||
getStateFromPath = getStateFromPathDefault,
|
||||
@@ -37,15 +38,17 @@ export default function useLinking(
|
||||
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
||||
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
||||
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
||||
const enabledRef = React.useRef(enabled);
|
||||
const prefixesRef = React.useRef(prefixes);
|
||||
const configRef = React.useRef(config);
|
||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||
|
||||
React.useEffect(() => {
|
||||
enabledRef.current = enabled;
|
||||
prefixesRef.current = prefixes;
|
||||
configRef.current = config;
|
||||
getStateFromPathRef.current = getStateFromPath;
|
||||
}, [config, getStateFromPath, prefixes]);
|
||||
}, [config, enabled, getStateFromPath, prefixes]);
|
||||
|
||||
const extractPathFromURL = React.useCallback((url: string) => {
|
||||
for (const prefix of prefixesRef.current) {
|
||||
@@ -58,7 +61,19 @@ export default function useLinking(
|
||||
}, []);
|
||||
|
||||
const getInitialState = React.useCallback(async () => {
|
||||
const url = await Linking.getInitialURL();
|
||||
if (!enabledRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = await (Promise.race([
|
||||
Linking.getInitialURL(),
|
||||
new Promise((resolve) =>
|
||||
// Timeout in 150ms if `getInitialState` doesn't resolve
|
||||
// Workaround for https://github.com/facebook/react-native/issues/25675
|
||||
setTimeout(resolve, 150)
|
||||
),
|
||||
]) as Promise<string | null | undefined>);
|
||||
|
||||
const path = url ? extractPathFromURL(url) : null;
|
||||
|
||||
if (path) {
|
||||
@@ -70,6 +85,10 @@ export default function useLinking(
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = ({ url }: { url: string }) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = extractPathFromURL(url);
|
||||
const navigation = ref.current;
|
||||
|
||||
@@ -91,7 +110,7 @@ export default function useLinking(
|
||||
Linking.addEventListener('url', listener);
|
||||
|
||||
return () => Linking.removeEventListener('url', listener);
|
||||
}, [extractPathFromURL, ref]);
|
||||
}, [enabled, extractPathFromURL, ref]);
|
||||
|
||||
return {
|
||||
getInitialState,
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '@react-navigation/core';
|
||||
import { LinkingOptions } from './types';
|
||||
|
||||
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||
|
||||
const getStateLength = (state: NavigationState) => {
|
||||
let length = 0;
|
||||
|
||||
@@ -32,6 +34,7 @@ let isUsingLinking = false;
|
||||
export default function useLinking(
|
||||
ref: React.RefObject<NavigationContainerRef>,
|
||||
{
|
||||
enabled,
|
||||
config,
|
||||
getStateFromPath = getStateFromPathDefault,
|
||||
getPathFromState = getPathFromStateDefault,
|
||||
@@ -54,25 +57,34 @@ export default function useLinking(
|
||||
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
||||
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
||||
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
||||
const enabledRef = React.useRef(enabled);
|
||||
const configRef = React.useRef(config);
|
||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||
const getPathFromStateRef = React.useRef(getPathFromState);
|
||||
|
||||
React.useEffect(() => {
|
||||
enabledRef.current = enabled;
|
||||
configRef.current = config;
|
||||
getStateFromPathRef.current = getStateFromPath;
|
||||
getPathFromStateRef.current = getPathFromState;
|
||||
}, [config, getPathFromState, getStateFromPath]);
|
||||
}, [config, enabled, getPathFromState, getStateFromPath]);
|
||||
|
||||
// Make it an async function to keep consistent with the native impl
|
||||
const getInitialState = React.useCallback(async () => {
|
||||
const path = location.pathname + location.search;
|
||||
const getInitialState = React.useCallback(() => {
|
||||
let value: ResultState | undefined;
|
||||
|
||||
if (path) {
|
||||
return getStateFromPathRef.current(path, configRef.current);
|
||||
} else {
|
||||
return undefined;
|
||||
if (enabledRef.current) {
|
||||
const path = location.pathname + location.search;
|
||||
|
||||
if (path) {
|
||||
value = getStateFromPathRef.current(path, configRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
// Make it a thenable to keep consistent with the native impl
|
||||
return {
|
||||
then: (callback: (state: ResultState | undefined) => void) =>
|
||||
callback(value),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
|
||||
@@ -92,10 +104,10 @@ export default function useLinking(
|
||||
const numberOfIndicesAhead = React.useRef(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('popstate', () => {
|
||||
const onPopState = () => {
|
||||
const navigation = ref.current;
|
||||
|
||||
if (!navigation) {
|
||||
if (!navigation || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,10 +181,18 @@ export default function useLinking(
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [ref]);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', onPopState);
|
||||
|
||||
return () => window.removeEventListener('popstate', onPopState);
|
||||
}, [enabled, ref]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ref.current && previousStateLengthRef.current === undefined) {
|
||||
previousStateLengthRef.current = getStateLength(
|
||||
ref.current.getRootState()
|
||||
|
||||
49
packages/native/src/useThenable.tsx
Normal file
49
packages/native/src/useThenable.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function useThenable<T>(
|
||||
create: () => {
|
||||
then(success: (result: T) => void, error?: (error: any) => void): void;
|
||||
}
|
||||
) {
|
||||
const [promise] = React.useState(create);
|
||||
|
||||
// Check if our thenable is synchronous
|
||||
let resolved = false;
|
||||
let value: T | undefined;
|
||||
|
||||
promise.then((result) => {
|
||||
resolved = true;
|
||||
value = result;
|
||||
});
|
||||
|
||||
const [state, setState] = React.useState<[boolean, T | undefined]>([
|
||||
resolved,
|
||||
value,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
if (!resolved) {
|
||||
promise.then(
|
||||
(result) => {
|
||||
if (!cancelled) {
|
||||
setState([true, result]);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
console.error(error);
|
||||
setState([true, undefined]);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [promise, resolved]);
|
||||
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user