diff --git a/docs/useGetIdentity.md b/docs/useGetIdentity.md
index 421a27ae6ac..e25673c59b3 100644
--- a/docs/useGetIdentity.md
+++ b/docs/useGetIdentity.md
@@ -5,7 +5,27 @@ title: "useGetIdentity"
# `useGetIdentity`
-You may want to use the current user name, avatar, or id in your code. for that purpose, call the `useGetIdentity()` hook, which calls `authProvider.getIdentity()` on mount. It returns an object containing the loading state, the error state, and the identity.
+React-admin calls `authProvider.getIdentity()` to retrieve and display the current logged-in username and avatar. The logic for calling this method is packaged into a custom hook, `useGetIdentity`, which you can use in your own code.
+
+![identity](./img/identity.png)
+
+## Syntax
+
+`useGetIdentity()` calls `authProvider.getIdentity()` on mount. It returns an object containing the loading state, the error state, and the identity.
+
+```jsx
+const { data, isLoading, error } = useGetIdentity();
+```
+
+Once loaded, the `data` object contains the following properties:
+
+```jsx
+const { id, fullName, avatar } = data;
+```
+
+`useGetIdentity` uses [react-query's `useQuery` hook](https://react-query-v3.tanstack.com/reference/useQuery) to call the `authProvider`.
+
+## Usage
Here is an example Edit component, which falls back to a Show component if the record is locked for edition by another user:
@@ -14,7 +34,7 @@ import { useGetIdentity, useGetOne } from 'react-admin';
const PostDetail = ({ id }) => {
const { data: post, isLoading: postLoading } = useGetOne('posts', { id });
- const { identity, isLoading: identityLoading } = useGetIdentity();
+ const { data: identity, isLoading: identityLoading } = useGetIdentity();
if (postLoading || identityLoading) return <>Loading...>;
if (!post.lockedBy || post.lockedBy === identity.id) {
// post isn't locked, or is locked by me
@@ -25,3 +45,42 @@ const PostDetail = ({ id }) => {
}
}
```
+
+## Refreshing The Identity
+
+If your application contains a form letting the current user update their name and/or avatar, you may want to refresh the identity after the form is submitted. As `useGetIdentity` uses [react-query's `useQuery` hook](https://react-query-v3.tanstack.com/reference/useQuery) to call the `authProvider`, you can take advantage of the `refetch` function to do so:
+
+```jsx
+const IdentityForm = () => {
+ const { isLoading, error, data, refetch } = useGetIdentity();
+ const [newIdentity, setNewIdentity] = useState('');
+
+ if (isLoading) return <>Loading>;
+ if (error) return <>Error>;
+
+ const handleChange = event => {
+ setNewIdentity(event.target.value);
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (!newIdentity) return;
+ fetch('/update_identity', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ identity: newIdentity })
+ }).then(() => {
+ // call authProvider.getIdentity() again and notify the listeners of the result,
+ // including the UserMenu in the AppBar
+ refetch();
+ });
+ };
+
+ return (
+
+ );
+};
+```
\ No newline at end of file
diff --git a/packages/ra-core/src/auth/index.ts b/packages/ra-core/src/auth/index.ts
index ba3c9abcdf7..fec06d41d5f 100644
--- a/packages/ra-core/src/auth/index.ts
+++ b/packages/ra-core/src/auth/index.ts
@@ -6,7 +6,6 @@ import usePermissionsOptimized from './usePermissionsOptimized';
import WithPermissions, { WithPermissionsProps } from './WithPermissions';
import useLogin from './useLogin';
import useLogout from './useLogout';
-import useGetIdentity from './useGetIdentity';
import useGetPermissions from './useGetPermissions';
import useLogoutIfAccessDenied from './useLogoutIfAccessDenied';
import convertLegacyAuthProvider from './convertLegacyAuthProvider';
@@ -15,6 +14,7 @@ export * from './Authenticated';
export * from './types';
export * from './useAuthenticated';
export * from './useCheckAuth';
+export * from './useGetIdentity';
export {
AuthContext,
@@ -23,7 +23,6 @@ export {
// low-level hooks for calling a particular verb on the authProvider
useLogin,
useLogout,
- useGetIdentity,
useGetPermissions,
// hooks with state management
usePermissions,
diff --git a/packages/ra-core/src/auth/useGetIdentity.spec.tsx b/packages/ra-core/src/auth/useGetIdentity.spec.tsx
new file mode 100644
index 00000000000..ca85b34dc1c
--- /dev/null
+++ b/packages/ra-core/src/auth/useGetIdentity.spec.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+
+import { Basic, ErrorCase, ResetIdentity } from './useGetIdentity.stories';
+
+describe('useGetIdentity', () => {
+ it('should return the identity', async () => {
+ render();
+ await screen.findByText('John Doe');
+ });
+ it('should return the authProvider error', async () => {
+ jest.spyOn(console, 'error').mockImplementationOnce(() => {});
+ render();
+ await screen.findByText('Error');
+ });
+ it('should allow to update the identity after a change', async () => {
+ render();
+ expect(await screen.findByText('John Doe')).not.toBeNull();
+ const input = screen.getByDisplayValue('John Doe');
+ fireEvent.change(input, { target: { value: 'Jane Doe' } });
+ fireEvent.click(screen.getByText('Save'));
+ await screen.findByText('Jane Doe');
+ expect(screen.queryByText('John Doe')).toBeNull();
+ });
+});
diff --git a/packages/ra-core/src/auth/useGetIdentity.stories.tsx b/packages/ra-core/src/auth/useGetIdentity.stories.tsx
new file mode 100644
index 00000000000..9e80e008dbc
--- /dev/null
+++ b/packages/ra-core/src/auth/useGetIdentity.stories.tsx
@@ -0,0 +1,97 @@
+import * as React from 'react';
+import { QueryClientProvider, QueryClient } from 'react-query';
+import { useGetIdentity } from './useGetIdentity';
+import AuthContext from './AuthContext';
+
+export default {
+ title: 'ra-core/auth/useGetIdentity',
+};
+
+const authProvider = {
+ login: () => Promise.resolve(),
+ logout: () => Promise.resolve(),
+ checkAuth: () => Promise.resolve(),
+ checkError: () => Promise.resolve(),
+ getPermissions: () => Promise.resolve(),
+ getIdentity: () => Promise.resolve({ id: 1, fullName: 'John Doe' }),
+};
+
+const Identity = () => {
+ const { data, error, isLoading } = useGetIdentity();
+ return isLoading ? <>Loading> : error ? <>Error> : <>{data.fullName}>;
+};
+
+export const Basic = () => (
+
+
+
+
+
+);
+
+export const ErrorCase = () => (
+
+ Promise.reject(new Error('Error')),
+ }}
+ >
+
+
+
+);
+
+export const ResetIdentity = () => {
+ let fullName = 'John Doe';
+
+ const IdentityForm = () => {
+ const { isLoading, error, data, refetch } = useGetIdentity();
+ const [newIdentity, setNewIdentity] = React.useState('');
+
+ if (isLoading) return <>Loading>;
+ if (error) return <>Error>;
+
+ const handleChange = event => {
+ setNewIdentity(event.target.value);
+ };
+
+ const handleSubmit = e => {
+ e.preventDefault();
+ if (!newIdentity) return;
+ fullName = newIdentity;
+ refetch();
+ };
+
+ return (
+
+ );
+ };
+
+ return (
+
+ Promise.resolve({ id: 1, fullName }),
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/packages/ra-core/src/auth/useGetIdentity.ts b/packages/ra-core/src/auth/useGetIdentity.ts
index 24a11cbeda8..a29255e6903 100644
--- a/packages/ra-core/src/auth/useGetIdentity.ts
+++ b/packages/ra-core/src/auth/useGetIdentity.ts
@@ -1,12 +1,16 @@
-import { useEffect } from 'react';
+import { useMemo } from 'react';
+import { useQuery, UseQueryOptions, QueryObserverResult } from 'react-query';
+
import useAuthProvider from './useAuthProvider';
import { UserIdentity } from '../types';
-import { useSafeSetState } from '../util/hooks';
const defaultIdentity = {
id: '',
fullName: null,
};
+const defaultQueryParams = {
+ staleTime: 5 * 60 * 1000,
+};
/**
* Return the current user identity by calling authProvider.getIdentity() on mount
@@ -14,20 +18,19 @@ const defaultIdentity = {
* The return value updates according to the call state:
*
* - mount: { isLoading: true }
- * - success: { identity: Identity, isLoading: false }
+ * - success: { data: Identity, refetch: () => {}, isLoading: false }
* - error: { error: Error, isLoading: false }
*
* The implementation is left to the authProvider.
*
- * @returns The current user identity. Destructure as { identity, error, isLoading }.
+ * @returns The current user identity. Destructure as { isLoading, data, error, refetch }.
*
* @example
- *
* import { useGetIdentity, useGetOne } from 'react-admin';
*
* const PostDetail = ({ id }) => {
* const { data: post, isLoading: postLoading } = useGetOne('posts', { id });
- * const { identity, isLoading: identityLoading } = useGetIdentity();
+ * const { data: identity, isLoading: identityLoading } = useGetIdentity();
* if (postLoading || identityLoading) return <>Loading...>;
* if (!post.lockedBy || post.lockedBy === identity.id) {
* // post isn't locked, or is locked by me
@@ -38,42 +41,61 @@ const defaultIdentity = {
* }
* }
*/
-const useGetIdentity = () => {
- const [state, setState] = useSafeSetState({
- isLoading: true,
- });
+export const useGetIdentity = (
+ queryParams: UseQueryOptions = defaultQueryParams
+): UseGetIdentityResult => {
const authProvider = useAuthProvider();
- useEffect(() => {
- if (authProvider && typeof authProvider.getIdentity === 'function') {
- const callAuthProvider = async () => {
- try {
- const identity = await authProvider.getIdentity();
- setState({
- isLoading: false,
- identity: identity || defaultIdentity,
- });
- } catch (error) {
- setState({
- isLoading: false,
- error,
- });
- }
- };
- callAuthProvider();
- } else {
- setState({
- isLoading: false,
- identity: defaultIdentity,
- });
- }
- }, [authProvider, setState]);
- return state;
+
+ const result = useQuery(
+ ['auth', 'getIdentity'],
+ authProvider
+ ? () => authProvider.getIdentity()
+ : async () => defaultIdentity,
+ queryParams
+ );
+
+ // @FIXME: return useQuery's result directly by removing identity prop (BC break - to be done in v5)
+ return useMemo(
+ () =>
+ result.isLoading
+ ? { isLoading: true }
+ : result.error
+ ? { error: result.error, isLoading: false }
+ : {
+ data: result.data,
+ identity: result.data,
+ refetch: result.refetch,
+ isLoading: false,
+ },
+
+ [result]
+ );
};
-interface State {
- isLoading: boolean;
- identity?: UserIdentity;
- error?: any;
-}
+export type UseGetIdentityResult =
+ | {
+ isLoading: true;
+ data?: undefined;
+ identity?: undefined;
+ error?: undefined;
+ refetch?: undefined;
+ }
+ | {
+ isLoading: false;
+ data?: undefined;
+ identity?: undefined;
+ error: Error;
+ refetch?: undefined;
+ }
+ | {
+ isLoading: false;
+ data: UserIdentity;
+ /**
+ * @deprecated Use data instead
+ */
+ identity: UserIdentity;
+ error?: undefined;
+ refetch: () => Promise>;
+ };
export default useGetIdentity;
diff --git a/packages/ra-core/src/auth/usePermissions.ts b/packages/ra-core/src/auth/usePermissions.ts
index 10386ef12aa..d293c3cdf61 100644
--- a/packages/ra-core/src/auth/usePermissions.ts
+++ b/packages/ra-core/src/auth/usePermissions.ts
@@ -49,13 +49,14 @@ const usePermissions = (
queryParams
);
- return useMemo(() => {
- return {
+ return useMemo(
+ () => ({
permissions: result.data,
isLoading: result.isLoading,
error: result.error,
- };
- }, [result]);
+ }),
+ [result]
+ );
};
export default usePermissions;