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 (