Skip to content

Consolidate Router component #145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
74 changes: 13 additions & 61 deletions docs/api/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,82 +29,34 @@ 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) |
| `location` | `string` | If `history` prop is omitted, this configures the initial location for the default memory history (optional, useful for tests and storybooks) |
| `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.

29 changes: 17 additions & 12 deletions docs/router/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,25 @@ 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 `location` instead (or pass your own `MemoryHistory` if needed)

```js
// server-app.js
import { StaticRouter } from 'react-resource-router';
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 routes={routes} location={location}>
<App />
</StaticRouter>
</Router>
);
```

Expand All @@ -47,8 +50,10 @@ import { Router, createBrowserHistory } from 'react-resource-router';
import { App } from '../components';
import { routes } from '../routing/routes';

const history = createBrowserHistory();

export const ClientApp = () => (
<Router routes={routes} history={createBrowserHistory()}>
<Router routes={routes} history={history}>
<App />
</Router>
);
Expand All @@ -58,21 +63,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.
5 changes: 1 addition & 4 deletions examples/hydration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Router,
RouteComponent,
createBrowserHistory,
StaticRouter,
} from 'react-resource-router';

import { homeRoute } from './routes';
Expand All @@ -16,9 +15,7 @@ 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
23 changes: 13 additions & 10 deletions src/__tests__/integration/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { mount } from 'enzyme';
import * as historyHelper from 'history';
import { defaultRegistry } from 'react-sweet-state';

import { Router, RouterActions, StaticRouter } from '../../controllers';
import { RouteComponent } from '../../ui';
import { RouterActionsType } from '../../controllers/router-store/types';
import { mockRoute } from '../../common/mocks';
import { isServerEnvironment } from '../../common/utils/is-server-environment';
import { Router, RouterActions } from '../../controllers';
import { ResourceStore } from '../../controllers/resource-store';
import type { RouterActionsType } from '../../controllers/router-store/types';
import { RouteComponent } from '../../ui';

jest.mock('../../common/utils/is-server-environment');

const mockLocation = {
pathname: '/projects/123/board/456',
Expand Down Expand Up @@ -57,6 +60,7 @@ describe('<Router /> integration tests', () => {
history = historyHelper.createMemoryHistory(historyBuildOptions);
historyPushSpy = jest.spyOn(history, 'push');
historyReplaceSpy = jest.spyOn(history, 'replace');
(isServerEnvironment as any).mockReturnValue(false);
});

afterEach(() => {
Expand Down Expand Up @@ -137,8 +141,7 @@ describe('<Router /> integration tests', () => {
},
];

const serverData = await StaticRouter.requestResources({
// @ts-ignore
const serverData = await Router.requestResources({
routes: mockedRoutes,
location: mockLocation.pathname,
timeout: 350,
Expand Down Expand Up @@ -228,7 +231,7 @@ describe('<Router /> integration tests', () => {
});
});

describe('<StaticRouter /> integration tests', () => {
describe('<Router /> SSR-like integration tests', () => {
const basePath = '/base';
const route = {
path: '/anotherpath',
Expand All @@ -238,23 +241,23 @@ describe('<StaticRouter /> integration tests', () => {

it('should match the right route when basePath is set', async () => {
const wrapper = mount(
<StaticRouter
<Router
routes={[route]}
location={`${basePath}${route.path}`}
basePath={basePath}
>
<RouteComponent />
</StaticRouter>
</Router>
);

expect(wrapper.text()).toBe('important');
});

it('should match the right route when basePath is not set', async () => {
const wrapper = mount(
<StaticRouter routes={[route]} location={route.path}>
<Router routes={[route]} location={route.path}>
<RouteComponent />
</StaticRouter>
</Router>
);

expect(wrapper.text()).toBe('important');
Expand Down
21 changes: 0 additions & 21 deletions src/__tests__/unit/controllers/memory-router/test.tsx

This file was deleted.

11 changes: 8 additions & 3 deletions src/__tests__/unit/controllers/resource-store/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import { mount } from 'enzyme';
import { BoundActions, defaultRegistry } from 'react-sweet-state';

import { isServerEnvironment } from '../../../../common/utils/is-server-environment';
import { useResource } from '../../../../controllers/hooks';
import { getResourceStore } from '../../../../controllers/resource-store';
import { BASE_DEFAULT_STATE_SLICE } from '../../../../controllers/resource-store/constants';
Expand All @@ -24,6 +25,8 @@ import {
import { createResource } from '../../../../controllers/resource-utils';
import * as routerStoreModule from '../../../../controllers/router-store';

jest.mock('../../../../common/utils/is-server-environment');

jest.mock('../../../../controllers/resource-store/utils', () => ({
...jest.requireActual<any>('../../../../controllers/resource-store/utils'),
shouldUseCache: jest.fn(),
Expand Down Expand Up @@ -389,16 +392,18 @@ describe('resource store', () => {
});

describe('requestResources', () => {
it('should skip isBrowserOnly resources if isStatic is true', () => {
it('should skip isBrowserOnly resources if server environment', () => {
(isServerEnvironment as any).mockReturnValue(true);
const data = actions.requestResources(
[{ ...mockResource, isBrowserOnly: true }],
mockRouterStoreContext,
{ ...mockOptions, isStatic: true }
mockOptions
);

expect(data).toEqual([]);
});
it('should ignore isBrowserOnly if isStatic is falsey', async () => {
it('should ignore isBrowserOnly if not server environment', async () => {
(isServerEnvironment as any).mockReturnValue(false);
(getDefaultStateSlice as any).mockImplementation(() => ({
...BASE_DEFAULT_STATE_SLICE,
expiresAt: 1,
Expand Down
Loading