diff --git a/example/index.jsx b/example/index.jsx
index ff5513e67..52b37aec5 100644
--- a/example/index.jsx
+++ b/example/index.jsx
@@ -7,10 +7,11 @@ import {
AppProvider,
AuthenticatedPageRoute,
ErrorPage,
- PageRoute,
+ PageWrap,
} from '@edx/frontend-platform/react';
import { APP_INIT_ERROR, APP_READY, initialize } from '@edx/frontend-platform';
import { subscribe } from '@edx/frontend-platform/pubSub';
+import { Routes, Route } from 'react-router-dom';
import './index.scss';
import ExamplePage from './ExamplePage';
@@ -19,13 +20,14 @@ import AuthenticatedPage from './AuthenticatedPage';
subscribe(APP_READY, () => {
ReactDOM.render(
-
- }
- />
-
+
+ } />
+ }
+ />
+ } />
+
,
document.getElementById('root'),
);
diff --git a/package-lock.json b/package-lock.json
index 2226df9c5..7729ead8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,7 +50,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-redux": "7.2.9",
- "react-router-dom": "5.3.4",
+ "react-router-dom": "^6.6.1",
"redux": "4.2.1",
"regenerator-runtime": "0.13.11"
},
@@ -60,7 +60,7 @@
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-redux": "^7.1.1",
- "react-router-dom": "^5.0.1",
+ "react-router-dom": "^6.0.0",
"redux": "^4.0.4"
}
},
@@ -3589,6 +3589,15 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.2.1.tgz",
+ "integrity": "sha512-XiY0IsyHR+DXYS5vBxpoBe/8veTeoRpMHP+vDosLZxL5bnpetzI0igkxkLZS235ldLzyfkxF+2divEwWHP3vMQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
@@ -15028,15 +15037,6 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
- "node_modules/path-to-regexp": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
- "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
- "dev": true,
- "dependencies": {
- "isarray": "0.0.1"
- }
- },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -16849,41 +16849,35 @@
}
},
"node_modules/react-router": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
- "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.6.1.tgz",
+ "integrity": "sha512-YkvlYRusnI/IN0kDtosUCgxqHeulN5je+ew8W+iA1VvFhf86kA+JEI/X/8NqYcr11hCDDp906S+SGMpBheNeYQ==",
"dev": true,
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "hoist-non-react-statics": "^3.1.0",
- "loose-envify": "^1.3.1",
- "path-to-regexp": "^1.7.0",
- "prop-types": "^15.6.2",
- "react-is": "^16.6.0",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8"
}
},
"node_modules/react-router-dom": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
- "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.6.1.tgz",
+ "integrity": "sha512-u+8BKUtelStKbZD5UcY0NY90WOzktrkJJhyhNg7L0APn9t1qJNLowzrM9CHdpB6+rcPt6qQrlkIXsTvhuXP68g==",
"dev": true,
"dependencies": {
- "@babel/runtime": "^7.12.13",
- "history": "^4.9.0",
- "loose-envify": "^1.3.1",
- "prop-types": "^15.6.2",
- "react-router": "5.3.4",
- "tiny-invariant": "^1.0.2",
- "tiny-warning": "^1.0.0"
+ "@remix-run/router": "1.2.1",
+ "react-router": "6.6.1"
+ },
+ "engines": {
+ "node": ">=14"
},
"peerDependencies": {
- "react": ">=15"
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
}
},
"node_modules/react-style-singleton": {
diff --git a/package.json b/package.json
index 640df90cf..de0b24188 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"react-redux": "7.2.9",
- "react-router-dom": "5.3.4",
+ "react-router-dom": "^6.6.1",
"redux": "4.2.1",
"regenerator-runtime": "0.13.11"
},
@@ -79,7 +79,7 @@
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-redux": "^7.1.1",
- "react-router-dom": "^5.0.1",
+ "react-router-dom": "^6.0.0",
"redux": "^4.0.4"
}
}
diff --git a/src/initialize.js b/src/initialize.js
index 7af609398..4c9b06156 100644
--- a/src/initialize.js
+++ b/src/initialize.js
@@ -11,19 +11,19 @@
* APP_READY,
* subscribe,
* } from '@edx/frontend-platform';
- * import { AppProvider, ErrorPage, PageRoute } from '@edx/frontend-platform/react';
+ * import { AppProvider, ErrorPage, PageWrap } from '@edx/frontend-platform/react';
* import React from 'react';
* import ReactDOM from 'react-dom';
- * import { Switch } from 'react-router-dom';
+ * import { Routes, Route } from 'react-router-dom';
*
* subscribe(APP_READY, () => {
* ReactDOM.render(
*
*
*
- *
- *
- *
+ *
+ * } />
+ *
*
*
* ,
diff --git a/src/react/AppProvider.jsx b/src/react/AppProvider.jsx
index 8255ab405..4743fdfa0 100644
--- a/src/react/AppProvider.jsx
+++ b/src/react/AppProvider.jsx
@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import PropTypes from 'prop-types';
-import { Router } from 'react-router-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
import OptionalReduxProvider from './OptionalReduxProvider';
@@ -10,7 +10,6 @@ import { useAppEvent, useTrackColorSchemeChoice } from './hooks';
import { getAuthenticatedUser, AUTHENTICATED_USER_CHANGED } from '../auth';
import { getConfig } from '../config';
import { CONFIG_CHANGED } from '../constants';
-import { history } from '../initialize';
import {
getLocale,
getMessages,
@@ -44,7 +43,7 @@ import {
* @param {Object} [props.store] A redux store.
* @memberof module:React
*/
-export default function AppProvider({ store, children }) {
+export default function AppProvider({ store, children, wrapWithRouter }) {
const [config, setConfig] = useState(getConfig());
const [authenticatedUser, setAuthenticatedUser] = useState(getAuthenticatedUser());
const [locale, setLocale] = useState(getLocale());
@@ -72,9 +71,11 @@ export default function AppProvider({ store, children }) {
value={appContextValue}
>
-
- {children}
-
+ {wrapWithRouter ? (
+
+ {children}
+
+ ) : children}
@@ -86,8 +87,10 @@ AppProvider.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
store: PropTypes.object,
children: PropTypes.node.isRequired,
+ wrapWithRouter: PropTypes.bool,
};
AppProvider.defaultProps = {
store: null,
+ wrapWithRouter: true,
};
diff --git a/src/react/AppProvider.test.jsx b/src/react/AppProvider.test.jsx
index 67ab6910b..231d2f0c3 100644
--- a/src/react/AppProvider.test.jsx
+++ b/src/react/AppProvider.test.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { createStore } from 'redux';
import { mount } from 'enzyme';
+import { BrowserRouter as Router } from 'react-router-dom';
import AppProvider from './AppProvider';
import { initialize } from '../initialize';
@@ -48,7 +49,7 @@ describe('AppProvider', () => {
});
});
- it('should render its children', () => {
+ it('should render its children with a router', () => {
const component = (
state)}>
Child One
@@ -58,6 +59,26 @@ describe('AppProvider', () => {
const wrapper = mount(component);
const list = wrapper.find('div');
+ expect(wrapper.find(Router).length).toEqual(1);
+ expect(list.length).toEqual(2);
+ expect(list.at(0).text()).toEqual('Child One');
+ expect(list.at(1).text()).toEqual('Child Two');
+
+ const reduxProvider = wrapper.find('Provider');
+ expect(reduxProvider.length).toEqual(1);
+ });
+
+ it('should render its children without a router', () => {
+ const component = (
+ state)} wrapWithRouter={false}>
+ Child One
+ Child Two
+
+ );
+
+ const wrapper = mount(component);
+ const list = wrapper.find('div');
+ expect(wrapper.find(Router).length).toEqual(0);
expect(list.length).toEqual(2);
expect(list.at(0).text()).toEqual('Child One');
expect(list.at(1).text()).toEqual('Child Two');
diff --git a/src/react/AuthenticatedPageRoute.jsx b/src/react/AuthenticatedPageRoute.jsx
index 8cd23850e..d67231c8e 100644
--- a/src/react/AuthenticatedPageRoute.jsx
+++ b/src/react/AuthenticatedPageRoute.jsx
@@ -1,9 +1,8 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
-import { useRouteMatch } from 'react-router-dom';
import AppContext from './AppContext';
-import PageRoute from './PageRoute';
+import PageWrap from './PageWrap';
import { getLoginRedirectUrl } from '../auth';
/**
@@ -14,45 +13,34 @@ import { getLoginRedirectUrl } from '../auth';
*
* It can optionally accept an override URL to redirect to instead of the login page.
*
- * Like a `PageRoute`, also calls `sendPageEvent` when the route becomes active.
+ * Like a `PageWrap`, also calls `sendPageEvent` when the route becomes active.
*
- * @see PageRoute
+ * @see PageWrap
* @see {@link module:frontend-platform/analytics~sendPageEvent}
* @memberof module:React
* @param {Object} props
* @param {string} props.redirectUrl The URL anonymous users should be redirected to, rather than
* viewing the route's contents.
*/
-export default function AuthenticatedPageRoute({ redirectUrl, ...props }) {
+export default function AuthenticatedPageRoute({ redirectUrl, children }) {
const { authenticatedUser } = useContext(AppContext);
-
- const match = useRouteMatch({
- // eslint-disable-next-line react/prop-types
- path: props.path,
- // eslint-disable-next-line react/prop-types
- exact: props.exact,
- // eslint-disable-next-line react/prop-types
- strict: props.strict,
- // eslint-disable-next-line react/prop-types
- sensitive: props.sensitive,
- });
-
if (authenticatedUser === null) {
- if (match) {
- const destination = redirectUrl || getLoginRedirectUrl(global.location.href);
- global.location.assign(destination);
- }
- // This emulates a Route's way of displaying nothing if the route's path doesn't match the
- // current URL.
+ const destination = redirectUrl || getLoginRedirectUrl(global.location.href);
+ global.location.assign(destination);
+
return null;
}
+
return (
-
+
+ {children}
+
);
}
AuthenticatedPageRoute.propTypes = {
redirectUrl: PropTypes.string,
+ children: PropTypes.node.isRequired,
};
AuthenticatedPageRoute.defaultProps = {
diff --git a/src/react/AuthenticatedPageRoute.test.jsx b/src/react/AuthenticatedPageRoute.test.jsx
index 1ab90374c..b8d68b0b1 100644
--- a/src/react/AuthenticatedPageRoute.test.jsx
+++ b/src/react/AuthenticatedPageRoute.test.jsx
@@ -1,8 +1,7 @@
/* eslint-disable react/jsx-no-constructed-context-values */
import React from 'react';
import { mount } from 'enzyme';
-import { Router, Route } from 'react-router-dom';
-import { createBrowserHistory } from 'history';
+import { Route, Routes, MemoryRouter } from 'react-router-dom';
import { getAuthenticatedUser, getLoginRedirectUrl } from '../auth';
import AuthenticatedPageRoute from './AuthenticatedPageRoute';
import AppContext from './AppContext';
@@ -14,7 +13,6 @@ jest.mock('../auth');
describe('AuthenticatedPageRoute', () => {
const { location } = global;
- let history;
beforeEach(() => {
delete global.location;
@@ -24,7 +22,6 @@ describe('AuthenticatedPageRoute', () => {
sendPageEvent.mockReset();
getLoginRedirectUrl.mockReset();
getAuthenticatedUser.mockReset();
- history = createBrowserHistory();
});
afterEach(() => {
@@ -41,13 +38,14 @@ describe('AuthenticatedPageRoute', () => {
config: getConfig(),
}}
>
-
- Anonymous
} />
- Authenticated
} />
-
+
+
+ Anonymous
} />
+ Authenticated
} />
+
+
);
- history.push('/authenticated');
global.location.href = 'http://localhost/authenticated';
mount(component);
expect(getLoginRedirectUrl).toHaveBeenCalledWith('http://localhost/authenticated');
@@ -58,6 +56,11 @@ describe('AuthenticatedPageRoute', () => {
it('should redirect to custom redirect URL if not authenticated', () => {
getAuthenticatedUser.mockReturnValue(null);
getLoginRedirectUrl.mockReturnValue('http://localhost/login?next=http%3A%2F%2Flocalhost%2Fauthenticated');
+ const authenticatedElement = (
+
+ Authenticated
+
+ );
const component = (
{
config: getConfig(),
}}
>
-
- Anonymous
} />
- Authenticated
} />
-
+
+
+ Anonymous
} />
+
+
+
);
- history.push('/authenticated');
mount(component);
expect(getLoginRedirectUrl).not.toHaveBeenCalled();
expect(sendPageEvent).not.toHaveBeenCalled();
@@ -88,13 +92,14 @@ describe('AuthenticatedPageRoute', () => {
config: getConfig(),
}}
>
-
- Anonymous
} />
- Authenticated
} />
-
+
+
+ Anonymous
} />
+ Authenticated
} />
+
+
);
- history.push('/');
const wrapper = mount(component);
expect(getLoginRedirectUrl).not.toHaveBeenCalled();
@@ -112,13 +117,14 @@ describe('AuthenticatedPageRoute', () => {
config: getConfig(),
}}
>
-
- Anonymous
} />
- Authenticated
} />
-
+
+
+ Anonymous} />
+ Authenticated
} />
+
+
);
- history.push('/authenticated');
const wrapper = mount(component);
expect(getLoginRedirectUrl).not.toHaveBeenCalled();
expect(global.location.assign).not.toHaveBeenCalled();
diff --git a/src/react/PageRoute.jsx b/src/react/PageRoute.jsx
deleted file mode 100644
index 66ecc1158..000000000
--- a/src/react/PageRoute.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-/* eslint-disable react/prop-types */
-import React, { useEffect } from 'react';
-import { Route, useRouteMatch } from 'react-router-dom';
-import { sendPageEvent } from '../analytics';
-
-/**
- * A react-router Route component that calls `sendPageEvent` when it becomes active.
- *
- * @see {@link module:frontend-platform/analytics~sendPageEvent}
- * @memberof module:React
- * @param {Object} props
- */
-export default function PageRoute(props) {
- const match = useRouteMatch({
- path: props.path,
- exact: props.exact,
- strict: props.strict,
- sensitive: props.sensitive,
- });
-
- useEffect(() => {
- if (match) {
- sendPageEvent();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(match)]);
-
- return (
-
- );
-}
diff --git a/src/react/PageWrap.jsx b/src/react/PageWrap.jsx
new file mode 100644
index 000000000..d7130cd73
--- /dev/null
+++ b/src/react/PageWrap.jsx
@@ -0,0 +1,24 @@
+/* eslint-disable react/prop-types */
+// eslint-disable-next-line no-unused-vars
+import React, { useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { sendPageEvent } from '../analytics';
+
+/**
+ * A Wrapper component that calls `sendPageEvent` when it becomes active.
+ *
+ * @see {@link module:frontend-platform/analytics~sendPageEvent}
+ * @memberof module:React
+ * @param {Object} props
+ */
+export default function PageWrap({ children }) {
+ const location = useLocation();
+
+ useEffect(() => {
+ sendPageEvent();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [location.pathname]);
+
+ return children;
+}
diff --git a/src/react/index.js b/src/react/index.js
index 37efa68b1..0985d9271 100644
--- a/src/react/index.js
+++ b/src/react/index.js
@@ -12,5 +12,5 @@ export { default as AuthenticatedPageRoute } from './AuthenticatedPageRoute';
export { default as ErrorBoundary } from './ErrorBoundary';
export { default as ErrorPage } from './ErrorPage';
export { default as LoginRedirect } from './LoginRedirect';
-export { default as PageRoute } from './PageRoute';
+export { default as PageWrap } from './PageWrap';
export { useAppEvent } from './hooks';