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;