diff --git a/client/js/selfoss-base.js b/client/js/selfoss-base.js
index ad5b5d45a..10336b42b 100644
--- a/client/js/selfoss-base.js
+++ b/client/js/selfoss-base.js
@@ -152,6 +152,10 @@ const selfoss = {
basePath,
appRef: (app) => {
selfoss.app = app;
+
+ return () => {
+ selfoss.app = null;
+ };
},
configuration,
}),
diff --git a/client/js/templates/App.jsx b/client/js/templates/App.jsx
index cc74753e8..92f498174 100644
--- a/client/js/templates/App.jsx
+++ b/client/js/templates/App.jsx
@@ -172,6 +172,11 @@ function PureApp({
const entriesRef = useCallback((entriesPage) => {
setEntriesPage(entriesPage);
selfoss.entriesPage = entriesPage;
+
+ return () => {
+ setEntriesPage(null);
+ selfoss.entriesPage = null;
+ };
}, []);
const [title, setTitle] = useState(null);
@@ -812,8 +817,8 @@ export default class App extends React.Component {
render() {
return (
-
-
+
+
-
-
+
+
);
}
}
diff --git a/client/js/templates/EntriesPage.jsx b/client/js/templates/EntriesPage.jsx
index 2cb67840c..1a5f6cc80 100644
--- a/client/js/templates/EntriesPage.jsx
+++ b/client/js/templates/EntriesPage.jsx
@@ -4,7 +4,6 @@ import React, {
useEffect,
useMemo,
useState,
- forwardRef,
} from 'react';
import PropTypes from 'prop-types';
import { Link, useLocation, useParams } from 'react-router';
@@ -1255,16 +1254,14 @@ StateHolder.propTypes = {
unreadItemsCount: PropTypes.number.isRequired,
};
-const StateHolderOuter = forwardRef(function StateHolderOuter(
- {
- configuration,
- setNavExpanded,
- navSourcesExpanded,
- setGlobalUnreadCount,
- unreadItemsCount,
- },
+export default function StateHolderOuter({
+ configuration,
+ setNavExpanded,
+ navSourcesExpanded,
+ setGlobalUnreadCount,
+ unreadItemsCount,
ref,
-) {
+}) {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
@@ -1282,14 +1279,13 @@ const StateHolderOuter = forwardRef(function StateHolderOuter(
unreadItemsCount={unreadItemsCount}
/>
);
-});
+}
StateHolderOuter.propTypes = {
+ ref: 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 StateHolderOuter;
diff --git a/client/js/templates/HashPassword.jsx b/client/js/templates/HashPassword.jsx
index 5b2047bce..24746e65f 100644
--- a/client/js/templates/HashPassword.jsx
+++ b/client/js/templates/HashPassword.jsx
@@ -1,45 +1,48 @@
import PropTypes from 'prop-types';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+ startTransition,
+ useActionState,
+ useCallback,
+ useEffect,
+} from 'react';
import { useNavigate } from 'react-router';
-import { useInput } from 'rooks';
-import { LoadingState } from '../requests/LoadingState';
import { HttpError } from '../errors';
import { hashPassword } from '../requests/common';
export default function HashPassword({ setTitle }) {
- const [state, setState] = useState(LoadingState.INITIAL);
- const [hashedPassword, setHashedPassword] = useState('');
- const [error, setError] = useState(null);
- const passwordEntry = useInput('');
-
const navigate = useNavigate();
+ const [
+ /** @type {({} | { hashedPassword: string } | { error: Error })} */
+ state,
+ submitAction,
+ isPending,
+ ] = useActionState(async (_previousState, formData) => {
+ try {
+ const password = formData.get('password').trim();
+ const hashedPassword = await hashPassword(password);
+ return { hashedPassword };
+ } catch (error) {
+ if (error instanceof HttpError && error.response.status === 403) {
+ navigate('/sign/in', {
+ error: 'Generating a new password hash requires being logged in or not setting “password” in selfoss configuration.',
+ returnLocation: '/password',
+ });
+ return {};
+ }
+ return { error };
+ }
+ }, {});
+
const submit = useCallback(
(event) => {
+ // Unlike `action` prop, `onSubmit` avoids clearing the form on submit.
+ // https://github.com/facebook/react/issues/29034#issuecomment-2143595195
event.preventDefault();
-
- setState(LoadingState.LOADING);
- hashPassword(passwordEntry.value.trim())
- .then((hashedPassword) => {
- setHashedPassword(hashedPassword);
- setState(LoadingState.SUCCESS);
- })
- .catch((error) => {
- if (
- error instanceof HttpError &&
- error.response.status === 403
- ) {
- navigate('/sign/in', {
- error: 'Generating a new password hash requires being logged in or not setting “password” in selfoss configuration.',
- returnLocation: '/password',
- });
- return;
- }
- setError(error);
- setState(LoadingState.ERROR);
- });
+ const formData = new FormData(event.target);
+ startTransition(() => submitAction(formData));
},
- [navigate, passwordEntry.value],
+ [submitAction],
);
useEffect(() => {
@@ -50,22 +53,21 @@ export default function HashPassword({ setTitle }) {
};
}, [setTitle]);
- const message =
- state === LoadingState.SUCCESS ? (
-
-
-
- ) : state === LoadingState.ERROR ? (
-
- Unexpected happened.
-
- ${JSON.stringify(error)}
-
-
- ) : null;
+ const message = isPending ? null : 'hashedPassword' in state ? (
+
+
+
+ ) : 'error' in state ? (
+
+ Unexpected happened.
+
+ ${JSON.stringify(state.error)}
+
+
+ ) : null;
return (