Skip to content

Commit

Permalink
Update react README.md to include Authentication section and add jsdo…
Browse files Browse the repository at this point in the history
…cs to the components and hooks
  • Loading branch information
kristianpd committed Jul 5, 2023
1 parent e925113 commit c510f5c
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 1,075 deletions.
93 changes: 92 additions & 1 deletion packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const MyApp = (props) => {

## Example usage

```typescript
```tsx
// import the API client for your specific application from your client package, see your app's installing instructions
import { Client } from "@gadget-client/my-gadget-app";
// import the required Provider object and some example hooks from this package
Expand Down Expand Up @@ -1039,3 +1039,94 @@ export const App = () => (
</Provider>
);
```
## Authentication
When working with Gadget auth, there are several hooks and components that can help you manage the authentication state of your application.
The `Provider` component exported from this library accepts an `auth` prop which can be used to configure the relative paths to your app's sign in and sign out endpoints. If you do not provide these paths, the default values of `/auth/signin` and `/auth/signout` will be used.
The hooks use the Gadget client's `suspense: true` option, making it easier to manage the async nature of the hooks without having to deal with loading state.
```tsx
import { Client } from "@gadget-client/my-gadget-app";
import { Provider } from "@gadgetinc/react";
import React, { Suspense } from "react";
import App from "./App";

// instantiate the API client for our app
const api = new Client({ authenticationMode: { browserSession: true } });

export function main() {
// ensure any components which use the @gadgetinc/react hooks are wrapped with the Provider and a Suspense component
return (
<Provider api={api} auth={{ signInPath: '/auth/signin', signOutPath: '/auth/signout' }}>
<Suspense fallback={<>Loading...</>}>
<App />
</Suspense>
</Provider>
);
}
```
### Hooks
React hooks are available to help you manage the authentication state of your application.
### `useSession()`
Returns the current session, equivalent to `await api.currentSession.get()` or `useGet(api.currentSession)`, but uses Promises for an easier interface. Throws a Suspense promise while the session is being loaded
### `useUser()`
Returns the current user of the session, if present. For unauthenticated sessions, returns `null`. Throws a Suspense promise while the session/user are loading
### `useIsSignedIn()`
Returns true if the session is logged in (has a user associated with it), and false otherwise. Throws a Suspense promise while the session/user is loading
```tsx
export default function App() {
const user = useUser();
const isSignedIn = useIsSignedIn();
const gadgetContext = useGadgetContext();

return (
<>
{ isSignedIn ? <p>Hello, {user.firstName} {user.lastName}}</p> : <a href={gadgetContext.auth.signInPath}>Please sign in</a> }
</>
);
}
```
### Components
If you are trying to control the layout of your application based on authentication state, it may be helpful to use the Gadget auth React components instead of, or in addition to, the hooks.
### `<SignedIn />`
Conditionally renders its children if the current session has a user associated with it, similar to the `useIsSignedIn()` hook.
```tsx
<h1>Hello<SignedIn>, human</SignedIn>!</h1>
```
### `<SignedOut />`
Conditionally renders its children if the current session has a user associated with it, similar to the `useIsSignedIn()` hook.
```tsx
<SignedOut><a href="/auth/signin">Sign In!</a></SignedOut>
```
### <SignedInOrRedirect />
Conditionally renders its children if the current session has a user associated with it, or redirects the browser via `window.location.assign` if the user is not currently signed in. This component is helpful for protecting front-end routes.
```tsx
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="my-profile" element={
<SignedInOrRedirect>
<MyProfile />
</SignedInOrRedirect>
} />
</Route>
</Routes>
</BrowserRouter>
```
20 changes: 20 additions & 0 deletions packages/react/spec/auth/useUser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,24 @@ describe("useUser", () => {
rerender();
expect(result.current).toBe(null);
});

test("it does something when the user fails to fetch", async () => {
const { result, rerender } = renderHook(() => useUser(), { wrapper: TestWrapper(superAuthApi) });

expect(mockUrqlClient.executeQuery).toBeCalledTimes(1);
mockUrqlClient.executeQuery.pushResponse("currentSession", {
data: {
currentSession: {
id: "123",
userId: null,
user: null,
},
},
stale: false,
hasNext: false,
});

rerender();
expect(result.current).toBe(null);
})
});
2 changes: 1 addition & 1 deletion packages/react/spec/components/SignedInOrRedirect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { TestWrapper } from "../../spec/testWrapper";
import { expectMockSignedInUser, expectMockSignedOutUser } from "../../spec/utils";
import { SignedInOrRedirect } from "../../src/components/SignedInOrRedirect";

describe("SignedInOrRedirectOrRedirect", () => {
describe("SignedInOrRedirect", () => {
const { location } = window;
const mockAssign = jest.fn();

Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/auth/useIsSignedIn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useUser } from "./useUser";

/**
* Used for determining if the current `Session` has a user `User` associated with it (is signed in). Will suspend while the session is being fetched.
* @returns `true` if the `Session` has a `User`, `false` otherwise.
*/
export const useIsSignedIn = () => {
const user = useUser();
return !!user;
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/auth/useSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const useGetSessionAndUser = () => {
}
};

/**
* Used for fetching the current `Session` record from Gadget. Will suspend while the user is being fetched.
* @returns The current session
*/
export const useSession = (): GadgetSession | undefined => {
const [{ data: session }] = useGetSessionAndUser();
return session;
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/auth/useUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useSession } from "./useSession";

/**
* Used for fetching the current `User` record from Gadget. Will return `null` if the session is unauthenticated. Will suspend while the user is being fetched.
* @returns The current user associated with the session or `null`.
*/
export const useUser = () => {
const session = useSession();
return session && session.user;
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/components/SignedIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import React from "react";
import { useSession } from "../../src/auth/useSession";
import { isSessionSignedIn } from "../../src/auth/utils";

/**
* Renders its `children` if the current `Session` is signed in (has an associated `User`), otherwise renders nothing.
*/
export const SignedIn = (props: { children: ReactNode }) => {
const session = useSession();
if (session && isSessionSignedIn(session)) {
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/components/SignedInOrRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { GadgetClientContext } from "../../src/GadgetProvider";
import { useSession } from "../../src/auth/useSession";
import { isSessionSignedIn } from "../../src/auth/utils";

/**
/**
* Renders its `children` if the current `Session` is signed in, otherwise redirects the browser to the `signInPath` configured in the `Provider`. Uses `window.location.assign` to perform the redirect.
*/
export const SignedInOrRedirect = (props: { children: ReactNode }) => {
const [redirected, setRedirected] = useState(false);
const session = useSession();
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/components/SignedOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import React from "react";
import { useSession } from "../../src/auth/useSession";
import { isSessionSignedOut } from "../../src/auth/utils";

/**
* Renders its `children` if the current `Session` is signed out (no associated `User`), otherwise renders nothing.
*/
export const SignedOut = (props: { children: ReactNode }) => {
const session = useSession();
if (!session || isSessionSignedOut(session)) {
Expand Down
Loading

0 comments on commit c510f5c

Please sign in to comment.