diff --git a/examples/basic-routing/index.tsx b/examples/basic-routing/index.tsx
index cc5139f7..91c9b358 100644
--- a/examples/basic-routing/index.tsx
+++ b/examples/basic-routing/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
import { About } from './about';
import { Home } from './home';
@@ -42,4 +42,9 @@ const App = () => {
);
};
-render(, document.getElementById('root'));
+const container = document.getElementById('root');
+if (!container)
+ throw new Error('No root element found to render basic routing example');
+
+const root = createRoot(container);
+root.render();
diff --git a/examples/hash-routing/index.tsx b/examples/hash-routing/index.tsx
index 2faaccd3..a93b4cc0 100644
--- a/examples/hash-routing/index.tsx
+++ b/examples/hash-routing/index.tsx
@@ -1,7 +1,7 @@
import { createHashHistory as createHashHistory4 } from 'history';
import { createHashHistory as createHashHistory5 } from 'history-5';
import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
import { About } from './about';
import { Home } from './home';
@@ -42,4 +42,9 @@ const App = () => {
);
};
-render(, document.getElementById('root'));
+const container = document.getElementById('root');
+if (!container)
+ throw new Error('No root element found to render hash routing example');
+
+const root = createRoot(container);
+root.render();
diff --git a/examples/hooks/index.tsx b/examples/hooks/index.tsx
index 5ec9aed6..9b93f5e5 100644
--- a/examples/hooks/index.tsx
+++ b/examples/hooks/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
import { shouldReloadWhenRouteMatchChanges } from '../../src/utils';
@@ -68,4 +68,9 @@ const App = () => {
);
};
-render(, document.getElementById('root'));
+const container = document.getElementById('root');
+if (!container)
+ throw new Error('No root element found to render hooks example');
+
+const root = createRoot(container);
+root.render();
diff --git a/examples/hydration/index.tsx b/examples/hydration/index.tsx
index e353facf..ef23a3d9 100644
--- a/examples/hydration/index.tsx
+++ b/examples/hydration/index.tsx
@@ -1,6 +1,6 @@
import { createMemoryHistory } from 'history';
import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
import { defaultRegistry } from 'react-sweet-state';
import { homeRoute } from './routes';
@@ -53,7 +53,12 @@ const main = async () => {
);
};
- render(, document.getElementById('root'));
+ const container = document.getElementById('root');
+ if (!container)
+ throw new Error('No root element found to render hydration example');
+
+ const root = createRoot(container);
+ root.render();
};
main();
diff --git a/examples/routing-with-resources/index.tsx b/examples/routing-with-resources/index.tsx
index 9eb45fed..0ec5ea86 100644
--- a/examples/routing-with-resources/index.tsx
+++ b/examples/routing-with-resources/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
import { homeRoute, aboutRoute } from './routes';
@@ -28,4 +28,9 @@ const App = () => {
);
};
-render(, document.getElementById('root'));
+const container = document.getElementById('root');
+if (!container)
+ throw new Error('No root element found to render resources example');
+
+const root = createRoot(container);
+root.render();
diff --git a/src/__tests__/integration.test.tsx b/src/__tests__/integration.test.tsx
index 985a06e3..689c61a9 100644
--- a/src/__tests__/integration.test.tsx
+++ b/src/__tests__/integration.test.tsx
@@ -1,7 +1,7 @@
import { act, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { createMemoryHistory } from 'history';
-import React from 'react';
+import React, { StrictMode } from 'react';
import { defaultRegistry } from 'react-sweet-state';
import { isServerEnvironment } from '../common/utils/is-server-environment';
@@ -23,125 +23,56 @@ describe(' client-side integration tests', () => {
routes,
plugins = [],
location,
+ strictMode,
}: {
routes: Route[];
plugins?: Plugin[];
location?: string;
+ strictMode: boolean;
}) {
const history = createMemoryHistory({
initialEntries: [location || routes[0].path],
});
render(
-
-
-
+ strictMode ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )
);
return { history };
}
- it('renders route', () => {
- const location = '/pathname?search=search#hash=hash';
- const route = {
- component: () =>
test
,
- name: 'mock-route',
- path: location.substring(0, location.indexOf('?')),
- };
+ const strictModeStates = ['on', 'off'];
- mountRouter({ routes: [route] });
+ for (const strictModeState of strictModeStates) {
+ const strictMode = strictModeState === 'on';
- expect(screen.getByText('test')).toBeInTheDocument();
- });
-
- it('triggers plugin.loadRoute when mounted', () => {
- const location = '/pathname?search=search#hash=hash';
- const route = {
- component: () => test
,
- name: 'mock-route',
- path: location.substring(0, location.indexOf('?')),
- };
-
- const plugin: Plugin = {
- id: 'test-plugin',
- routeLoad: jest.fn(),
- };
-
- mountRouter({
- routes: [route],
- plugins: [plugin],
- });
-
- expect(plugin.routeLoad).toBeCalled();
- });
-
- it('renders next route', () => {
- const location = '/pathname?search=search#hash=hash';
- const route = {
- component: () => first route
,
- name: 'mock-route',
- path: location.substring(0, location.indexOf('?')),
- };
-
- const route2 = {
- component: () => second route
,
- name: 'mock-route2',
- path: '/route2',
- };
-
- const { history } = mountRouter({
- routes: [route, route2],
- });
-
- expect(screen.getByText('first route')).toBeInTheDocument();
-
- act(() => {
- history.push('/route2');
- });
-
- expect(screen.getByText('second route')).toBeInTheDocument();
- });
-
- it('triggers plugin.loadRoute after URL change', async () => {
- const location = '/pathname?search=search#hash=hash';
- const route = {
- component: () => first route
,
- name: 'mock-route',
- path: location.substring(0, location.indexOf('?')),
- };
-
- const route2 = {
- component: () => second route
,
- name: 'mock-route2',
- path: '/route2',
- };
-
- const plugin: Plugin = {
- id: 'test-plugin',
- routeLoad: jest.fn(),
- };
-
- const { history } = mountRouter({
- routes: [route, route2],
- plugins: [plugin],
- });
+ it(`renders route: strict mode ${strictModeState}`, () => {
+ const location = '/pathname?search=search#hash=hash';
+ const route = {
+ component: () => test
,
+ name: 'mock-route',
+ path: location.substring(0, location.indexOf('?')),
+ };
- expect(plugin.routeLoad).toBeCalled();
+ mountRouter({ routes: [route], strictMode });
- act(() => {
- history.push('/route2');
+ expect(screen.getByText('test')).toBeInTheDocument();
});
- expect((plugin.routeLoad as any).mock.calls[1][0].context.route).toBe(
- route2
- );
- });
-
- describe('route re-rendering', () => {
- it('route loaded once as URL pathname did not change', () => {
+ it(`triggers plugin.loadRoute when mounted: strict mode ${strictModeState}`, () => {
const location = '/pathname?search=search#hash=hash';
const route = {
- component: () => first route
,
+ component: () => test
,
name: 'mock-route',
path: location.substring(0, location.indexOf('?')),
};
@@ -151,121 +82,241 @@ describe(' client-side integration tests', () => {
routeLoad: jest.fn(),
};
- const { history } = mountRouter({
+ mountRouter({
routes: [route],
plugins: [plugin],
+ strictMode,
});
expect(plugin.routeLoad).toBeCalled();
-
- act(() => {
- history.push('/pathname?search=blah-blah-blah');
- });
-
- expect(plugin.routeLoad).toBeCalledTimes(1);
});
- it('route loads twice as query params change', () => {
+ it(`renders next route: strict mode ${strictModeState}`, () => {
const location = '/pathname?search=search#hash=hash';
const route = {
component: () => first route
,
name: 'mock-route',
- query: ['search'],
path: location.substring(0, location.indexOf('?')),
};
- const plugin: Plugin = {
- id: 'test-plugin',
- routeLoad: jest.fn(),
+ const route2 = {
+ component: () => second route
,
+ name: 'mock-route2',
+ path: '/route2',
};
const { history } = mountRouter({
- routes: [route],
- plugins: [plugin],
- location,
+ routes: [route, route2],
+ strictMode,
});
- expect(plugin.routeLoad).toBeCalled();
+ expect(screen.getByText('first route')).toBeInTheDocument();
act(() => {
- history.push('/pathname?search=blah-blah-blah');
+ history.push('/route2');
});
- expect(plugin.routeLoad).toBeCalledTimes(2);
+ expect(screen.getByText('second route')).toBeInTheDocument();
});
- it('route loads once as defined query param did not change', () => {
- const location = '/pathname?search=search';
+ it(`triggers plugin.loadRoute after URL change : strict mode ${strictModeState}`, async () => {
+ const location = '/pathname?search=search#hash=hash';
const route = {
component: () => first route
,
name: 'mock-route',
- query: ['search'],
path: location.substring(0, location.indexOf('?')),
};
+ const route2 = {
+ component: () => second route
,
+ name: 'mock-route2',
+ path: '/route2',
+ };
+
const plugin: Plugin = {
id: 'test-plugin',
routeLoad: jest.fn(),
};
const { history } = mountRouter({
- routes: [route],
+ routes: [route, route2],
plugins: [plugin],
- location,
+ strictMode,
});
expect(plugin.routeLoad).toBeCalled();
act(() => {
- history.push('/pathname?search=search&issue-key=1');
+ history.push('/route2');
});
- expect(plugin.routeLoad).toBeCalledTimes(1);
+ expect((plugin.routeLoad as any).mock.calls[1][0].context.route).toBe(
+ route2
+ );
});
- });
-});
-describe(' server-side integration tests', () => {
- const route = {
- component: () => <>route component>,
- name: '',
- path: '/path',
- };
+ describe(`route re-rendering: strict mode ${strictModeState}`, () => {
+ it(`route loaded once as URL pathname did not change: strict mode ${strictModeState}`, () => {
+ const location = '/pathname?search=search#hash=hash';
+ const route = {
+ component: () => first route
,
+ name: 'mock-route',
+ path: location.substring(0, location.indexOf('?')),
+ };
+
+ const plugin: Plugin = {
+ id: 'test-plugin',
+ routeLoad: jest.fn(),
+ };
+
+ const { history } = mountRouter({
+ routes: [route],
+ plugins: [plugin],
+ strictMode,
+ });
+
+ expect(plugin.routeLoad).toBeCalled();
+
+ act(() => {
+ history.push('/pathname?search=blah-blah-blah');
+ });
+
+ expect(plugin.routeLoad).toBeCalledTimes(1);
+ });
- beforeEach(() => {
- (isServerEnvironment as any).mockReturnValue(true);
- });
+ it(`route loads twice as query params change: strict mode ${strictModeState}`, () => {
+ const location = '/pathname?search=search#hash=hash';
+ const route = {
+ component: () => first route
,
+ name: 'mock-route',
+ query: ['search'],
+ path: location.substring(0, location.indexOf('?')),
+ };
+
+ const plugin: Plugin = {
+ id: 'test-plugin',
+ routeLoad: jest.fn(),
+ };
+
+ const { history } = mountRouter({
+ routes: [route],
+ plugins: [plugin],
+ location,
+ strictMode,
+ });
+
+ expect(plugin.routeLoad).toBeCalled();
+
+ act(() => {
+ history.push('/pathname?search=blah-blah-blah');
+ });
+
+ expect(plugin.routeLoad).toBeCalledTimes(2);
+ });
- it('renders the expected route when basePath is set', () => {
- render(
-
-
-
- );
+ it(`route loads once as defined query param did not change: strict mode ${strictModeState}`, () => {
+ const location = '/pathname?search=search';
+ const route = {
+ component: () => first route
,
+ name: 'mock-route',
+ query: ['search'],
+ path: location.substring(0, location.indexOf('?')),
+ };
+
+ const plugin: Plugin = {
+ id: 'test-plugin',
+ routeLoad: jest.fn(),
+ };
+
+ const { history } = mountRouter({
+ routes: [route],
+ plugins: [plugin],
+ location,
+ strictMode,
+ });
+
+ expect(plugin.routeLoad).toBeCalled();
+
+ act(() => {
+ history.push('/pathname?search=search&issue-key=1');
+ });
+
+ expect(plugin.routeLoad).toBeCalledTimes(1);
+ });
+ });
- expect(screen.getByText('route component')).toBeInTheDocument();
- });
+ describe(` server-side integration tests: strict mode ${strictModeState}`, () => {
+ const route = {
+ component: () => <>route component>,
+ name: '',
+ path: '/path',
+ };
- it('renders the expected route when basePath is not set', () => {
- render(
-
-
-
- );
+ beforeEach(() => {
+ (isServerEnvironment as any).mockReturnValue(true);
+ });
- expect(screen.getByText('route component')).toBeInTheDocument();
- });
+ it(`renders the expected route when basePath is set: strict mode ${strictModeState}`, () => {
+ render(
+ strictMode ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ );
+
+ expect(screen.getByText('route component')).toBeInTheDocument();
+ });
+
+ it(`renders the expected route when basePath is not set: strict mode ${strictModeState}`, () => {
+ render(
+ strictMode ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ );
+
+ expect(screen.getByText('route component')).toBeInTheDocument();
+ });
+ });
+ }
});
diff --git a/src/controllers/router/index.tsx b/src/controllers/router/index.tsx
index 22acf3e6..d309d93b 100644
--- a/src/controllers/router/index.tsx
+++ b/src/controllers/router/index.tsx
@@ -15,13 +15,13 @@ export const Router = ({
onPrefetch,
routes,
}: RouterProps) => {
- useEffect(() => {
- const { unlisten } = getRouterState();
+ const { unlisten } = getRouterState();
+ useEffect(() => {
return () => {
unlisten && unlisten();
};
- }, []);
+ }, [unlisten]);
return (