Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ root.render(
);
```

## ⚙️ The hook `useDirectus`
## ⚙️ Hooks

### `useDirectus`

After adding the provider, you can access the configured client anywhere in the app, using the `useDirectus` hook:

Expand All @@ -93,6 +95,43 @@ export const TodoList = () => {
};
```

### `useDirectusAuth`

The `useDirectusAuth` hook provides a few methods for working with the [Directus Authentication API](https://docs.directus.io/reference/old-sdk.html#authentication):

- `login` - a function that accepts an email and password and returns a promise that resolves to the user object if the login is successful or rejects with an error otherwise
- `logout` - a function that logs out the current user
- `user` - the current user object
- `authState` - the current authentication state, one of `loading` (the initial state), `logged-in` or `logged-out`

```jsx
import { useDirectusAuth } from 'react-directus';

const Login = () => {
const { login } = useDirectusAuth();

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const { email, password } = e.currentTarget.elements;
login(email.value, password.value).catch(err => {
console.error(err);
});
};

return (
<form onSubmit={handleSubmit}>
<input type='email' name='email' />
<input type='password' name='password' />
<button type='submit'>Login</button>
</form>
);
};

export default Login;

```

## 🧩 Components (so far...)

This package contains a few components for working with Direcuts [files](https://docs.directus.io/reference/files/). They are all configured for using the `apiUrl` specified in the provider. Hopefully, more will come in the future 🤗.
Expand Down
3 changes: 2 additions & 1 deletion jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"testMatch": ["<rootDir>/src/**/*(*.)@(spec|test).[tj]s?(x)"],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^@components/(.*)$": "<rootDir>/src/components/$1"
"^@components/(.*)$": "<rootDir>/src/components/$1",
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1"
}
}
52 changes: 48 additions & 4 deletions src/DirectusProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as React from 'react';

import { Directus, TypeMap } from '@directus/sdk';
import {
AuthStates,
DirectusAssetProps,
DirectusContextType,
DirectusContextTypeGeneric,
DirectusImageProps,
DirectusProviderProps,
} from '@/types';

import { Directus, TypeMap, UserType } from '@directus/sdk';

import { DirectusAsset } from '@components/DirectusAsset';
import { DirectusImage } from '@components/DirectusImage';

Expand All @@ -22,12 +24,18 @@ export const DirectusContext = React.createContext<DirectusContextTypeGeneric<an
export const DirectusProvider = <T extends TypeMap = TypeMap>({
apiUrl,
options,
autoLogin,
children,
}: DirectusProviderProps): JSX.Element => {
const [user, setUser] = React.useState<UserType | null>(null);
const [authState, setAuthState] = React.useState<AuthStates>('loading');

const directus = React.useMemo(() => new Directus<T>(apiUrl, options), [apiUrl, options]);

const value = React.useMemo<DirectusContextType<T>>(
() => ({
apiUrl: apiUrl,
directus: new Directus<T>(apiUrl, options),
apiUrl,
directus,
DirectusAsset: ({ asset, render, ...props }: DirectusAssetProps) => {
console.warn('Deprecated: Please import DirectusAsset directly from react-directus');
return <DirectusAsset asset={asset} render={render} {...props} />;
Expand All @@ -36,10 +44,46 @@ export const DirectusProvider = <T extends TypeMap = TypeMap>({
console.warn('Deprecated: Please import DirectusImage directly from react-directus');
return <DirectusImage asset={asset} render={render} {...props} />;
},
_directusUser: user,
_setDirecctusUser: setUser,
_authState: authState,
_setAuthState: setAuthState,
}),
[apiUrl, options]
[apiUrl, directus, user, authState]
);

React.useEffect(() => {
const checkAuth = async () => {
let newAuthState: AuthStates = 'unauthenticated';
try {
await directus.auth.refresh();
const token = await directus.auth.token;

if (token) {
const dUser = (await directus.users.me.read({
// * is a valid field, but typescript doesn't like it
// It's a wildcard, so it will return all fields
// This is the only way to get all fields
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fields: ['*'] as any,
})) as UserType;

if (dUser) {
newAuthState = 'authenticated';
setUser(dUser);
}
}
} catch (error) {
console.log('auth-error', error);
} finally {
setAuthState(newAuthState || 'unauthenticated');
}
};
if (autoLogin) {
checkAuth();
}
}, [directus, autoLogin]);

return <DirectusContext.Provider value={value}>{children}</DirectusContext.Provider>;
};

Expand Down
95 changes: 95 additions & 0 deletions src/hooks/useDirectusAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { DirectusAuthHook } from '../types';
import { DirectusContext } from '../DirectusProvider';
import React from 'react';
import { UserType } from '@directus/sdk';

/**
* A hook to access the Directus authentication state and methods.
*
* @example
* ```tsx
* import { useDirectusAuth } from 'react-directus';
*
* const Login = () => {
* const { login } = useDirectusAuth();
*
* const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
* e.preventDefault();
*
* const { email, password } = e.currentTarget.elements;
* login(email.value, password.value)
* .catch((err) => {
* console.error(err);
* });
* };
*
* return (
* <form onSubmit={handleSubmit}>
* <input type="email" name="email" />
* <input type="password" name="password" />
* <button type="submit">Login</button>
* </form>
* );
* };
*
* export default Login;
* ```
*/
export const useDirectusAuth = (): DirectusAuthHook => {
const directusContext = React.useContext(DirectusContext);

if (!directusContext) {
throw new Error('useDirectusAuth has to be used within the DirectusProvider');
}

const {
directus,
_authState: authState,
_setAuthState: setAuthState,
_directusUser: directusUser,
_setDirecctusUser: setDirectusUser,
} = directusContext;

const login = React.useCallback<DirectusAuthHook['login']>(
async (email: string, password: string) => {
await directus.auth.login({
email,
password,
});

const dUser = (await directus.users.me.read({
fields: ['*'],
})) as UserType;

if (dUser) {
setDirectusUser(dUser);
setAuthState('authenticated');
} else {
setDirectusUser(null);
setAuthState('unauthenticated');
}
},
[directus]
);

const logout = React.useCallback<DirectusAuthHook['logout']>(async () => {
try {
await directus.auth.logout();
} finally {
setAuthState('unauthenticated');
setDirectusUser(null);
}
}, [directus]);

const value = React.useMemo<DirectusAuthHook>(
() => ({
user: directusUser,
authState,
login,
logout,
}),
[directus, directusUser, authState]
);

return value;
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { DirectusProvider, useDirectus } from '@/DirectusProvider';
export { DirectusAsset } from '@components/DirectusAsset';
export { DirectusImage } from '@components/DirectusImage';
export { useDirectusAuth } from '@hooks/useDirectusAuth';
58 changes: 57 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { DirectusOptions, IDirectus, TypeMap } from '@directus/sdk';
import { DirectusOptions, IDirectus, TypeMap, UserType } from '@directus/sdk';
import { DirectusAsset } from '@components/DirectusAsset';
import { DirectusImage } from '@components/DirectusImage';

Expand Down Expand Up @@ -61,9 +61,16 @@ export interface DirectusProviderProps {
apiUrl: string;
/** A set of options to pass to the Directus client. */
options?: DirectusOptions;
/**
* If `true`, the provider will try to login the user automatically on mount.
* @default false
*/
autoLogin?: boolean;
children: React.ReactNode;
}

export type AuthStates = 'loading' | 'authenticated' | 'unauthenticated';

/**
* Shape of the main context.
*/
Expand All @@ -75,6 +82,55 @@ export interface DirectusContextType<T extends TypeMap> {
DirectusAsset: typeof DirectusAsset;
/** The context-aware `DirectusImage` component, with pre-filled props. */
DirectusImage: typeof DirectusImage;
/**
* Please use the data provided by the `useDirectusAuth` hook instead.
* @default 'loading'
* @internal
*/
_authState: AuthStates;
/**
* Please use the functions provided by the `useDirectusAuth` hook instead.
* @internal
*/
_setAuthState: React.Dispatch<React.SetStateAction<AuthStates>>;
/**
* Please use the data provided by the `useDirectusAuth` hook instead.
* @default null
* @internal
*/
_directusUser: UserType | null;
/**
* Please use the functions provided by the `useDirectusAuth` hook instead.
* @internal
*/
_setDirecctusUser: React.Dispatch<React.SetStateAction<UserType | null>>;
}

export type DirectusContextTypeGeneric<T extends TypeMap> = DirectusContextType<T> | null;

export interface DirectusAuthHook {
/**
* Login the user. If successful, the user will be stored in the context.
* Else, an error will be thrown.
* @param email - The user email.
* @param password - The user password.
* @throws {Error} - If the login fails.
*/
login: (email: string, password: string) => Promise<void>;
/**
* Logout the user. If successful, the user will be removed from the context.
* Else, an error will be thrown.
* @throws {Error} - If the logout fails.
*/
logout: () => Promise<void>;
/**
* Represents the current authentication state.
* @default 'loading'
*/
authState: AuthStates;
/**
* The current authenticated user.
* @default null
*/
user: UserType | null;
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"],
"@hooks/*": ["hooks/*"]
}
}
}