Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
* **Router**

- [Configuration](/router/configuration.md)
- [Selection](/router/selection.md)
- [State](/router/state.md)
- [SSR](/router/ssr.md)

Expand Down
73 changes: 12 additions & 61 deletions docs/api/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,82 +29,33 @@ import { appRoutes } from './routing';

### Router props

| prop | type | description |
| ----------------- | ------------------------- | -------------------------------------------------------------------------------------------------- |
| `routes` | `Routes[]` | Your application's routes |
| `initialRoute` | `Route` | The route your application is initially showing |
| `history` | `History` | The history instance for the router |
| `basePath` | `string` | Base path string that will get prepended to all route paths |
| `resourceContext` | `ResourceContext` | Custom contextual data that will be provided to all your resources' `getKey` and `getData` methods |
| `resourceData` | `ResourceData` | Pre-resolved resource data. When provided, the router will not request resources on mount |
| `onPrefetch` | `function(RouterContext)` | Called when prefetch is triggered from a Link |

## StaticRouter

If you are planning to render your application on the server, you must use the `StaticRouter` in your server side entry. The `StaticRouter` should only be used on server as it omits all browser-only resources. It does not require a `history` prop to be provided, instead, you simply need to provide the current `location` as a string. In order to achieve this, we recommend your server side application uses [`jsdom`](https://github.com/jsdom/jsdom).

```js
// server-app.js
import { StaticRouter } from 'react-resource-router';
import { App } from '../components';
import { appRoutes } from '../routing';

const { pathname, search } = window.location;
const location = `${pathname}${search}`;

export const ServerApp = () => (
<StaticRouter routes={appRoutes} location={location}>
<App />
</StaticRouter>
);
```

### StaticRouter props

| prop | type | description |
| ---------- | ---------- | ----------------------------------------------------------- |
| `routes` | `Routes[]` | Your application's routes |
| `location` | `string` | The string representation of the app's current location |
| `basePath` | `string` | Base path string that will get prepended to all route paths |
| prop | type | description |
| ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `routes` | `Routes[]` | Your application's routes |
| `history` | `History` | The history instance for the router, if omitted memory history will be used (optional but recommended) |
| `basePath` | `string` | Base path string that will get prepended to all route paths (optional) |
| `initialRoute` | `Route` | The route your application is initially showing, it's a performance optimisation to avoid route matching cost on initial render(optional) |
| `resourceContext` | `ResourceContext` | Custom contextual data that will be provided to all your resources' `getKey` and `getData` methods (optional) |
| `resourceData` | `ResourceData` | Pre-resolved resource data. When provided, the router will not request resources on mount (optional) |
| `onPrefetch` | `function(RouterContext)` | Called when prefetch is triggered from a Link (optional) |

## MemoryRouter

The `MemoryRouter` component can be used for your application's unit tests.

```js
it('should send right props after render with routes', () => {
mount(
<MemoryRouter routes={[mockRoutes[0]]}>
<RouterSubscriber>
{({ history, location, routes, route, match, query }) => {
expect(history).toEqual(mockHistory);
expect(location).toEqual(mockLocation);
expect(routes).toEqual(routes);
expect(route).toEqual(
expect.objectContaining({
path: `/pathname`,
})
);
expect(match).toBeTruthy();
expect(query).toEqual({
foo: 'bar',
});

return <div>I am a subscriber</div>;
}}
</RouterSubscriber>
</MemoryRouter>
);
render(<MemoryRouter routes={[mockRoutes[0]]}>{/* ... */}</MemoryRouter>);
});
```

### MemoryRouter props

| prop | type | description |
| ---------- | ---------- | ----------------------------------------------------------- |
| `routes` | `Routes[]` | Your application's routes |
| `location` | `string` | The string representation of the app's current location |
| `basePath` | `string` | Base path string that will get prepended to all route paths |
| `location` | `string` | The string representation of the app's current location |
| `routes` | `Routes[]` | Your application's routes |

## Link component

Expand Down
1 change: 0 additions & 1 deletion docs/router/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
- **Router**

- [Configuration](./configuration.md)
- [Selection](./selection.md)
- [State](./state.md)
- [SSR](./ssr.md)
3 changes: 0 additions & 3 deletions docs/router/selection.md

This file was deleted.

30 changes: 18 additions & 12 deletions docs/router/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,28 @@ import { RouteComponent } from 'react-resource-router';

export const App = () => (
<>
<Navigation />
<RouteComponent />
<Footer />
</>
);
```

The reason for this is that currently, you will need to use the [`Router`](#router-component) component on the client and the [`StaticRouter`](#staticrouter-component) component on the server. Following the above composition pattern will allow you to use the correct router in your server side entry and client side entry respectively. This could look something like the following examples:
When you need to SSR your app, we need to pass different props to Router, as `createBrowserHistory` does not really work on server, so we recommend to use your own `MemoryHistory`

```js
// server-app.js
import { StaticRouter } from 'react-resource-router';
import { createMemoryHistory } from 'history';
import { Router } from 'react-resource-router';
import { App } from '../components';
import { routes } from '../routing/routes';

export const ServerApp = ({ location, routes }) => (
<StaticRouter routes={routes} location={location}>
export const ServerApp = ({ location }) => (
<Router
history={createMemoryHistory({ initialEntries: [location]} )}
routes={routes}>
<App />
</StaticRouter>
</Router>
);
```

Expand All @@ -50,7 +56,7 @@ import { routes } from '../routing/routes';
const history = createBrowserHistory();

export const ClientApp = () => (
<Router routes={routes} history={history}>
<Router history={history} routes={routes}>
<App />
</Router>
);
Expand All @@ -60,21 +66,21 @@ export const ClientApp = () => (

Until React Suspense works on the server, we cannot do progressive rendering server side. To get around this, we need to `await` all resource requests to render our app _with all our resource data_ on the server.

Luckily the `StaticRouter` provides a convenient static method to do this for us.
Luckily the `Router` provides a convenient static method to do this for us.

```js
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-resource-router';
import { Router } from 'react-resource-router';
import { routes } from '../routing/routes';
import { ServerApp } from './app';

const renderToStringWithData = async ({ location }) => {
await StaticRouter.requestResources({ location, routes });
await Router.requestResources({ location, routes });

return renderToString(<ServerApp routes={routes} location={location} />);
return renderToString(<ServerApp location={location} />);
};
```

Notice that we do not need to provide any `resourceData` object to the `ServerApp`, the `StaticRouter` handles this for us internally.
Notice that we do not need to provide any `resourceData` object to the `ServerApp`, the `Router` handles this for us internally.

To prevent slow APIs from causing long renders on the server you can optionally pass in `timeout` as an option to `StaticRouter.requestResources`. If a route resource does not return within the specified time then its data and promise will be set to null.
To prevent slow APIs from causing long renders on the server you can optionally pass in `timeout` as an option to `Router.requestResources`. If a route resource does not return within the specified time then its data and promise will be set to null.
10 changes: 10 additions & 0 deletions examples/hash-routing/about.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

import { Link } from 'react-resource-router';

export const About = () => (
<div>
<h1>About</h1>
<Link to="/">Go to home</Link>
</div>
);
10 changes: 10 additions & 0 deletions examples/hash-routing/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

import { Link } from 'react-resource-router';

export const Home = () => (
<div>
<h1>Home</h1>
<Link to="/about">Go to about</Link>
</div>
);
11 changes: 11 additions & 0 deletions examples/hash-routing/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Example - Hash Routing</title>
</head>

<body>
<div id="root"></div>
<script src="bundle.js" type="text/javascript"></script>
</body>
</html>
45 changes: 45 additions & 0 deletions examples/hash-routing/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createHashHistory as createHashHistory4 } from 'history';
import { createHashHistory as createHashHistory5 } from 'history-5';
import React from 'react';
import { render } from 'react-dom';

import { About } from './about';
import { Home } from './home';

import { Redirect, RouteComponent, Router } from 'react-resource-router';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const history4 = createHashHistory4();
const history5 = createHashHistory5();
const history = history5;

const routes = [
{
name: 'home',
path: '/home',
exact: true,
component: () => <Home />,
},
{
name: 'about',
path: '/about',
exact: true,
component: () => <About />,
},
{
name: 'default',
path: '/*',
exact: true,
component: () => <Redirect to="/home" />,
},
];

const App = () => {
return (
<Router history={history} routes={routes}>
<RouteComponent />
</Router>
);
};

render(<App />, document.getElementById('root'));
5 changes: 1 addition & 4 deletions examples/hydration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ import {
Router,
RouteComponent,
createBrowserHistory,
StaticRouter,
} from 'react-resource-router';

const myHistory = createBrowserHistory();

const appRoutes = [homeRoute];

const getStateFromServer = async () => {
// StaticRouter should only be used on Server!
// It's used in Browser in this example for simplicity.
const resourceData = await StaticRouter.requestResources({
const resourceData = await Router.requestResources({
location: '/',
routes: appRoutes,
});
Expand Down
24 changes: 15 additions & 9 deletions src/__tests__/integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
RouteComponent,
Router,
RouteResource,
StaticRouter,
useResource,
} from '../index';

Expand Down Expand Up @@ -53,7 +52,7 @@ describe('<Router /> client-side integration tests', () => {
resources: [completedResource, timeoutResource],
};

const serverData = await StaticRouter.requestResources({
const serverData = await Router.requestResources({
location,
routes: [route],
timeout: 350,
Expand Down Expand Up @@ -153,7 +152,7 @@ describe('<Router /> client-side integration tests', () => {
const waitForData = () => new Promise(resolve => setTimeout(resolve));

const router = mount(
<Router history={history} isGlobal routes={routes}>
<Router history={history} routes={routes}>
<RouteComponent />
</Router>
);
Expand Down Expand Up @@ -289,7 +288,7 @@ describe('<Router /> client-side integration tests', () => {
});
});

describe('<StaticRouter /> server-side integration tests', () => {
describe('<Router /> server-side integration tests', () => {
const route = {
component: () => <>route component</>,
name: '',
Expand All @@ -302,23 +301,30 @@ describe('<StaticRouter /> server-side integration tests', () => {

it('renders the expected route when basePath is set', async () => {
const wrapper = mount(
<StaticRouter
<Router
basePath="/base-path"
location={`/base-path${route.path}`}
history={createMemoryHistory({
initialEntries: [`/base-path${route.path}`],
})}
routes={[route]}
>
<RouteComponent />
</StaticRouter>
</Router>
);

expect(wrapper.text()).toBe('route component');
});

it('renders the expected route when basePath is not set', async () => {
const wrapper = mount(
<StaticRouter location={route.path} routes={[route]}>
<Router
history={createMemoryHistory({
initialEntries: [route.path],
})}
routes={[route]}
>
<RouteComponent />
</StaticRouter>
</Router>
);

expect(wrapper.text()).toBe('route component');
Expand Down
17 changes: 4 additions & 13 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { History, Location as HistoryLocationShape } from 'history';
import { History as History4, Location as HistoryLocationShape } from 'history';
import { History as History5 } from 'history-5';
import {
ComponentType,
Expand All @@ -19,14 +19,16 @@ export type Location = {
};

export type BrowserHistory = (
| Omit<History, 'location' | 'go' | 'createHref' | 'push' | 'replace'>
| Omit<History4, 'location' | 'go' | 'createHref' | 'push' | 'replace'>
| Omit<History5, 'location' | 'go' | 'createHref' | 'push' | 'replace'>
) & {
location: Location;
push: (path: string | Location) => void;
replace: (path: string | Location) => void;
};

export type History = BrowserHistory;

export type MatchParams = {
[key: string]: string | null | typeof undefined;
};
Expand Down Expand Up @@ -308,17 +310,6 @@ export type HistoryActions = {
listen: HistoryListen;
};

export type MemoryRouterProps = {
basePath?: string;
isStatic?: boolean;
location?: string;
isGlobal?: boolean;
routes: Routes;
children?: ReactNode;
resourceData?: ResourceStoreData;
resourceContext?: ResourceStoreContext;
};

export type GenerateLocationOptions = {
params?: MatchParams;
query?: Query;
Expand Down
Loading