Merge pull request #58 from bknd-io/release/0.6.2

Release 0.6.2
This commit is contained in:
dswbx
2025-01-25 09:40:44 +01:00
committed by GitHub
22 changed files with 337 additions and 72 deletions

View File

@@ -3,7 +3,13 @@
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png)
bknd simplifies app development by providing fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
<p align="center" width="100%">
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema">
<strong>⭐ Live Demo</strong>
</a>
</p>
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
**For documentation and examples, please visit https://docs.bknd.io.**
@@ -17,7 +23,7 @@ bknd simplifies app development by providing fully functional backend for databa
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/index.js?compression=gzip&label=bknd/ui)
The unpacked size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
## Motivation
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
@@ -43,8 +49,8 @@ The package is mainly split into 4 parts, each serving a specific purpose:
| Import | Purpose |
|-----------------------------|------------------------------------------------------|
| `bknd`<br/>`bknd/adapter/*` | Backend including all APIs |
| `bknd/ui` | Admin UI components for react frameworks at |
| `bknd`<br/>`bknd/adapter/*` | Backend including APIs and adapters |
| `bknd/ui` | Admin UI components for react frameworks |
| `bknd/client` | TypeScript SDK and React hooks for the API endpoints |
| `bknd/elements` | React components for authentication and media |
@@ -110,6 +116,7 @@ export default function App() {
You don't have to figure out API details to include media uploads to your app. For an user avatar upload, this is all you need:
```tsx
import { Media } from "bknd/elements"
import "bknd/dist/main.css"
export function UserAvatar() {
return <Media.Dropzone

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { Guard } from "../../src/auth";
import { DebugLogger } from "../../src/core";
import { EventManager } from "../../src/core/events";
import { Default, stripMark } from "../../src/core/utils";
import { EntityManager } from "../../src/data";
@@ -17,6 +18,7 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
emgr: new EventManager(),
guard: new Guard(),
flags: Module.ctx_flags,
logger: new DebugLogger(false),
...overrides
};
}

View File

@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.6.1",
"version": "0.6.2",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io",
"repository": {

View File

@@ -1,10 +1,10 @@
import { cloneDeep, get, has, mergeWith, omit, set } from "lodash-es";
import { get, has, omit, set } from "lodash-es";
import {
Default,
type Static,
type TObject,
getFullPathKeys,
mark,
mergeObjectWith,
parse,
stripMark
} from "../utils";
@@ -33,7 +33,7 @@ export class SchemaObject<Schema extends TObject> {
) {
this._default = Default(_schema, {} as any) as any;
this._value = initial
? parse(_schema, cloneDeep(initial as any), {
? parse(_schema, structuredClone(initial as any), {
forceParse: this.isForceParse(),
skipMark: this.isForceParse()
})
@@ -64,8 +64,12 @@ export class SchemaObject<Schema extends TObject> {
return this._config;
}
clone() {
return structuredClone(this._config);
}
async set(config: Static<Schema>, noEmit?: boolean): Promise<Static<Schema>> {
const valid = parse(this._schema, cloneDeep(config) as any, {
const valid = parse(this._schema, structuredClone(config) as any, {
forceParse: true,
skipMark: this.isForceParse()
});
@@ -114,7 +118,7 @@ export class SchemaObject<Schema extends TObject> {
}
async patch(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
const current = cloneDeep(this._config);
const current = this.clone();
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
this.throwIfRestricted(partial);
@@ -122,11 +126,13 @@ export class SchemaObject<Schema extends TObject> {
// overwrite arrays and primitives, only deep merge objects
// @ts-ignore
const config = mergeWith(current, partial, (objValue, srcValue) => {
//console.log("---alt:new", _jsonp(mergeObject(current, partial)));
const config = mergeObjectWith(current, partial, (objValue, srcValue) => {
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
return srcValue;
}
});
//console.log("---new", _jsonp(config));
//console.log("overwritePaths", this.options?.overwritePaths);
if (this.options?.overwritePaths) {
@@ -164,14 +170,14 @@ export class SchemaObject<Schema extends TObject> {
}
}
//console.log("patch", { path, value, partial, config, current });
//console.log("patch", _jsonp({ path, value, partial, config, current }));
const newConfig = await this.set(config);
return [partial, newConfig];
}
async overwrite(path: string, value: any): Promise<[Partial<Static<Schema>>, Static<Schema>]> {
const current = cloneDeep(this._config);
const current = this.clone();
const partial = path.length > 0 ? (set({}, path, value) as Partial<Static<Schema>>) : value;
this.throwIfRestricted(partial);
@@ -192,7 +198,7 @@ export class SchemaObject<Schema extends TObject> {
if (p.length > 1) {
const parent = p.slice(0, -1).join(".");
if (!has(this._config, parent)) {
console.log("parent", parent, JSON.stringify(this._config, null, 2));
//console.log("parent", parent, JSON.stringify(this._config, null, 2));
throw new Error(`Parent path "${parent}" does not exist`);
}
}
@@ -207,7 +213,7 @@ export class SchemaObject<Schema extends TObject> {
throw new Error(`Path "${path}" does not exist`);
}
const current = cloneDeep(this._config);
const current = this.clone();
const removed = get(current, path) as Partial<Static<Schema>>;
const config = omit(current, path);
const newConfig = await this.set(config);

View File

@@ -4,6 +4,14 @@ export function _jsonp(obj: any, space = 2): string {
return JSON.stringify(obj, null, space);
}
export function isPlainObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === "[object Object]";
}
export function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object";
}
export function safelyParseObjectValues<T extends { [key: string]: any }>(obj: T): T {
return Object.entries(obj).reduce((acc, [key, value]) => {
try {
@@ -97,15 +105,6 @@ export function objectEach<T extends Record<string, any>, U>(
);
}
/**
* Simple object check.
* @param item
* @returns {boolean}
*/
export function isObject(item) {
return item && typeof item === "object" && !Array.isArray(item);
}
/**
* Deep merge two objects.
* @param target
@@ -197,3 +196,73 @@ export function objectCleanEmpty<Obj extends { [key: string]: any }>(obj: Obj):
return acc;
}, {} as any);
}
/**
* Lodash's merge implementation caused issues in Next.js environments
* From: https://thescottyjam.github.io/snap.js/#!/nolodash/merge
* NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge.
* @param object
* @param sources
*/
export function mergeObject(object, ...sources) {
for (const source of sources) {
for (const [key, value] of Object.entries(source)) {
if (value === undefined) {
continue;
}
// These checks are a week attempt at mimicking the various edge-case behaviors
// that Lodash's `_.merge()` exhibits. Feel free to simplify and
// remove checks that you don't need.
if (!isPlainObject(value) && !Array.isArray(value)) {
object[key] = value;
} else if (Array.isArray(value) && !Array.isArray(object[key])) {
object[key] = value;
} else if (!isObject(object[key])) {
object[key] = value;
} else {
mergeObject(object[key], value);
}
}
}
return object;
}
/**
* Lodash's mergeWith implementation caused issues in Next.js environments
* From: https://thescottyjam.github.io/snap.js/#!/nolodash/mergeWith
* NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge.
* @param object
* @param sources
* @param customizer
*/
export function mergeObjectWith(object, source, customizer) {
for (const [key, value] of Object.entries(source)) {
const mergedValue = customizer(object[key], value, key, object, source);
if (mergedValue !== undefined) {
object[key] = mergedValue;
continue;
}
// Otherwise, fall back to default behavior
if (value === undefined) {
continue;
}
// These checks are a week attempt at mimicking the various edge-case behaviors
// that Lodash's `_.merge()` exhibits. Feel free to simplify and
// remove checks that you don't need.
if (!isPlainObject(value) && !Array.isArray(value)) {
object[key] = value;
} else if (Array.isArray(value) && !Array.isArray(object[key])) {
object[key] = value;
} else if (!isObject(object[key])) {
object[key] = value;
} else {
mergeObjectWith(object[key], value, customizer);
}
}
return object;
}

View File

@@ -13,7 +13,15 @@ import { type AppDataConfig, dataConfigSchema } from "./data-schema";
export class AppData extends Module<typeof dataConfigSchema> {
override async build() {
const entities = transformObject(this.config.entities ?? {}, (entityConfig, name) => {
const {
entities: _entities = {},
relations: _relations = {},
indices: _indices = {}
} = this.config;
this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities));
const entities = transformObject(_entities, (entityConfig, name) => {
return constructEntity(name, entityConfig);
});
@@ -21,14 +29,14 @@ export class AppData extends Module<typeof dataConfigSchema> {
const name = typeof _e === "string" ? _e : _e.name;
const entity = entities[name];
if (entity) return entity;
throw new Error(`Entity "${name}" not found`);
throw new Error(`[AppData] Entity "${name}" not found`);
};
const relations = transformObject(this.config.relations ?? {}, (relation) =>
const relations = transformObject(_relations, (relation) =>
constructRelation(relation, _entity)
);
const indices = transformObject(this.config.indices ?? {}, (index, name) => {
const indices = transformObject(_indices, (index, name) => {
const entity = _entity(index.entity)!;
const fields = index.fields.map((f) => entity.field(f)!);
return new EntityIndex(entity, fields, index.unique, name);
@@ -52,6 +60,7 @@ export class AppData extends Module<typeof dataConfigSchema> {
);
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
this.ctx.logger.clear();
this.setBuilt();
}

View File

@@ -14,3 +14,6 @@ export { registries } from "modules/registries";
export type * from "./adapter";
export { Api, type ApiOptions } from "./Api";
export type { MediaFieldSchema } from "media/AppMedia";
export type { UserFieldSchema } from "auth/AppAuth";

View File

@@ -1,6 +1,6 @@
import type { App } from "App";
import type { Guard } from "auth";
import { SchemaObject } from "core";
import { type DebugLogger, SchemaObject } from "core";
import type { EventManager } from "core/events";
import type { Static, TSchema } from "core/utils";
import {
@@ -35,6 +35,7 @@ export type ModuleBuildContext = {
em: EntityManager;
emgr: EventManager<any>;
guard: Guard;
logger: DebugLogger;
flags: (typeof Module)["ctx_flags"];
};

View File

@@ -231,7 +231,8 @@ export class ModuleManager {
em: this.em,
emgr: this.emgr,
guard: this.guard,
flags: Module.ctx_flags
flags: Module.ctx_flags,
logger: this.logger
};
}

View File

@@ -43,7 +43,11 @@ export function useBkndData() {
return {
config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => {
console.log("patch config", entityName, partial);
return await bkndActions.patch("data", `entities.${entityName}.config`, partial);
return await bkndActions.overwrite(
"data",
`entities.${entityName}.config`,
partial
);
},
fields: entityFieldActions(bkndActions, entityName)
};

View File

@@ -16,6 +16,7 @@ export type JsonSchemaFormProps = any & {
uiSchema?: any;
direction?: "horizontal" | "vertical";
onChange?: (value: any, isValid: () => boolean) => void;
cleanOnChange?: boolean;
};
export type JsonSchemaFormRef = {
@@ -36,6 +37,7 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
templates,
fields,
widgets,
cleanOnChange,
...props
},
ref
@@ -51,8 +53,8 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
return false;
};
const handleChange = ({ formData }: any, e) => {
const clean = JSON.parse(JSON.stringify(formData));
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
const clean = cleanOnChange !== false ? JSON.parse(JSON.stringify(formData)) : formData;
console.log("Data changed: ", clean, { cleanOnChange });
setValue(clean);
onChange?.(clean, () => isValid(clean));
};

View File

@@ -7,10 +7,23 @@ export type AuthScreenProps = {
action?: "login" | "register";
logo?: ReactNode;
intro?: ReactNode;
formOnly?: boolean;
};
export function AuthScreen({ method = "POST", action = "login", logo, intro }: AuthScreenProps) {
export function AuthScreen({
method = "POST",
action = "login",
logo,
intro,
formOnly
}: AuthScreenProps) {
const { strategies, basepath, loading } = useAuthStrategies();
const Form = <AuthForm auth={{ basepath, strategies }} method={method} action={action} />;
if (formOnly) {
if (loading) return null;
return Form;
}
return (
<div className="flex flex-1 flex-col select-none h-dvh w-dvw justify-center items-center bknd-admin">
@@ -25,7 +38,7 @@ export function AuthScreen({ method = "POST", action = "login", logo, intro }: A
<p className="text-primary/50">Enter your credentials below to get access.</p>
</div>
)}
<AuthForm auth={{ basepath, strategies }} method={method} action={action} />
{Form}
</div>
)}
</div>

View File

@@ -1,3 +1,4 @@
import { useAuthStrategies } from "../hooks/use-auth";
import { AuthForm } from "./AuthForm";
import { AuthScreen } from "./AuthScreen";
import { SocialLink } from "./SocialLink";
@@ -7,3 +8,5 @@ export const Auth = {
Form: AuthForm,
SocialLink: SocialLink
};
export { useAuthStrategies };

View File

@@ -1,2 +1,2 @@
export { Auth } from "./auth";
export * from "./auth";
export * from "./media";

View File

@@ -1,31 +1,32 @@
import type { RepoQuery, RepoQueryIn } from "data";
import type { RepoQueryIn } from "data";
import type { MediaFieldSchema } from "media/AppMedia";
import type { TAppMediaConfig } from "media/media-schema";
import { useId } from "react";
import { useApi, useBaseUrl, useEntityQuery, useInvalidate } from "ui/client";
import { type ReactNode, createContext, useContext, useId } from "react";
import { useApi, useEntityQuery, useInvalidate } from "ui/client";
import { useEvent } from "ui/hooks/use-event";
import { Dropzone, type DropzoneProps, type DropzoneRenderProps, type FileState } from "./Dropzone";
import { mediaItemsToFileStates } from "./helper";
export type DropzoneContainerProps = {
children?: (props: DropzoneRenderProps) => JSX.Element;
children?: ReactNode;
initialItems?: MediaFieldSchema[];
entity?: {
name: string;
id: number;
field: string;
};
media?: Pick<TAppMediaConfig, "entity_name" | "storage">;
query?: RepoQueryIn;
} & Partial<Pick<TAppMediaConfig, "basepath" | "entity_name" | "storage">> &
Partial<DropzoneProps>;
} & Omit<Partial<DropzoneProps>, "children" | "initialItems">;
const DropzoneContainerContext = createContext<DropzoneRenderProps>(undefined!);
export function DropzoneContainer({
initialItems,
basepath = "/api/media",
storage = {},
entity_name = "media",
media,
entity,
query,
children,
...props
}: DropzoneContainerProps) {
const id = useId();
@@ -33,10 +34,11 @@ export function DropzoneContainer({
const baseUrl = api.baseUrl;
const invalidate = useInvalidate();
const limit = query?.limit ? query?.limit : props.maxItems ? props.maxItems : 50;
console.log("dropzone:baseUrl", baseUrl);
const entity_name = (media?.entity_name ?? "media") as "media";
//console.log("dropzone:baseUrl", baseUrl);
const $q = useEntityQuery(
entity_name as "media",
entity_name,
undefined,
{
...query,
@@ -89,6 +91,18 @@ export function DropzoneContainer({
autoUpload
initialItems={_initialItems}
{...props}
/>
>
{children
? (props) => (
<DropzoneContainerContext.Provider value={props}>
{children}
</DropzoneContainerContext.Provider>
)
: undefined}
</Dropzone>
);
}
export function useDropzone() {
return useContext(DropzoneContainerContext);
}

View File

@@ -1,11 +1,14 @@
import { PreviewWrapperMemoized } from "./Dropzone";
import { DropzoneContainer } from "./DropzoneContainer";
import { DropzoneContainer, useDropzone } from "./DropzoneContainer";
export const Media = {
Dropzone: DropzoneContainer,
Preview: PreviewWrapperMemoized
Preview: PreviewWrapperMemoized,
useDropzone: useDropzone
};
export { useDropzone as useMediaDropzone };
export type {
PreviewComponentProps,
FileState,

View File

@@ -201,7 +201,7 @@ const EntityContextMenu = ({
separator,
{
icon: IconSettings,
label: "Settings",
label: "Advanced",
onClick: () =>
navigate(routes.settings.path(["data", "entities", entity.name]), {
absolute: true

View File

@@ -40,8 +40,8 @@ export function DataEntityList({ params }) {
useBrowserTitle(["Data", entity?.label ?? params.entity]);
const [navigate] = useNavigate();
const search = useSearch(searchSchema, {
select: undefined,
sort: undefined
select: entity.getSelect(undefined, "form"),
sort: entity.getDefaultSort()
});
const $q = useApiQuery(

View File

@@ -1,4 +1,4 @@
import { type DropzoneRenderProps, Media } from "ui/elements";
import { Media } from "ui/elements";
import { Scrollable } from "ui/layouts/AppShell/AppShell";
export default function DropzoneElementTest() {
@@ -12,7 +12,7 @@ export default function DropzoneElementTest() {
maxItems={1}
overwrite
>
{(props) => <CustomUserAvatarDropzone {...props} />}
<CustomUserAvatarDropzone />
</Media.Dropzone>
</div>
@@ -49,12 +49,13 @@ export default function DropzoneElementTest() {
);
}
function CustomUserAvatarDropzone({
wrapperRef,
inputProps,
state: { files, isOver, isOverAccepted, showPlaceholder },
actions: { openFileInput }
}: DropzoneRenderProps) {
function CustomUserAvatarDropzone() {
const {
wrapperRef,
inputProps,
state: { files, isOver, isOverAccepted, showPlaceholder },
actions: { openFileInput }
} = Media.useDropzone();
const file = files[0];
return (

BIN
bun.lockb

Binary file not shown.

View File

@@ -3,23 +3,134 @@ title: "React Elements"
description: "Speed up your frontend development"
---
<Note>
The documentation is currently a work in progress and not complete.
</Note>
Not only creating and maintaing a backend is time-consuming, but also integrating it into your frontend can be a hassle. With `bknd/elements`, you can easily add media uploads and authentication forms to your app without having to figure out API details.
## Media uploads
<Note>
In order to use these exported elements, make sure to wrap your app inside `ClientProvider`. See the [React Setup](/usage/react#setup) for more information.
</Note>
# Media
## Media.Dropzone
The `Media.Dropzone` element allows retrieving from and uploading media items to your bknd instance. Without any properties specified, it will behave similar to your media library inside the bknd Admin UI. Here is how to get the last 10 items:
```tsx
import { Media } from "bknd/elements"
export function UserAvatar() {
export default function MediaGallery() {
return <Media.Dropzone query={{ limit: 10, sort: "-id" }} />
}
```
Since you can also upload media to a specific entity, you can also point that `Dropzone` to it. Here is an example of a single user avatar that gets overwritten on re-upload:
```tsx
import { Media } from "bknd/elements";
export default function UserAvatar() {
return <Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
/>
}
```
### Props
- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects.
- `entity?: { name: string; id: number; field: string }`: If given, the initial media items fetched will be from this entity.
- `query?: RepoQueryIn`: Query to filter the media items.
- `overwrite?: boolean`: If true, the media item will be overwritten on entity media uploads if limit was reached.
- `maxItems?: number`: Maximum number of media items that can be uploaded.
- `autoUpload?: boolean`: If true, the media items will be uploaded automatically.
- `onRejected?: (files: FileWithPath[]) => void`: Callback when a file is rejected.
- `onDeleted?: (file: FileState) => void`: Callback when a file is deleted.
- `onUploaded?: (file: FileState) => void`: Callback when a file is uploaded.
- `placeholder?: { show?: boolean; text?: string }`: Placeholder text to show when no media items are present.
### Customize Rendering
You can also customize the rendering of the media items and its uploading by passing a react element as a child. Here is an example of a custom `Media.Dropzone` that renders an user avatar (styled using tailwind):
```tsx
import { Media, useMediaDropzone } from "bknd/elements";
export default function CustomUserAvatar() {
return <Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
>
<CustomUserAvatar />
</Media.Dropzone>
}
function CustomUserAvatar() {
const {
wrapperRef,
inputProps,
state: { files, isOver, isOverAccepted, showPlaceholder },
actions: { openFileInput }
} = useMediaDropzone();
const file = files[0];
return (
<div
ref={wrapperRef}
className="size-32 rounded-full border border-gray-200 flex justify-center items-center leading-none overflow-hidden"
>
<div className="hidden">
<input {...inputProps} />
</div>
{showPlaceholder && <>{isOver && isOverAccepted ? "let it drop" : "drop here"}</>}
{file && (
<Media.Preview
file={file}
className="object-cover w-full h-full"
onClick={openFileInput}
/>
)}
</div>
);
}
```
# Auth
Adding authentication to your app with bknd is as easy as adding a `<form method="POST" />` with an action pointing to the action (`login` or `register`) to the strategy you want to use, e.g. for the password strategy, use `/api/auth/password/login`. But to make it even easier, you can use the `Auth.*` elements.
## `Auth.Screen`
The `Auth.Screen` element is a wrapper around the `Auth.Form` element that provides a full page screen. The current layout is admittedly very basic, but there will be more customization options in the future.
```tsx
import { Auth } from "bknd/elements"
export default function LoginScreen() {
return <Auth.Screen action="login" />
}
```
### Props
Note that this component doesn't require any strategy-specific information, as it gathers it itself.
- `action: "login" | "register"`: The action to perform.
- `method?: "POST" | "GET"`: The method to use for the form.
## `Auth.Form`
If you only wish to render the form itself without the screen, you can use the `Auth.Form` element. Unlike the `Auth.Screen`, this element requires the `strategy` prop to be set to the strategy you want to use. You can either specify it manually, use use the exported hook `useAuthStrategies()` for fetch them from your bknd instance.
```tsx
import { Auth, useAuthStrategies } from "bknd/elements"
export default function LoginForm() {
const { strategies, basepath, loading } = useAuthStrategies();
if (loading) return null;
return <Auth.Form
action="login"
strategies={strategies}
basepath={basepath}
/>
}
```

View File

@@ -3,7 +3,7 @@ title: 'SDK (React)'
description: 'Use the bknd SDK for React'
---
bknd exports 4 useful hooks to work with your backend:
There are 4 useful hooks to work with your backend:
1. simple hooks which are solely based on the [API](/usage/sdk):
- [`useApi`](#useapi)
- [`useEntity`](#useentity)
@@ -11,6 +11,22 @@ bknd exports 4 useful hooks to work with your backend:
- [`useApiQuery`](#useapiquery)
- [`useEntityQuery`](#useentityquery)
## Setup
In order to use them, make sure you wrap your `<App />` inside `<ClientProvider />`, so that these hooks point to your bknd instance:
```tsx
import { ClientProvider } from "bknd/client";
export default function App() {
return <ClientProvider>
{/* your app */}
</ClientProvider>
}
```
For all other examples below, we'll assume that your app is wrapped inside the `ClientProvider`.
## `useApi()`
To use the simple hook that returns the Api, you can use:
```tsx