diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 0a28db2b..34705108 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -11,7 +11,6 @@
* **Router**
- [Configuration](/router/configuration.md)
- - [Selection](/router/selection.md)
- [State](/router/state.md)
- [SSR](/router/ssr.md)
diff --git a/docs/api/components.md b/docs/api/components.md
index d4e601e2..34327d1b 100644
--- a/docs/api/components.md
+++ b/docs/api/components.md
@@ -29,43 +29,16 @@ 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 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
@@ -73,28 +46,7 @@ The `MemoryRouter` component can be used for your application's unit tests.
```js
it('should send right props after render with routes', () => {
- mount(
-
-
- {({ 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 I am a subscriber
;
- }}
-
-
- );
+ render({/* ... */});
});
```
@@ -102,9 +54,9 @@ it('should send right props after render with routes', () => {
| 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
diff --git a/docs/router/README.md b/docs/router/README.md
index 1df7d24f..7809f9d1 100644
--- a/docs/router/README.md
+++ b/docs/router/README.md
@@ -1,6 +1,5 @@
- **Router**
- [Configuration](./configuration.md)
- - [Selection](./selection.md)
- [State](./state.md)
- [SSR](./ssr.md)
diff --git a/docs/router/selection.md b/docs/router/selection.md
deleted file mode 100644
index f034e806..00000000
--- a/docs/router/selection.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Which Router should I use?
-
-React Resource Router provides three kinds of routers which should be quite familiar to anyone who has used `react-router` previously. These are the core [`Router`](../api/components.md#router), the [`StaticRouter`](../api/components.md#staticrouter) for use on the server and the [`MemoryRouter`](../api/components.md#memoryrouter) for use in tests. Please check the [API](../api) docs for more detailed information about these components.
diff --git a/docs/router/ssr.md b/docs/router/ssr.md
index b1b0f8f9..27c7a6a3 100644
--- a/docs/router/ssr.md
+++ b/docs/router/ssr.md
@@ -22,22 +22,25 @@ import { RouteComponent } from 'react-resource-router';
export const App = () => (
<>
+
+
>
);
```
-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 }) => (
-
+export const ServerApp = ({ location }) => (
+
-
+
);
```
@@ -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 = () => (
-
+
);
@@ -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();
+ return renderToString();
};
```
-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.
diff --git a/examples/hydration/index.tsx b/examples/hydration/index.tsx
index 36d32793..39958190 100644
--- a/examples/hydration/index.tsx
+++ b/examples/hydration/index.tsx
@@ -6,7 +6,6 @@ import {
Router,
RouteComponent,
createBrowserHistory,
- StaticRouter,
} from 'react-resource-router';
import { homeRoute } from './routes';
@@ -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,
});
diff --git a/src/__tests__/integration/test.tsx b/src/__tests__/integration/test.tsx
index 05618d3f..7ce756b3 100644
--- a/src/__tests__/integration/test.tsx
+++ b/src/__tests__/integration/test.tsx
@@ -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',
@@ -57,6 +60,7 @@ describe(' integration tests', () => {
history = historyHelper.createMemoryHistory(historyBuildOptions);
historyPushSpy = jest.spyOn(history, 'push');
historyReplaceSpy = jest.spyOn(history, 'replace');
+ (isServerEnvironment as any).mockReturnValue(false);
});
afterEach(() => {
@@ -137,8 +141,7 @@ describe(' integration tests', () => {
},
];
- const serverData = await StaticRouter.requestResources({
- // @ts-ignore
+ const serverData = await Router.requestResources({
routes: mockedRoutes,
location: mockLocation.pathname,
timeout: 350,
@@ -228,7 +231,7 @@ describe(' integration tests', () => {
});
});
-describe(' integration tests', () => {
+describe(' SSR-like integration tests', () => {
const basePath = '/base';
const route = {
path: '/anotherpath',
@@ -238,13 +241,13 @@ describe(' integration tests', () => {
it('should match the right route when basePath is set', async () => {
const wrapper = mount(
-
-
+
);
expect(wrapper.text()).toBe('important');
@@ -252,9 +255,9 @@ describe(' integration tests', () => {
it('should match the right route when basePath is not set', async () => {
const wrapper = mount(
-
+
-
+
);
expect(wrapper.text()).toBe('important');
diff --git a/src/__tests__/unit/controllers/memory-router/test.tsx b/src/__tests__/unit/controllers/memory-router/test.tsx
deleted file mode 100644
index 56776dee..00000000
--- a/src/__tests__/unit/controllers/memory-router/test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { mount } from 'enzyme';
-import { defaultRegistry } from 'react-sweet-state';
-
-import { MemoryRouter } from '../../../../controllers/memory-router';
-import { getRouterState } from '../../../../controllers/router-store';
-
-describe('', () => {
- afterEach(() => {
- defaultRegistry.stores.clear();
- });
-
- it('should register an instance of memory history in the router store when mounted', () => {
- mount({'hello world'});
-
- const { history } = getRouterState();
-
- expect(history).toHaveProperty('canGo');
- });
-});
diff --git a/src/__tests__/unit/controllers/resource-store/test.tsx b/src/__tests__/unit/controllers/resource-store/test.tsx
index d2c47f23..a59ca68e 100644
--- a/src/__tests__/unit/controllers/resource-store/test.tsx
+++ b/src/__tests__/unit/controllers/resource-store/test.tsx
@@ -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';
@@ -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('../../../../controllers/resource-store/utils'),
shouldUseCache: jest.fn(),
@@ -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,
diff --git a/src/__tests__/unit/controllers/router-store/test.tsx b/src/__tests__/unit/controllers/router-store/test.tsx
index 5b1e9a92..fcb2391d 100644
--- a/src/__tests__/unit/controllers/router-store/test.tsx
+++ b/src/__tests__/unit/controllers/router-store/test.tsx
@@ -8,9 +8,10 @@ import { act } from 'react-dom/test-utils';
import { DEFAULT_ACTION } from '../../../../common/constants';
import { mockRoute } from '../../../../common/mocks';
-import { MemoryRouter } from '../../../../controllers/memory-router';
+import { isServerEnvironment } from '../../../../common/utils/is-server-environment';
import { getResourceStore } from '../../../../controllers/resource-store';
import { createResource } from '../../../../controllers/resource-utils';
+import { Router } from '../../../../controllers/router';
import {
createRouterSelector,
getRouterState,
@@ -20,13 +21,15 @@ import {
} from '../../../../controllers/router-store';
import { ResourceSubscriber } from '../../../../controllers/subscribers/resource';
+jest.mock('../../../../common/utils/is-server-environment');
+
const mockLocation = {
pathname: '/pathname',
search: '?foo=bar',
hash: '#hash',
};
-const mockHistory = {
+const mockHistory: any = {
push: jest.fn(),
replace: jest.fn(),
goBack: jest.fn(),
@@ -57,6 +60,7 @@ describe('SPA Router store', () => {
const { location } = window;
beforeAll(() => {
+ // @ts-ignore
delete window.location;
// @ts-ignore
window.location = {};
@@ -65,6 +69,7 @@ describe('SPA Router store', () => {
assign: { value: jest.fn() },
replace: { value: jest.fn() },
});
+ (isServerEnvironment as any).mockReturnValue(false);
});
afterEach(() => {
@@ -87,11 +92,11 @@ describe('SPA Router store', () => {
it('should call the history listener when initialised', () => {
mount(
-
+
{() => I am a subscriber
}
-
+
);
expect(mockHistory.listen).toBeCalled();
@@ -99,7 +104,7 @@ describe('SPA Router store', () => {
it('should send right props after render with routes', () => {
mount(
-
+
{({
history,
@@ -125,7 +130,7 @@ describe('SPA Router store', () => {
return I am a subscriber
;
}}
-
+
);
});
@@ -166,9 +171,9 @@ describe('SPA Router store', () => {
it('should send location with route change', async () => {
mount(
-
+
{children}
-
+
);
const { history } = children.mock.calls[0][0];
@@ -207,9 +212,9 @@ describe('SPA Router store', () => {
it('should send correct action key for route changes', async () => {
mount(
-
+
{children}
-
+
);
const { history } = children.mock.calls[0][0];
@@ -258,9 +263,9 @@ describe('SPA Router store', () => {
it('should send location with route change', async () => {
mount(
-
+
{children}
-
+
);
const { history } = children.mock.calls[0][0];
@@ -299,9 +304,9 @@ describe('SPA Router store', () => {
it('should send correct action key for route changes', async () => {
mount(
-
+
{children}
-
+
);
const { history } = children.mock.calls[0][0];
@@ -355,9 +360,9 @@ describe('SPA Router store', () => {
it('should push a relative path if the URL is absolute but on the same domain', () => {
mount(
-
+
{children}
-
+
);
const path = 'http://localhost:3000/board/123';
const { actions } = getRouterStore();
@@ -374,9 +379,9 @@ describe('SPA Router store', () => {
.mockImplementation(() => jest.fn());
mount(
-
+
{children}
-
+
);
const path = 'http://example.com';
@@ -398,9 +403,9 @@ describe('SPA Router store', () => {
it('should push a relative path generated from route and parameters', () => {
mount(
-
+
{children}
-
+
);
const route = { name: '', path: '/page/:id', component: () => null };
const { actions } = getRouterStore();
@@ -424,9 +429,9 @@ describe('SPA Router store', () => {
it('should replace a relative path if the URL is absolute but on the same domain', () => {
mount(
-
+
{children}
-
+
);
const path = 'http://localhost:3000/board/123';
const { actions } = getRouterStore();
@@ -443,9 +448,9 @@ describe('SPA Router store', () => {
.mockImplementation(() => jest.fn());
mount(
-
+
{children}
-
+
);
const path = 'http://example.com';
@@ -467,9 +472,9 @@ describe('SPA Router store', () => {
it('should replace a relative path generated from route and parameters', () => {
mount(
-
+
{children}
-
+
);
const route = { name: '', path: '/page/:id', component: () => null };
const { actions } = getRouterStore();
@@ -487,7 +492,6 @@ describe('SPA Router store', () => {
describe('resource store interop', () => {
const containerProps = {
- isStatic: false,
history: mockHistory,
routes: [],
resourceContext: {},
@@ -524,9 +528,9 @@ describe('SPA Router store', () => {
const spy = jest.spyOn(getResourceStore().actions, 'requestAllResources');
mount(
-
+
{children}
-
+
);
const { route, match, query } = getRouterState();
@@ -572,7 +576,7 @@ describe('SPA Router store', () => {
};
mount(
-
+
{(
{ route, location: currentLocation, query, match, action },
@@ -591,7 +595,7 @@ describe('SPA Router store', () => {
);
}}
-
+
);
await nextTick();
@@ -889,9 +893,9 @@ describe('SPA Router store', () => {
const route = { path: '', name: 'home', component: () => null };
const wrapper = mount(
-
+
-
+
);
expect(mockSelector).toBeCalledWith(
@@ -910,9 +914,9 @@ describe('SPA Router store', () => {
const route = { path: '', name: 'home', component: () => null };
const wrapper = mount(
-
+
-
+
);
expect(mockSelector).toBeCalledWith(
diff --git a/src/__tests__/unit/controllers/router/test.tsx b/src/__tests__/unit/controllers/router/test.tsx
index 9ca20b85..473aa91d 100644
--- a/src/__tests__/unit/controllers/router/test.tsx
+++ b/src/__tests__/unit/controllers/router/test.tsx
@@ -1,16 +1,21 @@
import React from 'react';
-
import { mount } from 'enzyme';
+import { mockRoute } from '../../../../common/mocks';
+import { isServerEnvironment } from '../../../../common/utils/is-server-environment';
+import { RouterSubscriber } from '../../../../controllers/subscribers/route';
+import { getResourceStore } from '../../../../controllers/resource-store';
import { Router } from '../../../../controllers/router';
-const MockLocation = {
+jest.mock('../../../../common/utils/is-server-environment');
+
+const mockLocation = {
pathname: 'pathname',
search: 'search',
hash: 'hash',
};
-const HistoryMock = {
+const HistoryMock: any = {
push: jest.fn(),
replace: jest.fn(),
goBack: jest.fn(),
@@ -18,7 +23,7 @@ const HistoryMock = {
registerBlock: jest.fn(),
listen: jest.fn(),
createHref: jest.fn(),
- location: MockLocation,
+ location: mockLocation,
_history: jest.fn(),
};
@@ -29,6 +34,7 @@ const routes: any[] = [];
describe('', () => {
beforeEach(() => {
HistoryMock.listen.mockReturnValue(unlistenMock);
+ (isServerEnvironment as any).mockReturnValue(false);
});
afterEach(() => {
@@ -37,7 +43,6 @@ describe('', () => {
it('renders a RouterContainer', () => {
const wrapper = mount(
- // @ts-ignore
hello
@@ -57,7 +62,6 @@ describe('', () => {
it('should call the history unlistener on unmount', () => {
const wrapper = mount(
- // @ts-ignore
hello
@@ -65,7 +69,7 @@ describe('', () => {
wrapper.unmount();
- expect(unlistenMock).toHaveBeenCalledTimes(1);
+ expect(unlistenMock).toHaveBeenCalled();
});
describe('when the router is re-mounted by a parent component', () => {
@@ -80,7 +84,6 @@ describe('', () => {
};
const newUnlistener = jest.fn();
const router = (
- // @ts-ignore
hello
@@ -99,8 +102,174 @@ describe('', () => {
expect(HistoryMock.listen).toHaveBeenCalledTimes(2);
// the original unlistener is called and the new one is not called
- expect(unlistenMock).toHaveBeenCalledTimes(1);
- expect(newUnlistener).toHaveBeenCalledTimes(0);
+ expect(unlistenMock).toHaveBeenCalled();
+ expect(newUnlistener).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('static requestResources', () => {
+ const type = 'type';
+ const key = 'key';
+ const result = 'result';
+ const resolver = (r: any, d = 0) =>
+ new Promise(resolve => setTimeout(() => resolve(r), d));
+ const getDataPromise = Promise.resolve(result);
+ const mockResource: any = {
+ type,
+ getKey: () => key,
+ getData: () => getDataPromise,
+ };
+ const expiresAt = null;
+
+ const mockedRoutes = [
+ {
+ ...mockRoute,
+ path: mockLocation.pathname,
+ component: () => foo
,
+ resources: [
+ {
+ ...mockResource,
+ ...{ type: 'HI', getData: () => resolver('hello world', 250) },
+ },
+ {
+ ...mockResource,
+ ...{
+ type: 'BYE',
+ getData: () => resolver('goodbye cruel world', 500),
+ },
+ },
+ ],
+ },
+ ];
+
+ it('should expose a static requestResources method', () => {
+ expect(typeof Router.requestResources).toBe('function');
+ });
+
+ it('should return hydratable, cleaned resource store state.data when awaited', async () => {
+ const data = await Router.requestResources({
+ location: mockLocation.pathname,
+ routes: mockedRoutes,
+ });
+
+ expect(data).toEqual({
+ BYE: {
+ key: {
+ data: 'goodbye cruel world',
+ error: null,
+ loading: false,
+ promise: null,
+ accessedAt: null,
+ expiresAt,
+ },
+ },
+ HI: {
+ key: {
+ data: 'hello world',
+ error: null,
+ loading: false,
+ promise: null,
+ accessedAt: null,
+ expiresAt,
+ },
+ },
+ });
+ });
+
+ it('should respect timeout when fetching resources', async () => {
+ const data = await Router.requestResources({
+ routes: mockedRoutes,
+ location: mockLocation.pathname,
+ timeout: 350,
+ });
+
+ expect(data).toEqual({
+ BYE: {
+ key: {
+ data: null,
+ error: {
+ message: 'Resource timed out: BYE',
+ name: 'TimeoutError',
+ stack: expect.any(String),
+ },
+ loading: true,
+ promise: null,
+ accessedAt: null,
+ expiresAt,
+ },
+ },
+ HI: {
+ key: {
+ data: 'hello world',
+ error: null,
+ loading: false,
+ promise: null,
+ accessedAt: null,
+ expiresAt,
+ },
+ },
+ });
+ });
+
+ it('should maintain the pre-requested state in the resource store when mounted', async () => {
+ await Router.requestResources({
+ routes: mockedRoutes,
+ location: mockLocation.pathname,
+ });
+
+ const resourceData = {
+ BYE: {
+ key: {
+ data: 'goodbye cruel world',
+ error: null,
+ loading: false,
+ promise: null,
+ accessedAt: null,
+ expiresAt,
+ },
+ },
+ HI: {
+ key: {
+ data: 'hello world',
+ error: null,
+ loading: false,
+ promise: null,
+ accessedAt: null,
+ expiresAt,
+ },
+ },
+ };
+
+ mount(
+
+
+ {() => I am a subscriber
}
+
+
+ );
+
+ expect(getResourceStore().actions.getSafeData()).toEqual(resourceData);
+ });
+
+ it('should not re-request resources on mount if resources have already been requested by requestResources', async () => {
+ const resourceSpy1 = jest.spyOn(mockedRoutes[0].resources[0], 'getData');
+ const resourceSpy2 = jest.spyOn(mockedRoutes[0].resources[1], 'getData');
+
+ await Router.requestResources({
+ routes: mockedRoutes,
+ location: mockLocation.pathname,
+ });
+
+ mount(
+
+
+ {() => I am a subscriber
}
+
+
+ );
+
+ expect(resourceSpy1).toHaveBeenCalledTimes(1);
+ expect(resourceSpy2).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/src/__tests__/unit/controllers/static-router/test.tsx b/src/__tests__/unit/controllers/static-router/test.tsx
deleted file mode 100644
index bf4df278..00000000
--- a/src/__tests__/unit/controllers/static-router/test.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import React from 'react';
-
-import { mount } from 'enzyme';
-import * as historyHelper from 'history';
-import { defaultRegistry } from 'react-sweet-state';
-
-import { mockRoute } from '../../../../common/mocks';
-import { getResourceStore } from '../../../../controllers/resource-store';
-import { StaticRouter } from '../../../../controllers/static-router';
-import { RouterSubscriber } from '../../../../controllers/subscribers/route';
-
-const mockLocation = {
- pathname: '/pathname',
- search: '?foo=bar',
- hash: '#hash',
-};
-const mockHistory = {
- push: jest.fn(),
- replace: jest.fn(),
- goBack: jest.fn(),
- goForward: jest.fn(),
- registerBlock: jest.fn(),
- listen: jest.fn(),
- createHref: jest.fn(),
- location: mockLocation,
-};
-const expiresAt = null;
-
-describe('', () => {
- afterEach(() => {
- defaultRegistry.stores.clear();
- jest.resetAllMocks();
- jest.restoreAllMocks();
- });
- beforeEach(() => {
- jest
- .spyOn(historyHelper, 'createMemoryHistory')
- // @ts-ignore
- .mockImplementation(() => mockHistory);
- });
-
- it('should not respond to history changes', () => {
- mount(
-
-
- {() => I am a subscriber
}
-
-
- );
-
- expect(mockHistory.listen).not.toHaveBeenCalled();
- });
-
- describe('static requestResources', () => {
- const type = 'type';
- const key = 'key';
- const result = 'result';
- const resolver = (r: any, d = 0) =>
- new Promise(resolve => setTimeout(() => resolve(r), d));
- const getDataPromise = Promise.resolve(result);
- const mockResource = {
- type,
- getKey: () => key,
- getData: () => getDataPromise,
- };
- const mockedRoutes = [
- {
- ...mockRoute,
- path: mockLocation.pathname,
- component: () => foo
,
- resources: [
- {
- ...mockResource,
- ...{ type: 'HI', getData: () => resolver('hello world', 250) },
- },
- {
- ...mockResource,
- ...{
- type: 'BYE',
- getData: () => resolver('goodbye cruel world', 500),
- },
- },
- ],
- },
- ];
-
- it('should expose a static requestResources method', () => {
- expect(typeof StaticRouter.requestResources).toBe('function');
- });
-
- it('should return hydratable, cleaned resource store state.data when awaited', async () => {
- const data = await StaticRouter.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- });
-
- expect(data).toEqual({
- BYE: {
- key: {
- data: 'goodbye cruel world',
- error: null,
- loading: false,
- promise: null,
- accessedAt: null,
- expiresAt,
- },
- },
- HI: {
- key: {
- data: 'hello world',
- error: null,
- loading: false,
- promise: null,
- accessedAt: null,
- expiresAt,
- },
- },
- });
- });
-
- it('should respect timeout when fetching resources', async () => {
- const data = await StaticRouter.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- location: '/',
- timeout: 350,
- });
-
- expect(data).toEqual({
- BYE: {
- key: {
- data: null,
- error: {
- message: 'Resource timed out: BYE',
- name: 'TimeoutError',
- stack: expect.any(String),
- },
- loading: true,
- promise: null,
- accessedAt: null,
- expiresAt,
- },
- },
- HI: {
- key: {
- data: 'hello world',
- error: null,
- loading: false,
- promise: null,
- accessedAt: null,
- expiresAt,
- },
- },
- });
- });
-
- it('should maintain the pre-requested state in the resource store when mounted', async () => {
- await StaticRouter.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- location: '/',
- });
-
- const resourceData = {
- BYE: {
- key: {
- data: 'goodbye cruel world',
- error: null,
- loading: false,
- promise: null,
- accessedAt: null,
- expiresAt,
- },
- },
- HI: {
- key: {
- data: 'hello world',
- error: null,
- loading: false,
- promise: null,
- accessedAt: null,
- expiresAt,
- },
- },
- };
-
- mount(
-
-
- {() => I am a subscriber
}
-
-
- );
-
- expect(getResourceStore().actions.getSafeData()).toEqual(resourceData);
- });
-
- it('should not re-request resources on mount if resources have already been requested by requestResources', async () => {
- const { pathname: location } = mockLocation;
- const resourceSpy1 = jest.spyOn(mockedRoutes[0].resources[0], 'getData');
- const resourceSpy2 = jest.spyOn(mockedRoutes[0].resources[1], 'getData');
-
- await StaticRouter.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- location,
- });
-
- mount(
-
-
- {() => I am a subscriber
}
-
-
- );
-
- expect(resourceSpy1).toHaveBeenCalledTimes(1);
- expect(resourceSpy2).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/src/__tests__/unit/controllers/subscribers/route/test.tsx b/src/__tests__/unit/controllers/subscribers/route/test.tsx
index 39e7380a..176b08c3 100644
--- a/src/__tests__/unit/controllers/subscribers/route/test.tsx
+++ b/src/__tests__/unit/controllers/subscribers/route/test.tsx
@@ -2,9 +2,11 @@ import React from 'react';
import { shallow } from 'enzyme';
+import { isServerEnvironment } from '../../../../../common/utils/is-server-environment';
import { RouterSubscriber } from '../../../../../controllers/router-store';
import { RouterSubscriber as MainRouterSubscriber } from '../../../../../controllers/subscribers/route';
+jest.mock('../../../../../common/utils/is-server-environment');
jest.mock('../../../../../controllers/router-store');
test('Route Subscriber should call listen if container is not inited', () => {
@@ -39,8 +41,9 @@ test('Route Subscriber should not call listen if container is inited', () => {
expect(mockActions.listen).not.toHaveBeenCalled();
});
-test('Route Subscriber should not call listen if container is static', () => {
- const mockState = { unlisten: () => null, isStatic: true };
+test('Route Subscriber should not call listen if server environment', () => {
+ (isServerEnvironment as any).mockReturnValue(true);
+ const mockState = { unlisten: () => null };
const mockActions = { listen: jest.fn() };
(RouterSubscriber as any).mockImplementation(
diff --git a/src/__tests__/unit/controllers/universal-router/test.tsx b/src/__tests__/unit/controllers/universal-router/test.tsx
deleted file mode 100644
index 9c3a4e56..00000000
--- a/src/__tests__/unit/controllers/universal-router/test.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-import React from 'react';
-
-import { mount } from 'enzyme';
-import * as historyHelper from 'history';
-import { mockRoute } from '../../../../common/mocks';
-import { UniversalRouter as Router } from '../../../../controllers/universal-router';
-import { RouterSubscriber } from '../../../../controllers/subscribers/route';
-import { getResourceStore } from '../../../../controllers/resource-store';
-import { isServerEnvironment } from '../../../../common/utils';
-
-const mockLocation = {
- pathname: '/pathname',
- search: '?foo=bar',
- hash: '#hash',
-};
-const mockHistory = {
- push: jest.fn(),
- replace: jest.fn(),
- goBack: jest.fn(),
- goForward: jest.fn(),
- registerBlock: jest.fn(),
- listen: jest.fn(),
- createHref: jest.fn(),
- location: mockLocation,
-};
-const expiresAt = null;
-const accessedAt = null;
-const unlistenMock = jest.fn();
-const routes: any[] = [];
-
-jest.mock('../../../../common/utils', () => ({
- ...jest.requireActual('../../../../common/utils'),
- isServerEnvironment: jest.fn(),
-}));
-
-describe('UniversalRouter', () => {
- describe('Browser environment', () => {
- beforeEach(() => {
- (isServerEnvironment as any).mockImplementation(() => false);
- mockHistory.listen.mockReturnValue(unlistenMock);
- });
-
- afterEach(() => {
- jest.resetAllMocks();
- });
-
- it('renders a RouterContainer', () => {
- const wrapper = mount(
- // @ts-ignore
-
- hello
-
- );
-
- const component = wrapper.find('UniversalRouterContainer');
-
- expect(component).toHaveLength(1);
-
- expect(component.props()).toEqual(
- expect.objectContaining({
- history: mockHistory,
- routes,
- })
- );
- });
-
- it('should call the history unlistener on unmount', () => {
- const wrapper = mount(
- // @ts-ignore
-
- hello
-
- );
-
- wrapper.unmount();
-
- expect(unlistenMock).toHaveBeenCalledTimes(1);
- });
-
- describe('when the router is re-mounted by a parent component', () => {
- it('should clean up the original history listener', () => {
- // @ts-ignore
- const RemountingParent = ({ shouldRemount = false, children }) => {
- if (shouldRemount) {
- return <>{children}>;
- }
-
- return {children}
;
- };
- const newUnlistener = jest.fn();
- const router = (
- // @ts-ignore
-
- hello
-
- );
- const wrapper = mount({router});
-
- // first listener is created on mount
- expect(mockHistory.listen).toHaveBeenCalledTimes(1);
-
- mockHistory.listen.mockReturnValue(newUnlistener);
-
- // trigger the re-mount
- wrapper.setProps({ shouldRemount: true });
-
- // second listener is created by the RouterContainer on re-mount
- expect(mockHistory.listen).toHaveBeenCalledTimes(2);
-
- // the original unlistener is called and the new one is not called
- expect(unlistenMock).toHaveBeenCalledTimes(1);
- expect(newUnlistener).toHaveBeenCalledTimes(0);
- });
- });
- });
-
- describe('Server environment', () => {
- beforeEach(() => {
- jest
- .spyOn(historyHelper, 'createMemoryHistory')
- // @ts-ignore
- .mockImplementation(() => mockHistory);
- mockHistory.listen.mockReturnValue(unlistenMock);
- });
-
- afterEach(() => {
- jest.resetAllMocks();
- jest.restoreAllMocks();
- });
-
- it('should listen to memory history if no history provided', () => {
- mount(
-
-
- {() => I am a subscriber
}
-
-
- );
-
- expect(mockHistory.listen).toHaveBeenCalled();
- });
-
- describe('static requestResources', () => {
- const type = 'type';
- const key = 'key';
- const result = 'result';
- const resolver = (r: any, d = 0) =>
- new Promise(resolve => setTimeout(() => resolve(r), d));
- const getDataPromise = Promise.resolve(result);
- const mockResource = {
- type,
- getKey: () => key,
- getData: () => getDataPromise,
- };
- const mockedRoutes = [
- {
- ...mockRoute,
- path: mockLocation.pathname,
- component: () => foo
,
- resources: [
- {
- ...mockResource,
- ...{ type: 'HI', getData: () => resolver('hello world', 250) },
- },
- {
- ...mockResource,
- ...{
- type: 'BYE',
- getData: () => resolver('goodbye cruel world', 500),
- },
- },
- ],
- },
- ];
-
- it('should expose a static requestResources method', () => {
- expect(typeof Router.requestResources).toBe('function');
- });
-
- it('should return hydratable, cleaned resource store state.data when awaited', async () => {
- const data = await Router.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- });
-
- expect(data).toEqual({
- BYE: {
- key: {
- data: 'goodbye cruel world',
- error: null,
- loading: false,
- promise: null,
- expiresAt,
- accessedAt,
- },
- },
- HI: {
- key: {
- data: 'hello world',
- error: null,
- loading: false,
- promise: null,
- expiresAt,
- accessedAt,
- },
- },
- });
- });
-
- it('should maintain the pre-requested state in the resource store when mounted', async () => {
- await Router.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- location: '/',
- });
-
- const resourceData = {
- BYE: {
- key: {
- data: 'goodbye cruel world',
- error: null,
- loading: false,
- promise: null,
- expiresAt,
- accessedAt,
- },
- },
- HI: {
- key: {
- data: 'hello world',
- error: null,
- loading: false,
- promise: null,
- expiresAt,
- accessedAt,
- },
- },
- };
-
- mount(
-
-
- {() => I am a subscriber
}
-
-
- );
-
- expect(getResourceStore().actions.getSafeData()).toEqual(resourceData);
- });
-
- it('should not re-request resources on mount if resources have already been requested by requestResources', async () => {
- const { pathname: location } = mockLocation;
- const resourceSpy1 = jest.spyOn(
- mockedRoutes[0].resources[0],
- 'getData'
- );
- const resourceSpy2 = jest.spyOn(
- mockedRoutes[0].resources[1],
- 'getData'
- );
-
- await Router.requestResources({
- // @ts-ignore
- routes: mockedRoutes,
- location,
- });
-
- mount(
-
-
- {() => I am a subscriber
}
-
-
- );
-
- expect(resourceSpy1).toHaveBeenCalledTimes(1);
- expect(resourceSpy2).toHaveBeenCalledTimes(1);
- });
- });
- });
-
- describe('Memory environment', () => {
- it('should register an instance of memory history in the router store when mounted with a location set', () => {
- let memoryHistory;
-
- mount(
-
-
- {
- /* @ts-ignore */
- ({ history }) => {
- memoryHistory = history;
-
- return null;
- }
- }
-
-
- );
-
- expect(memoryHistory).toHaveProperty('canGo');
- });
- });
-});
diff --git a/src/common/types.ts b/src/common/types.ts
index d78929dc..b5f2bfcd 100644
--- a/src/common/types.ts
+++ b/src/common/types.ts
@@ -256,17 +256,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;
diff --git a/src/common/utils/create-location/index.ts b/src/common/utils/create-location/index.ts
index ffeb2676..47890748 100644
--- a/src/common/utils/create-location/index.ts
+++ b/src/common/utils/create-location/index.ts
@@ -1 +1 @@
-export { createLocation } from './create-location';
\ No newline at end of file
+export { createLocation } from './create-location';
diff --git a/src/common/utils/create-location/parse-path.ts b/src/common/utils/create-location/parse-path.ts
index c69b2a42..5b2b7c0f 100644
--- a/src/common/utils/create-location/parse-path.ts
+++ b/src/common/utils/create-location/parse-path.ts
@@ -9,7 +9,7 @@ export function parsePath(path: string) {
return {
pathname,
- search: url.query,
+ search: (url.query as any) as string,
hash: url.hash,
};
}
diff --git a/src/controllers/index.ts b/src/controllers/index.ts
index fd0a2322..559020ef 100644
--- a/src/controllers/index.ts
+++ b/src/controllers/index.ts
@@ -1,7 +1,5 @@
export { RouterSubscriber, ResourceSubscriber } from './subscribers';
-export { Router } from './router';
-export { MemoryRouter } from './memory-router';
-export { StaticRouter } from './static-router';
+export { Router, MemoryRouter } from './router';
export { RouterActions } from './router-actions';
export { Redirect } from './redirect';
export { withRouter, WithRouter } from './with-router';
diff --git a/src/controllers/memory-router/index.tsx b/src/controllers/memory-router/index.tsx
deleted file mode 100644
index e33f2909..00000000
--- a/src/controllers/memory-router/index.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-
-import { createMemoryHistory, MemoryHistoryBuildOptions } from 'history';
-
-import { Router } from '../router';
-import { RouterProps } from '../router/types';
-
-import { MemoryRouterProps } from '../../common/types';
-
-const getRouterProps = (memoryRouterProps: MemoryRouterProps) => {
- const {
- isStatic = false,
- isGlobal = true,
- basePath,
- routes,
- resourceData,
- resourceContext,
- } = memoryRouterProps;
- let routerProps: Partial = {
- basePath,
- routes,
- isStatic,
- isGlobal,
- };
-
- if (resourceData) {
- routerProps = { ...routerProps, resourceData };
- }
-
- if (resourceContext) {
- routerProps = { ...routerProps, resourceContext };
- }
-
- return routerProps;
-};
-
-/**
- * Ensures the router store uses memory history.
- *
- */
-export const MemoryRouter = (props: MemoryRouterProps) => {
- const { location, children } = props;
- const config: MemoryHistoryBuildOptions = {};
-
- if (location) {
- config.initialEntries = [location];
- }
-
- const history = createMemoryHistory(config);
- const routerProps = getRouterProps(props);
-
- return (
- // @ts-ignore suppress history will be overwritten warning
-
- {children}
-
- );
-};
diff --git a/src/controllers/resource-store/index.tsx b/src/controllers/resource-store/index.tsx
index 7909ce65..6ec059dc 100644
--- a/src/controllers/resource-store/index.tsx
+++ b/src/controllers/resource-store/index.tsx
@@ -11,6 +11,7 @@ import {
ResourceStoreData,
RouteResourceResponse,
} from '../../common/types';
+import { isServerEnvironment } from '../../common/utils';
import { getResourceStoreContext, getSliceForResource } from './selectors';
import {
@@ -230,7 +231,7 @@ export const actions: Actions = {
dispatch,
}) => {
// Filter out isBrowserOnly resources if on server
- const filteredResources = options.isStatic
+ const filteredResources = isServerEnvironment()
? resources.filter(resource => !resource.isBrowserOnly)
: resources;
diff --git a/src/controllers/resource-store/types.ts b/src/controllers/resource-store/types.ts
index 53afea74..66c82497 100644
--- a/src/controllers/resource-store/types.ts
+++ b/src/controllers/resource-store/types.ts
@@ -31,7 +31,6 @@ export type ResourceSliceIdentifier = {
type GetResourceOptions = {
prefetch?: boolean;
timeout?: number;
- isStatic?: boolean;
};
type ResourceAction = ({
diff --git a/src/controllers/router-store/index.tsx b/src/controllers/router-store/index.tsx
index 6cfa853b..6e9a93c5 100644
--- a/src/controllers/router-store/index.tsx
+++ b/src/controllers/router-store/index.tsx
@@ -27,7 +27,6 @@ import { getResourcesForNextLocation } from '../resource-store/utils';
import {
AllRouterActions,
ContainerProps,
- UniversalRouterContainerProps,
EntireRouterState,
RouterState,
} from './types';
@@ -51,7 +50,6 @@ export const INITIAL_STATE: EntireRouterState = {
action: DEFAULT_ACTION,
unlisten: null,
basePath: '',
- isStatic: false,
onPrefetch: undefined,
};
@@ -62,63 +60,29 @@ const actions: AllRouterActions = {
*/
bootstrapStore: props => ({ setState, dispatch }) => {
const {
+ basePath = '',
+ history,
+ initialRoute,
resourceContext,
resourceData,
- basePath = '',
routes,
- initialRoute,
- ...initialProps
} = props;
- const { history, isStatic } = initialProps;
const routerContext = findRouterContext(
initialRoute ? [initialRoute] : routes,
{ location: history.location, basePath }
);
setState({
- ...initialProps,
...routerContext,
basePath,
+ history,
routes,
location: history.location,
action: history.action,
});
getResourceStore().actions.hydrate({ resourceContext, resourceData });
- if (!isStatic) {
- dispatch(actions.listen());
- }
- },
-
- /**
- * Duplicate method that uses isServerEnvironment instead of removed isStatic prop
- * internally. We can remove this when UniversalRouter replaces Router completely.
- */
- bootstrapStoreUniversal: props => ({ setState, dispatch }) => {
- const {
- resourceContext,
- resourceData,
- basePath = '',
- ...initialProps
- } = props;
- const { history, routes } = initialProps;
- const routerContext = findRouterContext(routes, {
- location: history.location,
- basePath,
- });
-
- setState({
- ...initialProps,
- ...routerContext,
- basePath,
- location: history.location,
- action: history.action,
- });
- getResourceStore().actions.hydrate({ resourceContext, resourceData });
-
- if (!isServerEnvironment()) {
- dispatch(actions.listen());
- }
+ dispatch(actions.listen());
},
/**
@@ -174,7 +138,8 @@ const actions: AllRouterActions = {
*
*/
listen: () => ({ getState, setState }) => {
- const { history } = getState();
+ const { history, unlisten } = getState();
+ if (unlisten) unlisten();
type LocationUpateV4 = [Location, Action];
type LocationUpateV5 = [{ location: Location; action: Action }];
@@ -364,7 +329,7 @@ export const RouterContainer = createContainer(
displayName: 'RouterContainer',
onInit: () => ({ dispatch }, props) => {
dispatch(actions.bootstrapStore(props));
- !props.isStatic && dispatch(actions.requestRouteResources());
+ !isServerEnvironment() && dispatch(actions.requestRouteResources());
},
onCleanup: () => () => {
if (process.env.NODE_ENV === 'development') {
@@ -377,26 +342,6 @@ export const RouterContainer = createContainer(
}
);
-export const UniversalRouterContainer = createContainer<
- State,
- Actions,
- UniversalRouterContainerProps
->(RouterStore, {
- displayName: 'UniversalRouterContainer',
- onInit: () => ({ dispatch }, props) => {
- dispatch(actions.bootstrapStoreUniversal(props));
- !isServerEnvironment() && dispatch(actions.requestRouteResources());
- },
- onCleanup: () => () => {
- if (process.env.NODE_ENV === 'development') {
- // eslint-disable-next-line no-console
- console.warn(
- `Warning: react-resource-router has been unmounted! Was this intentional? Resources will be refetched when the router is mounted again.`
- );
- }
- },
-});
-
export const RouterSubscriber = createSubscriber(RouterStore, {
displayName: 'BaseRouterSubscriber',
});
diff --git a/src/controllers/router-store/types.ts b/src/controllers/router-store/types.ts
index f55f1fe6..341ad8dd 100644
--- a/src/controllers/router-store/types.ts
+++ b/src/controllers/router-store/types.ts
@@ -20,62 +20,52 @@ import {
import { MemoryHistory, UnregisterCallback } from 'history';
type PublicStateProperties = {
+ action: HistoryAction;
basePath: string;
location: Location;
+ match: Match;
query: Query;
route: Route;
- match: Match;
- action: HistoryAction;
};
export type UnlistenHistory = () => void;
type PrivateStateProperties = {
- routes: Routes;
history: BrowserHistory;
- unlisten: UnlistenHistory | null;
- isStatic: boolean;
onPrefetch?: (routerContext: RouterContext) => void;
+ routes: Routes;
+ unlisten: UnlistenHistory | null;
};
export type EntireRouterState = PublicStateProperties & PrivateStateProperties;
export type ContainerProps = {
- isStatic?: boolean;
+ basePath?: string;
history: BrowserHistory | MemoryHistory;
initialRoute?: Route;
+ isGlobal?: boolean;
location?: Location;
- basePath?: string;
- routes: Routes;
- resourceData?: ResourceStoreData;
- resourceContext?: ResourceStoreContext;
onPrefetch?: (routerContext: RouterContext) => void;
+ resourceContext?: ResourceStoreContext;
+ resourceData?: ResourceStoreData;
+ routes: Routes;
};
-export type UniversalRouterContainerProps = { isGlobal?: boolean } & Omit<
- ContainerProps,
- 'isStatic'
->;
-
export type RouterAction = Action;
export type HistoryUpdateType = 'push' | 'replace';
type ToAttributes = {
- params?: MatchParams;
query?: Query;
+ params?: MatchParams;
};
type RequestRouteResourcesOptions = {
timeout?: number;
- isStatic?: boolean;
};
type PrivateRouterActions = {
bootstrapStore: (initialState: ContainerProps) => RouterAction;
- bootstrapStoreUniversal: (
- initialState: UniversalRouterContainerProps
- ) => RouterAction;
requestRouteResources: (
options?: RequestRouteResourcesOptions
) => Action>;
diff --git a/src/controllers/router/index.tsx b/src/controllers/router/index.tsx
index c4be6434..55a19b69 100644
--- a/src/controllers/router/index.tsx
+++ b/src/controllers/router/index.tsx
@@ -1,77 +1,92 @@
-import React, { Component } from 'react';
+import React, { useMemo, useEffect } from 'react';
+import { createMemoryHistory } from 'history';
-import { DEFAULT_HISTORY } from '../../common/constants';
+import { createLocation } from '../../common/utils/create-location';
import { getRouterState, RouterContainer } from '../router-store';
-import { UnlistenHistory } from '../router-store/types';
+import { getResourceStore, ResourceContainer } from '../resource-store';
+import { getRouterStore } from '../router-store';
-import { RouterProps } from './types';
+import {
+ RouterProps,
+ MemoryRouterProps,
+ RequestResourcesParams,
+} from './types';
-/**
- * Default prop provider for the RouterContainer.
- *
- */
-export class Router extends Component {
- static defaultProps = {
- isStatic: false,
- isGlobal: true,
- history: DEFAULT_HISTORY,
- };
-
- /**
- * Keep a copy of the history listener so that we can be sure that
- * on unmount we call the listener that was in the router store at the
- * time of mounting.
- *
- * This prevents an issue where the wrong listener is removed if the router
- * is re-mounted.
- */
- unlistenHistory: UnlistenHistory | null = null;
+const Router = ({
+ basePath,
+ children,
+ initialRoute,
+ isGlobal = true,
+ onPrefetch,
+ resourceContext,
+ resourceData,
+ routes,
+ ...props
+}: RouterProps) => {
+ const history = useMemo(
+ () =>
+ props.history ||
+ createMemoryHistory(
+ props.location ? { initialEntries: [props.location] } : {}
+ ),
+ [props.history, props.location]
+ );
- componentDidMount() {
- if (!this.props.isStatic) {
- const state = getRouterState();
- this.unlistenHistory = state.unlisten;
- }
- }
+ useEffect(() => {
+ const { unlisten } = getRouterState();
- /**
- * Ensures that the router store stops listening to history when the Router
- * is unmounted.
- */
- componentWillUnmount() {
- if (this.unlistenHistory) {
- this.unlistenHistory();
- }
- }
+ return () => {
+ unlisten && unlisten();
+ };
+ }, []);
- render() {
- const {
- children,
- routes,
- history,
- initialRoute,
- isStatic,
- isGlobal,
- basePath,
- resourceContext,
- resourceData,
- onPrefetch,
- } = this.props;
-
- return (
+ return (
+
{children}
- );
- }
-}
+
+ );
+};
+
+/**
+ * The entry point for requesting resource data on the server.
+ * Pass the result data into the router as a prop in order to hydrate it.
+ */
+Router.requestResources = async ({
+ location,
+ timeout,
+ history,
+ ...bootstrapProps
+}: RequestResourcesParams) => {
+ const { bootstrapStore, requestRouteResources } = getRouterStore().actions;
+
+ bootstrapStore({
+ ...bootstrapProps,
+ history: history || createMemoryHistory({ initialEntries: [location] }),
+ location: createLocation(location),
+ });
+
+ await requestRouteResources({ timeout });
+
+ return getResourceStore().actions.getSafeData();
+};
+
+Router.addResourcesListener = (fn: (...args: any) => any) =>
+ getResourceStore().storeState.subscribe(fn);
+
+// Expose
+const MemoryRouter = (props: MemoryRouterProps) => (
+
+);
+
+export { Router, MemoryRouter };
diff --git a/src/controllers/router/types.ts b/src/controllers/router/types.ts
index 2227e485..2d43182c 100644
--- a/src/controllers/router/types.ts
+++ b/src/controllers/router/types.ts
@@ -10,13 +10,27 @@ import {
} from '../../common/types';
export type RouterProps = PropsWithChildren<{
- isStatic: boolean;
- history: BrowserHistory;
+ basePath?: string;
+ history?: BrowserHistory;
initialRoute?: Route;
+ isGlobal?: boolean;
+ location?: string;
+ onPrefetch?: (routerContext: RouterContext) => void;
resourceContext?: ResourceStoreContext;
resourceData?: ResourceStoreData;
+ routes: Routes;
+}>;
+
+export type MemoryRouterProps = PropsWithChildren<{
basePath?: string;
+ location?: string;
routes: Routes;
- isGlobal?: boolean;
- onPrefetch?: (routerContext: RouterContext) => void;
}>;
+
+export type RequestResourcesParams = {
+ history?: BrowserHistory;
+ location: string;
+ resourceContext?: ResourceStoreContext;
+ routes: Routes;
+ timeout?: number;
+};
diff --git a/src/controllers/static-router/index.tsx b/src/controllers/static-router/index.tsx
deleted file mode 100644
index c9d647c1..00000000
--- a/src/controllers/static-router/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-
-import { createMemoryHistory } from 'history';
-
-import { createLocation } from '../../common/utils/create-location';
-import { MemoryRouterProps } from '../../common/types';
-import { MemoryRouter } from '../memory-router';
-import { getResourceStore } from '../resource-store';
-import { getRouterStore } from '../router-store';
-
-import { RequestResourcesParams } from './types';
-
-/**
- * Ensures the router store will not respond to history changes.
- *
- */
-export const StaticRouter = ({
- location,
- routes,
- children,
- basePath,
-}: MemoryRouterProps) => (
-
- {children}
-
-);
-
-/**
- * The entry point for requesting resource data on the server.
- * Pass the result data into the router as a prop in order to hydrate it.
- * TODO: return type (see imports)
- */
-StaticRouter.requestResources = async (props: RequestResourcesParams) => {
- const { bootstrapStore, requestRouteResources } = getRouterStore().actions;
- const { location, timeout, ...bootstrapProps } = props;
- const initialEntries = [location];
- const overrides = {
- history: createMemoryHistory({ initialEntries }),
- location: createLocation(location),
- isStatic: true,
- };
-
- bootstrapStore({ ...bootstrapProps, ...overrides });
-
- await requestRouteResources({ timeout, isStatic: true });
-
- return getResourceStore().actions.getSafeData();
-};
-
-StaticRouter.addResourcesListener = (fn: (...args: any) => any) =>
- getResourceStore().storeState.subscribe(fn);
diff --git a/src/controllers/static-router/types.ts b/src/controllers/static-router/types.ts
deleted file mode 100644
index c3ca56c2..00000000
--- a/src/controllers/static-router/types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ResourceStoreContext, Routes } from '../../common/types';
-
-export type RequestResourcesParams = {
- location: string;
- routes: Routes;
- resourceContext?: ResourceStoreContext;
- timeout?: number;
-};
diff --git a/src/controllers/subscribers/route/index.tsx b/src/controllers/subscribers/route/index.tsx
index dfbf60e5..9ed62eed 100644
--- a/src/controllers/subscribers/route/index.tsx
+++ b/src/controllers/subscribers/route/index.tsx
@@ -1,5 +1,6 @@
import React, { ReactNode } from 'react';
+import { isServerEnvironment } from '../../../common/utils';
import { RouterSubscriber as BaseRouterSubscriber } from '../../router-store';
import { RouterActionsType, RouterState } from '../../router-store/types';
@@ -10,7 +11,7 @@ type Props = {
export const RouterSubscriber = ({ children }: Props) => (
{(state, { listen, ...actions }) => {
- if (!state.unlisten && !state.isStatic) {
+ if (!state.unlisten && !isServerEnvironment()) {
listen();
}
diff --git a/src/controllers/universal-router/index.tsx b/src/controllers/universal-router/index.tsx
deleted file mode 100644
index da683731..00000000
--- a/src/controllers/universal-router/index.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React, { Component } from 'react';
-
-import { BrowserHistory } from '../../common/types';
-import { createLocation } from '../../common/utils/create-location';
-import { getRouterState, UniversalRouterContainer } from '../router-store';
-import { UnlistenHistory } from '../router-store/types';
-import { UniversalRouterProps, RequestResourcesParams } from './types';
-import { createMemoryHistory } from 'history';
-import { getResourceStore, ResourceContainer } from '../resource-store';
-import { getRouterStore } from '../router-store';
-
-/**
- * Default prop provider for the RouterContainer.
- *
- */
-export class UniversalRouter extends Component {
- static defaultProps = {
- isGlobal: true,
- };
-
- /**
- * The entry point for requesting resource data on the server.
- * Pass the result data into the router as a prop in order to hydrate it.
- * TODO: return type
- */
- static async requestResources(props: RequestResourcesParams) {
- const {
- bootstrapStoreUniversal,
- requestRouteResources,
- } = getRouterStore().actions;
- const { location, timeout, ...bootstrapProps } = props;
- const initialEntries = [location];
- const overrides = {
- history: createMemoryHistory({ initialEntries }),
- location: createLocation(location),
- };
-
- bootstrapStoreUniversal({ ...bootstrapProps, ...overrides });
-
- await requestRouteResources({ timeout });
-
- return getResourceStore().actions.getSafeData();
- }
-
- /**
- * Keep a copy of the history listener so that we can be sure that
- * on unmount we call the listener that was in the router store at the
- * time of mounting.
- *
- * This prevents an issue where the wrong listener is removed if the router
- * is re-mounted.
- */
- unlistenHistory: UnlistenHistory | null = null;
- history: BrowserHistory;
-
- constructor(props: UniversalRouterProps) {
- super(props);
- const initialEntries = props.location ? [props.location] : [];
- this.history = props.history || createMemoryHistory({ initialEntries });
- }
-
- componentDidMount() {
- const state = getRouterState();
- this.unlistenHistory = state.unlisten;
- }
-
- /**
- * Ensures that the router store stops listening to history when the Router
- * is unmounted.
- */
- componentWillUnmount() {
- if (this.unlistenHistory) {
- this.unlistenHistory();
- }
- }
-
- render() {
- const {
- children,
- routes,
- resourceContext,
- resourceData,
- isGlobal,
- onPrefetch,
- } = this.props;
-
- return (
-
- {children}
-
- );
- }
-}
diff --git a/src/controllers/universal-router/types.ts b/src/controllers/universal-router/types.ts
deleted file mode 100644
index 7fd16317..00000000
--- a/src/controllers/universal-router/types.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ReactNode } from 'react';
-
-import {
- BrowserHistory,
- ResourceStoreContext,
- ResourceStoreData,
- RouterContext,
- Routes,
-} from '../../common/types';
-
-export type UniversalRouterProps = {
- history?: BrowserHistory;
- isGlobal?: boolean;
- location?: string;
- resourceContext?: ResourceStoreContext;
- resourceData?: ResourceStoreData;
- routes: Routes;
- children: ReactNode;
- onPrefetch?: (routerContext: RouterContext) => void;
-};
-
-export type RequestResourcesParams = {
- location: string;
- routes: Routes;
- resourceContext?: ResourceStoreContext;
- timeout?: number;
-};
diff --git a/src/index.js.flow b/src/index.js.flow
index dc9caa1c..ecad9145 100644
--- a/src/index.js.flow
+++ b/src/index.js.flow
@@ -23,7 +23,6 @@ import type {
RouterState,
RouterContext,
RouterSubscriberProps,
- StaticRouterProps,
CreateRouterContextOptions,
ResourceStoreData,
RequestResourcesParams,
@@ -44,16 +43,13 @@ declare export function RouterActions(props: {|
children: (actions: RouterActionsType) => Node,
|}): Node;
-declare export function Router(props: RouterProps): Node;
-declare export var MemoryRouter: {
- (props: MemoryRouterProps): Node,
- requestResources: RequestResourcesParams => Promise,
-};
-declare export var StaticRouter: {
- (props: StaticRouterProps): Node,
+declare export var Router: {
+ (props: RouterProps): Node,
requestResources: RequestResourcesParams => Promise,
};
+declare export function MemoryRouter(props: MemoryRouterProps): Node;
+
declare export function ResourceSubscriber(props: {
children: (
resource: RouteResourceResponse & {
@@ -144,7 +140,12 @@ type GetDataLoader = () => Promise<{
*
*/
type CreateResourceArg =
- | {| ...RouteResource, maxAge?: number, maxCache?: number, isBrowserOnly?: boolean, |}
+ | {|
+ ...RouteResource,
+ maxAge?: number,
+ maxCache?: number,
+ isBrowserOnly?: boolean,
+ |}
| {|
...$Diff<
RouteResource,
diff --git a/src/index.ts b/src/index.ts
index bf3bbcd9..01b5567d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,7 +3,6 @@ export { createBrowserHistory, createMemoryHistory } from 'history';
export {
Router,
MemoryRouter,
- StaticRouter,
RouterSubscriber,
RouteResourceEnabledSubscriber,
Redirect,
@@ -47,7 +46,6 @@ export {
MatchParams,
MatchedInvariantRoute,
MatchedRoute,
- MemoryRouterProps,
Query,
ResourceStoreContext,
ResourceStoreData,
diff --git a/src/types.js.flow b/src/types.js.flow
index 0e55eacc..187ced8d 100644
--- a/src/types.js.flow
+++ b/src/types.js.flow
@@ -146,7 +146,6 @@ export type InvariantRoutes = InvariantRoute[];
export type Routes = Route[];
type ContainerProps = {|
- isStatic?: boolean,
history: BrowserHistory,
location?: Location,
routes: Routes,
@@ -154,15 +153,6 @@ type ContainerProps = {|
resourceContext?: ResourceStoreContext,
|};
-export type UniversalRouterContainerProps = {|
- isGlobal?: boolean,
- history: BrowserHistory | MemoryHistory,
- location?: Location,
- routes: Routes,
- resourceData?: ResourceStoreData,
- resourceContext?: ResourceStoreContext,
-|};
-
type PublicStateProperties = {|
location: Location,
query: Query,
@@ -175,7 +165,6 @@ type PrivateStateProperties = {|
routes: Routes,
history: BrowserHistory,
unlisten: (() => void) | null,
- isStatic: boolean,
shouldUseSuspense: boolean,
|};
@@ -206,59 +195,31 @@ export type RouterActionsType = {|
getBasePath: () => string,
|};
-// TODO: DRY out all these different router props if possible
export type RouterProps = {
- isStatic?: boolean,
- history?: BrowserHistory,
- resourceContext?: ResourceStoreContext,
- resourceData?: ResourceStoreData,
- routes: Routes,
+ basePath?: string,
children: Node,
+ history?: BrowserHistory,
initialRoute?: Route,
isGlobal?: boolean,
- basePath?: string,
+ location?: string,
onPrefetch?: (routerContext: RouterContext) => void,
-};
-
-export type UniversalRouterProps = {
- isGlobal?: boolean,
- history?: BrowserHistory | MemoryHistory,
resourceContext?: ResourceStoreContext,
resourceData?: ResourceStoreData,
routes: Routes,
- children: Node,
- initialRoute?: Route,
- onPrefetch?: (routerContext: RouterContext) => void,
};
export type MemoryRouterProps = {
- isStatic?: boolean,
- isGlobal?: boolean,
- location?: string,
- routes?: Routes,
- children: Node,
- resourceData?: ResourceStoreData,
- resourceContext?: ResourceStoreContext,
- initialRoute?: Route,
basePath?: string,
- onPrefetch?: (routerContext: RouterContext) => void,
-};
-
-export type StaticRouterProps = {
- isStatic?: boolean,
location?: string,
- routes: Routes,
+ routes?: Routes,
children: Node,
- resourceData?: ResourceStoreData,
- resourceContext?: ResourceStoreContext,
- initialRoute?: Route,
- basePath?: string,
};
export type RequestResourcesParams = {
+ history?: BrowserHistory,
location: string,
- routes: Routes,
resourceContext?: ResourceStoreContext,
+ routes: Routes,
timeout?: number,
};