Skip to content

Commit d4869bf

Browse files
committed
client/login: Port to React 19 useActionState
This will make it slightly cleaner, allowing us to use `async`/`await`. https://react.dev/blog/2024/12/05/react-19#new-hook-useactionstate
1 parent cbcad27 commit d4869bf

File tree

1 file changed

+49
-71
lines changed

1 file changed

+49
-71
lines changed

client/js/templates/LoginForm.jsx

Lines changed: 49 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import React, { useCallback, useContext, useState } from 'react';
1+
import React, {
2+
startTransition,
3+
useCallback,
4+
useContext,
5+
useActionState,
6+
} from 'react';
27
import PropTypes from 'prop-types';
38
import classNames from 'classnames';
49
import { SpinnerBig } from './Spinner';
@@ -7,95 +12,73 @@ import { HttpError, LoginError } from '../errors';
712
import { ConfigurationContext } from '../helpers/configuration';
813
import { LocalizationContext } from '../helpers/i18n';
914

10-
function handleLogIn({
11-
event,
15+
async function handleLogIn({
1216
configuration,
1317
navigate,
14-
setLoading,
1518
username,
1619
password,
1720
enableOffline,
1821
returnLocation,
1922
}) {
20-
event.preventDefault();
21-
22-
setLoading(true);
23-
24-
selfoss
25-
.login({ configuration, username, password, enableOffline })
26-
.then(() => {
27-
navigate(returnLocation);
28-
})
29-
.catch((err) => {
30-
const message =
31-
err instanceof LoginError
32-
? selfoss.app._('login_invalid_credentials')
33-
: selfoss.app._('login_error_generic', {
34-
errorMessage:
35-
err instanceof HttpError
36-
? `HTTP ${err.response.status} ${err.message}`
37-
: err.message,
38-
});
39-
navigate('/sign/in', {
40-
replace: true,
41-
state: {
42-
error: message,
43-
},
44-
});
45-
})
46-
.finally(() => {
47-
setLoading(false);
23+
try {
24+
await selfoss.login({
25+
configuration,
26+
username,
27+
password,
28+
enableOffline,
29+
});
30+
navigate(returnLocation);
31+
} catch (err) {
32+
const message =
33+
err instanceof LoginError
34+
? selfoss.app._('login_invalid_credentials')
35+
: selfoss.app._('login_error_generic', {
36+
errorMessage:
37+
err instanceof HttpError
38+
? `HTTP ${err.response.status} ${err.message}`
39+
: err.message,
40+
});
41+
navigate('/sign/in', {
42+
replace: true,
43+
state: {
44+
error: message,
45+
},
4846
});
47+
}
4948
}
5049

5150
export default function LoginForm({ offlineEnabled }) {
52-
const [username, setUsername] = useState('');
53-
const [password, setPassword] = useState('');
54-
const [loading, setLoading] = useState(false);
55-
const [enableOffline, setEnableOffline] = useState(offlineEnabled);
56-
5751
const configuration = useContext(ConfigurationContext);
5852
const navigate = useNavigate();
5953
const location = useLocation();
6054
const error = location?.state?.error;
6155
const returnLocation = location?.state?.returnLocation ?? '/';
6256

63-
const formOnSubmit = useCallback(
64-
(event) =>
65-
handleLogIn({
66-
event,
57+
const [, submitAction, loading] = useActionState(
58+
async (_previousState, formData) => {
59+
const username = formData.get('username');
60+
const password = formData.get('password');
61+
const enableOffline = formData.get('enableoffline');
62+
await handleLogIn({
6763
configuration,
6864
navigate,
69-
setLoading,
7065
username,
7166
password,
7267
enableOffline,
7368
returnLocation,
74-
}),
75-
[
76-
configuration,
77-
navigate,
78-
username,
79-
password,
80-
enableOffline,
81-
returnLocation,
82-
],
83-
);
84-
85-
const usernameOnChange = useCallback(
86-
(event) => setUsername(event.target.value),
87-
[],
88-
);
89-
90-
const passwordOnChange = useCallback(
91-
(event) => setPassword(event.target.value),
92-
[],
69+
});
70+
return null;
71+
},
72+
null,
9373
);
9474

95-
const offlineOnChange = useCallback(
96-
(event) => setEnableOffline(event.target.checked),
97-
[setEnableOffline],
98-
);
75+
const formOnSubmit = useCallback((event) => {
76+
// Unlike `action` prop, `onSubmit` avoids clearing the form on submit.
77+
// https://github.com/facebook/react/issues/29034#issuecomment-2143595195
78+
event.preventDefault();
79+
const formData = new FormData(event.target);
80+
startTransition(() => submitAction(formData));
81+
}, []);
9982

10083
const _ = useContext(LocalizationContext);
10184

@@ -120,8 +103,6 @@ export default function LoginForm({ offlineEnabled }) {
120103
id="username"
121104
accessKey="u"
122105
autoComplete="username"
123-
onChange={usernameOnChange}
124-
value={username}
125106
autoFocus
126107
required
127108
/>
@@ -134,8 +115,6 @@ export default function LoginForm({ offlineEnabled }) {
134115
id="password"
135116
accessKey="p"
136117
autoComplete="current-password"
137-
onChange={passwordOnChange}
138-
value={password}
139118
/>
140119
</li>
141120
<li>
@@ -148,8 +127,7 @@ export default function LoginForm({ offlineEnabled }) {
148127
name="enableoffline"
149128
id="enableoffline"
150129
accessKey="o"
151-
onChange={offlineOnChange}
152-
checked={enableOffline}
130+
defaultChecked={offlineEnabled}
153131
/>{' '}
154132
<span className="badge-experimental">
155133
{_('experimental')}

0 commit comments

Comments
 (0)