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:
Satyajit Sahoo
2020-05-24 14:28:16 +02:00
committed by GitHub
parent ced2a24aa6
commit 68e750d5a6
5 changed files with 160 additions and 1 deletions

View 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>
);
}

View 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;

View 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();
});

View File

@@ -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';

View File

@@ -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);