mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 04:45:19 +08:00
feat: add a ServerContainer component for SSR (#8297)
When doing SSR, the app needs to be aware of request URL to render correct navigation state.
The `ServerContainer` component lets us pass the `location` object to use for SSR.
The shape of the `location` object matches the `location` object in the browser.
Usage:
```js
ReactDOM.renderToString(
<ServerContainer location={{ pathname: req.path, search: req.search }}>
<App />
</ServerContainer>
);
```
Updated example: https://github.com/react-navigation/react-navigation/pull/8298
This commit is contained in:
20
packages/native/src/ServerContainer.tsx
Normal file
20
packages/native/src/ServerContainer.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import ServerContext, { ServerContextType } from './ServerContext';
|
||||
|
||||
type Props = ServerContextType & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Container component for server rendering.
|
||||
*
|
||||
* @param props.location Location object to base the initial URL for SSR.
|
||||
* @param props.children Child elements to render the content.
|
||||
*/
|
||||
export default function ServerContainer({ location, children }: Props) {
|
||||
return (
|
||||
<ServerContext.Provider value={{ location }}>
|
||||
{children}
|
||||
</ServerContext.Provider>
|
||||
);
|
||||
}
|
||||
14
packages/native/src/ServerContext.tsx
Normal file
14
packages/native/src/ServerContext.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type ServerContextType = {
|
||||
location: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ServerContext = React.createContext<ServerContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export default ServerContext;
|
||||
115
packages/native/src/__tests__/ServerContainer.test.tsx
Normal file
115
packages/native/src/__tests__/ServerContainer.test.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useNavigationBuilder,
|
||||
createNavigatorFactory,
|
||||
StackRouter,
|
||||
NavigationHelpersContext,
|
||||
} from '@react-navigation/core';
|
||||
import { render } from 'react-native-testing-library';
|
||||
import NavigationContainer from '../NavigationContainer';
|
||||
import ServerContainer from '../ServerContainer';
|
||||
|
||||
// @ts-ignore
|
||||
global.window = global;
|
||||
|
||||
window.addEventListener = () => {};
|
||||
window.removeEventListener = () => {};
|
||||
|
||||
// We want to use the web version of useLinking
|
||||
jest.mock('../useLinking', () => require('../useLinking.tsx').default);
|
||||
|
||||
it('renders correct state with location from ServerContainer', () => {
|
||||
const createStackNavigator = createNavigatorFactory((props: any) => {
|
||||
const { navigation, state, descriptors } = useNavigationBuilder(
|
||||
StackRouter,
|
||||
props
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
{state.routes.map((route) => (
|
||||
<div key={route.key}>{descriptors[route.key].render()}</div>
|
||||
))}
|
||||
</NavigationHelpersContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const TestScreen = ({ route }: any): any =>
|
||||
`${route.name} ${JSON.stringify(route.params)}`;
|
||||
|
||||
const NestedStack = () => {
|
||||
return (
|
||||
<Stack.Navigator initialRouteName="Feed">
|
||||
<Stack.Screen name="Profile" component={TestScreen} />
|
||||
<Stack.Screen name="Settings" component={TestScreen} />
|
||||
<Stack.Screen name="Feed" component={TestScreen} />
|
||||
<Stack.Screen name="Updates" component={TestScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer
|
||||
linking={{
|
||||
prefixes: [],
|
||||
config: {
|
||||
Home: {
|
||||
initialRouteName: 'Profile',
|
||||
screens: {
|
||||
Settings: {
|
||||
path: ':user/edit',
|
||||
},
|
||||
Updates: {
|
||||
path: ':user/updates',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen name="Home" component={NestedStack} />
|
||||
<Stack.Screen name="Chat" component={TestScreen} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
window.location = { pathname: '/jane/edit', search: '' };
|
||||
|
||||
const client = render(element);
|
||||
|
||||
expect(client).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div>
|
||||
Profile undefined
|
||||
</div>
|
||||
<div>
|
||||
Settings {"user":"jane"}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
client?.unmount();
|
||||
|
||||
const server = render(
|
||||
<ServerContainer location={{ pathname: '/john/updates', search: '' }}>
|
||||
{element}
|
||||
</ServerContainer>
|
||||
);
|
||||
|
||||
expect(server).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div>
|
||||
Profile undefined
|
||||
</div>
|
||||
<div>
|
||||
Updates {"user":"john"}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
server?.unmount();
|
||||
});
|
||||
@@ -15,3 +15,5 @@ export { default as useLinking } from './useLinking';
|
||||
export { default as useLinkTo } from './useLinkTo';
|
||||
export { default as useLinkProps } from './useLinkProps';
|
||||
export { default as useLinkBuilder } from './useLinkBuilder';
|
||||
|
||||
export { default as ServerContainer } from './ServerContainer';
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
NavigationState,
|
||||
getActionFromState,
|
||||
} from '@react-navigation/core';
|
||||
import ServerContext from './ServerContext';
|
||||
import { LinkingOptions } from './types';
|
||||
|
||||
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||
@@ -84,11 +85,17 @@ export default function useLinking(
|
||||
getPathFromStateRef.current = getPathFromState;
|
||||
}, [config, enabled, getPathFromState, getStateFromPath]);
|
||||
|
||||
const server = React.useContext(ServerContext);
|
||||
|
||||
const getInitialState = React.useCallback(() => {
|
||||
let value: ResultState | undefined;
|
||||
|
||||
if (enabledRef.current) {
|
||||
const path = location.pathname + location.search;
|
||||
const location =
|
||||
server?.location ??
|
||||
(typeof window !== 'undefined' ? window.location : undefined);
|
||||
|
||||
const path = location ? location.pathname + location.search : undefined;
|
||||
|
||||
if (path) {
|
||||
value = getStateFromPathRef.current(path, configRef.current);
|
||||
@@ -106,6 +113,7 @@ export default function useLinking(
|
||||
};
|
||||
|
||||
return thenable as PromiseLike<ResultState | undefined>;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
|
||||
|
||||
Reference in New Issue
Block a user