React Admin v3: Zoom in the Authentication Layer
React-admin v3 is already in beta. While we iron out the last details before it can be released as stable, let's explore what changes in this version.
React-admin v3 doesn't bring many new features, because the framework follows Semantic Versioning. New features will come in minor release (like 3.1, 3.2, etc). Major releases are useful for breaking changes, and the introduction of new programming paradigms. In this new version, we focused on improving the developer experience through the use of hooks. And the changes are pretty dramatic.
Let's see how it affects the Authentication utilities - how you restrict access to certain components, how to display specific messages to anonymous users, and how to write a custom Login form.
Restricting Access To A Custom Page
If you add custom pages to your admin, you may want to restrict them to authenticated users only.
In react-admin 2.x, you had to use a component for that, which expected the current location
from react-router:
import { withRouter } from "react-router-dom";
import { Authenticated } from "react-admin";
const MyPage = ({ location }) => (
<Authenticated location={location}>
{" "}
// redirects to login if not authenticated
<div>...</div>
</Authenticated>
);
export default withRouter(MyPage);
<Authenticated>
calls the authProvider
to check if the user is authenticated, and redirects to the login page if the authProvider
returns a rejected Promise.
In react-admin v3, you can now use the new useAuthenticated()
hook instead:
// in src/MyPage.js
import { useAuthenticated } from "react-admin";
const MyPage = () => {
useAuthenticated(); // redirects to login if not authenticated
return <div>...</div>;
};
export default MyPage;
The useAuthenticated
hook is optimistic: it doesn't block rendering during the authProvider
call. In the above example, the MyPage
component renders even before getting the response from the authProvider
. If the call returns a rejected promise, the hook redirects to the login page, but the user may have seen the content of the MyPage
component for a brief moment.
No need to use useAuthenticated
hook in the List
or Edit
pages. React-admin already restricts access to all the Resource and Dashboard pages. You only need to deal with access restriction for your custom pages.
React-admin v3 keeps the <Authenticated>
component to ease the v3 upgrade, and to help restricting access to parts of a component without needing to split it in two.
Displaying Alternative content To Anonymous Users
To avoid rendering a component and force waiting for the authProvider
response, use the useAuthState()
hook instead of the useAuthenticated()
hook. It returns an object with 3 properties:
loading
:true
just after mount, while theauthProvider
is being called.false
once theauthProvider
has answeredloaded
: the opposite ofloading
.authenticated
:undefined
while loading. thentrue
orfalse
depending on theauthProvider
response.
You can then render different content depending on the authenticated status.
import { useAuthState } from 'react-admin';
const MyPage = () => {
const { loading, authenticated } = useAuthState();
if (loading) {
return <Loading>;
}
if (authenticated) {
return <AuthenticatedContent />;
}
return <AnonymousContent />;
};
There was no equivalent of useAuthState
in react-admin v2, so it's a new feature.
Customizing The Login Component
By default, the react-admin Login page asks for a username and a password. But what if you want to use an email instead of a username? What if you want to use a Single-Sign-On (SSO) with a third-party authentication service? What if you want to use two-factor authentication?
For all these cases, it's up to you to implement your own LoginPage
component, which will be displayed under the /login
route instead of the default username/password form.
The implementation of the LoginPage
did change quite a bit. In v2, you had to dispatch a Redux action:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { userLogin } from 'react-admin';
import { MuiThemeProvider } from '@material-ui/core/styles';
class MyLoginPage extends Component {
state = {
email: '',
password: '';
}
setEmail(email) {
this.setState({ email });
}
setPassword(password) {
this.setState({ password })
}
submit(e) {
e.preventDefault();
// Dispatch the userLogin action (injected by connect)
this.props.userLogin(this.state);
}
render() {
return (
<MuiThemeProvider theme={this.props.theme}>
<form onSubmit={this.submit}>
<input name="email" type="email" value={email} onChange={e => this.setEmail(e.target.value)} />
<input name="password" type="password" value={password} onChange={e => this.setPassword(e.target.value)} />
</form>
</MuiThemeProvider>
);
}
};
export default connect(undefined, { userLogin })(MyLoginPage);
In react-admin v3, Redux becomes an implementation detail, and you just need to use the useLogin
hook.
// in src/MyLoginPage.js
import React, { useState } from "react";
import { useLogin, useNotify } from "react-admin";
import { ThemeProvider } from "@material-ui/styles";
const MyLoginPage = ({ theme }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const login = useLogin();
const notify = useNotify();
const submit = e => {
e.preventDefault();
// call the login callback returned by the useLogin hook
login({ email, password }).catch(() => notify("Invalid email or password"));
};
return (
<ThemeProvider theme={theme}>
<form onSubmit={submit}>
<input
name="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
name="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</form>
</ThemeProvider>
);
};
export default MyLoginPage;
You may have noticed that in the v3 version requires a bit more work: You must handle the failure case and show a notification yourself (using the new useNotify
hook). In v2, the login failure notification was handled by react-admin - but you couldn't easily customize it. In v3, it's up to you to handle login failure side effects, but as it's a one liner, it shouldn't be too much work.
New authProvider
Signature
You may not notice it at first, but react-admin now uses a new authProvider
signature based on an object rather than a function. You don't need to change anything in your v2 authProvider
because we've added a compatibility layer.
As a reminder, here was the authProvider
structure in v2:
// in src/authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from "react-admin";
export default (type, params) => {
if (type === AUTH_LOGIN) {
// ...
return Promise.resolve();
}
if (type === AUTH_LOGOUT) {
// ...
return Promise.resolve();
}
if (type === AUTH_ERROR) {
// ...
return Promise.resolve();
}
if (type === AUTH_CHECK) {
return Promise.resolve();
}
if (type === AUTH_GET_PERMISSIONS) {
return Promise.resolve();
}
return Promise.reject("Unknown method");
};
In react-admin v3, here is how an authProvider
looks like:
const authProvider = {
login: params => Promise.resolve(),
logout: params => Promise.resolve(),
checkAuth: params => Promise.resolve(),
checkError: error => Promise.resolve(),
getPermissions: params => Promise.resolve(),
};
It's more idiomatic to JavaScript, and besides, it makes it possible to check the authProvider
structure at compile time using TypeScript. React-admin exports an AuthProvider
type for that purpose.
Conclusion
The move to react 16.8 and hooks opened a whole new world of developer experience. We believe the new react-admin hooks will allow better maintainability, will ease the learning curve, and facilitate reusability of custom business logic across components and projects. The Marmelab developers who already worked with react-admin v3 now think the v2 API is painful.
So don't wait too long, and make the switch to v3! It's in beta, but it's really worth it.