Skip to content

Commit

Permalink
client: Upgrade to react-router 6
Browse files Browse the repository at this point in the history
https://remix.run/blog/react-router-v6
https://reactrouter.com/6.28.1/upgrading/v5

- Explicit `History` object manipulation was replaced with `navigate` function.
- Similarly `Redirect` component was replaced with `Navigate` component.
  Also it now defaults to `push` so had to switch to `replace`.
- Regex path had to be replaced with a custom validation logic (see `EntriesFilter` component):
  https://reactrouter.com/6.28.1/start/faq#what-happened-to-regexp-routes-paths
- `Route` `path`s need to be relative.
- `Link`’s `to` prop no longer accepts callback or state.
- `Prompt` component was replaced with an unstable hook.
  Like before, it still does not handle `beforeunload` event.
  https://reactrouter.com/6.28.1/hooks/use-prompt#unstable_useprompt
  But it only works with data routers:
  https://reactrouter.com/6.28.1/routers/picking-a-router
  Yuck, I do not want framework, I just want a routing library.
  Dropping the feature for now.
  • Loading branch information
jtojnar committed Dec 29, 2024
1 parent 4d4bdd8 commit 88deede
Show file tree
Hide file tree
Showing 18 changed files with 333 additions and 347 deletions.
34 changes: 29 additions & 5 deletions client/js/helpers/uri.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { generatePath } from 'react-router-dom';
import { useLocation, useMatch } from 'react-router-dom';
import { FilterType } from '../Filter';

/**
Expand Down Expand Up @@ -36,9 +36,28 @@ export function filterTypeToString(type) {
return 'starred';
}
}
function generatePath({ filter, category, id }) {
return `/${filter}/${category}${id ? `/${id}` : ''}`;
}

export function useEntriesParams() {
const match = useMatch(':filter/:category/:id?');

if (match === null) {
return null;
}

const { params } = match;
const filterValid = /^(newest|unread|starred)$/.test(params.filter);
const categoryValid = /^(all|tag-.+|source-[0-9]+)$/.test(params.category);
const idValid = params.id === undefined || /^\d+$/.test(params.id);

export const ENTRIES_ROUTE_PATTERN =
'/:filter(newest|unread|starred)/:category(all|tag-[^/]+|source-[0-9]+)/:id?';
if (!filterValid || !idValid || !categoryValid) {
return null;
}

return params;
}

export function makeEntriesLinkLocation(
location,
Expand All @@ -50,13 +69,13 @@ export function makeEntriesLinkLocation(
if (location.pathname.match(/^\/(newest|unread|starred)\//) !== null) {
const [, ...segments] = location.pathname.split('/');

path = generatePath(ENTRIES_ROUTE_PATTERN, {
path = generatePath({
filter: filter ?? segments[0],
category: category ?? segments[1],
id: typeof id !== 'undefined' ? id : segments[2],
});
} else {
path = generatePath(ENTRIES_ROUTE_PATTERN, {
path = generatePath({
// TODO: change default from config
filter: filter ?? 'unread',
category: category ?? 'all',
Expand Down Expand Up @@ -92,3 +111,8 @@ export function forceReload(location) {
forceReload: (state.forceReload ?? 0) + 1,
};
}

export function useForceReload() {
const location = useLocation();
return forceReload(location);
}
2 changes: 1 addition & 1 deletion client/js/selfoss-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const selfoss = {
await logout();

if (!selfoss.config.publicMode) {
selfoss.history.push('/sign/in');
selfoss.navigate('/sign/in');
}
} catch (error) {
selfoss.app.showError(
Expand Down
118 changes: 80 additions & 38 deletions client/js/templates/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import PropTypes from 'prop-types';
import nullable from 'prop-types-nullable';
import {
BrowserRouter as Router,
Switch,
Routes,
Route,
Link,
Redirect,
useHistory,
Navigate,
useNavigate,
useLocation,
} from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
Expand All @@ -25,11 +25,11 @@ import * as icons from '../icons';
import { useAllowedToRead, useAllowedToWrite } from '../helpers/authorizations';
import { ConfigurationContext } from '../helpers/configuration';
import { useIsSmartphone, useListenableValue } from '../helpers/hooks';
import { ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import { i18nFormat, LocalizationContext } from '../helpers/i18n';
import { LoadingState } from '../requests/LoadingState';
import * as sourceRequests from '../requests/sources';
import locales from '../locales';
import { useEntriesParams } from '../helpers/uri';

function handleNavToggle({ event, setNavExpanded }) {
event.preventDefault();
Expand Down Expand Up @@ -91,12 +91,12 @@ function NotFound() {
}

function CheckAuthorization({ isAllowed, returnLocation, _, children }) {
const history = useHistory();
const navigate = useNavigate();
if (!isAllowed) {
const [preLink, inLink, postLink] = _('error_unauthorized').split(
/\{(?:link_begin|link_end)\}/,
);
history.push('/sign/in', {
navigate('/sign/in', {
returnLocation,
});

Expand Down Expand Up @@ -153,10 +153,10 @@ function PureApp({
}, []);

// TODO: move stuff that depends on this to the App.
const history = useHistory();
const navigate = useNavigate();
useEffect(() => {
selfoss.history = history;
}, [history]);
selfoss.navigate = navigate;
}, [navigate]);

// Prepare path of the homepage for redirecting from /
const homePagePath = configuration.homepage.split('/');
Expand Down Expand Up @@ -191,20 +191,20 @@ function PureApp({
<React.StrictMode>
<Message message={globalMessage} />

<Switch>
<Routes>
<Route
path="/sign/in"
render={() => (
element={
/* menu open for smartphone */
<div id="loginform" role="main">
<LoginForm {...{ offlineEnabled }} />
</div>
)}
}
/>

<Route
path="/password"
render={() => (
element={
<CheckAuthorization
isAllowed={isAllowedToWrite}
returnLocation="/password"
Expand All @@ -214,12 +214,12 @@ function PureApp({
<HashPassword setTitle={setTitle} />
</div>
</CheckAuthorization>
)}
}
/>

<Route
path="/opml"
render={() => (
element={
<CheckAuthorization
isAllowed={isAllowedToWrite}
returnLocation="/opml"
Expand All @@ -229,12 +229,12 @@ function PureApp({
<OpmlImport setTitle={setTitle} />
</main>
</CheckAuthorization>
)}
}
/>

<Route
path="/"
render={() => (
path="*"
element={
<CheckAuthorization isAllowed={isAllowedToRead} _={_}>
<div id="nav-mobile" role="navigation">
<div id="nav-mobile-logo">
Expand Down Expand Up @@ -322,21 +322,21 @@ function PureApp({

{/* content */}
<div id="content" role="main">
<Switch>
<Routes>
<Route
exact
path="/"
render={() => (
<Redirect
element={
<Navigate
to={`/${homePagePath.join('/')}`}
replace
/>
)}
}
/>
<Route
path={ENTRIES_ROUTE_PATTERN}
render={() => (
<EntriesPage
ref={entriesRef}
path="/:filter/:category/:id?"
element={
<EntriesFilter
entriesRef={entriesRef}
setNavExpanded={setNavExpanded}
configuration={configuration}
navSourcesExpanded={
Expand All @@ -349,22 +349,19 @@ function PureApp({
setGlobalUnreadCount
}
/>
)}
/>
<Route
path="/manage/sources"
render={() => <SourcesPage />}
}
/>
<Route
path="*"
render={() => <NotFound />}
path="/manage/sources/add?"
element={<SourcesPage />}
/>
</Switch>
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</CheckAuthorization>
)}
}
/>
</Switch>
</Routes>
</React.StrictMode>
);
}
Expand All @@ -388,6 +385,43 @@ PureApp.propTypes = {
reloadAll: PropTypes.func.isRequired,
};

// Work around for regex patterns not being supported
// https://github.com/remix-run/react-router/issues/8254
function EntriesFilter({
entriesRef,
setNavExpanded,
configuration,
navSourcesExpanded,
unreadItemsCount,
setGlobalUnreadCount,
}) {
const params = useEntriesParams();

if (params === null) {
return <NotFound />;
}

return (
<EntriesPage
ref={entriesRef}
setNavExpanded={setNavExpanded}
configuration={configuration}
navSourcesExpanded={navSourcesExpanded}
unreadItemsCount={unreadItemsCount}
setGlobalUnreadCount={setGlobalUnreadCount}
/>
);
}

EntriesFilter.propTypes = {
entriesRef: PropTypes.func.isRequired,
configuration: PropTypes.object.isRequired,
setNavExpanded: PropTypes.func.isRequired,
navSourcesExpanded: PropTypes.bool.isRequired,
setGlobalUnreadCount: PropTypes.func.isRequired,
unreadItemsCount: PropTypes.number.isRequired,
};

export default class App extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -818,7 +852,15 @@ App.propTypes = {
*/
export function createApp({ basePath, appRef, configuration }) {
return (
<Router basename={basePath}>
<Router
basename={basePath}
future={{
// eslint-disable-next-line camelcase
v7_startTransition: true,
// eslint-disable-next-line camelcase
v7_relativeSplatPath: true,
}}
>
<App ref={appRef} configuration={configuration} />
</Router>
);
Expand Down
Loading

0 comments on commit 88deede

Please sign in to comment.