Skip to content

Commit

Permalink
Merge pull request #9031 from marmelab/doc-mutation-middlewares
Browse files Browse the repository at this point in the history
Document useRegisterMutationMiddleware
  • Loading branch information
slax57 committed Jul 3, 2023
2 parents 5d742a3 + 5566966 commit 4be368a
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 21 deletions.
1 change: 1 addition & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ title: "Index"
* [`useRedirect`](./useRedirect.md)
* [`useReference`](./useGetOne.md#aggregating-getone-calls)
* [`useRefresh`](./useRefresh.md)
* [`useRegisterMutationMiddleware`](./useRegisterMutationMiddleware.md)
* [`useRemoveFromStore`](./useRemoveFromStore.md)
* [`useResetStore`](./useResetStore.md)
* [`useResourceAppLocation`](https://marmelab.com/ra-enterprise/modules/ra-navigation#useresourceapplocation-access-current-resource-app-location)<img class="icon" src="./img/premium.svg" />
Expand Down
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<li {% if page.path == 'useEditContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useEditContext.html"><code>useEditContext</code></a></li>
<li {% if page.path == 'useEditController.md' %} class="active" {% endif %}><a class="nav-link" href="./useEditController.html"><code>useEditController</code></a></li>
<li {% if page.path == 'useSaveContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useSaveContext.html"><code>useSaveContext</code></a></li>
<li {% if page.path == 'useRegisterMutationMiddleware.md' %} class="active" {% endif %}><a class="nav-link" href="./useRegisterMutationMiddleware.html"><code>useRegisterMutationMiddleware</code></a></li>
</ul>

<ul><div>Show Page</div>
Expand Down
172 changes: 172 additions & 0 deletions docs/useRegisterMutationMiddleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
layout: default
title: "useRegisterMutationMiddleware"
---

# `useRegisterMutationMiddleware`

React-admin lets you hook into the save logic of the forms in Creation and Edition pages using middleware functions. These functions "wrap" the main mutation (`dataProvider.create()` in a Creation page, `dataProvider.update()` in an Edition page), so you can add you own code to be executed before and after it. This allows you to perform various advanced form use cases, such as:

- transforming the data passed to the main mutation,
- updating the mutation parameters before it is called,
- creating, updating or deleting related data,
- adding performances logs,
- etc.

Middleware functions have access to the same parameters as the underlying mutation (`create` or `update`), and to a `next` function to call the next function in the mutation lifecycle.

`useRegisterMutationMiddleware` allows to register a mutation middleware function for the current form.

## Usage

Define a middleware function, then use the hook to register it.

For example, a middleware for the create mutation looks like the following:

```tsx
import * as React from 'react';
import {
useRegisterMutationMiddleware,
CreateParams,
MutateOptions,
CreateMutationFunction
} from 'react-admin';

const MyComponent = () => {
const createMiddleware = async (
resource: string,
params: CreateParams,
options: MutateOptions,
next: CreateMutationFunction
) => {
// Do something before the mutation

// Call the next middleware
await next(resource, params, options);

// Do something after the mutation
}
const memoizedMiddleWare = React.useCallback(createMiddleware, []);
useRegisterMutationMiddleware(memoizedMiddleWare);
// ...
}
```

Then, render that component as a descendent of the page controller component (`<Create>` or `<Edit>`).

React-admin will wrap each call to the `dataProvider.create()` mutation with the `createMiddleware` function as long as the `MyComponent` component is mounted.

`useRegisterMutationMiddleware` unregisters the middleware function when the component unmounts. For this to work correctly, you must provide a stable reference to the function by wrapping it in a `useCallback` hook for instance.

## Params

`useRegisterMutationMiddleware` expects a single parameter: a middleware function.

A middleware function must have the following signature:

```jsx
const middlware = async (resource, params, options, next) => {
// Do something before the mutation

// Call the next middleware
await next(resource, params, options);

// Do something after the mutation
}
```

The `params` type depends on the mutation:

- For a `create` middleware, `{ data, meta }`
- For an `update` middleware, `{ id, data, previousData }`

## Example

The following example shows a custom `<ImageInput>` that converts its images to base64 on submit, and updates the main resource record to use the base64 versions of those images:

```tsx
import { useCallback } from 'react';
import {
CreateMutationFunction,
ImageInput,
Middleware,
useRegisterMutationMiddleware
} from 'react-admin';

const ThumbnailInput = () => {
const middleware = useCallback(async (
resource,
params,
options,
next
) => {
const b64 = await convertFileToBase64(params.data.thumbnail);
// Update the parameters that will be sent to the dataProvider call
const newParams = { ...params, data: { ...data, thumbnail: b64 } };
await next(resource, newParams, options);
}, []);
useRegisterMutationMiddleware(middleware);

return <ImageInput source="thumbnail" />;
};

const convertFileToBase64 = (file: {
rawFile: File;
src: string;
title: string;
}) =>
new Promise((resolve, reject) => {
// If the file src is a blob url, it must be converted to b64.
if (file.src.startsWith('blob:')) {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;

reader.readAsDataURL(file.rawFile);
} else {
resolve(file.src);
}
});
```

Use the `<ThumbnailInput>` component in a creation form just like any regular Input component:

```jsx
const PostCreate = () => (
<Create>
<SimpleForm>
<TextInput source="title" />
<TextInput source="body" multiline />
<ThumbnailInput />
</SimpleForm>
</Create>
);

With this middleware, given the following form values:

```json
{
"data": {
"thumbnail": {
"rawFile": {
"path": "avatar.jpg"
},
"src": "blob:http://localhost:9010/c925dc18-5918-4782-8087-b2464896b8f9",
"title": "avatar.jpg"
}
}
}
```

The dataProvider `create` function will be called with:

```json
{
"data": {
"thumbnail": {
"title":"avatar.jpg",
"src":"data:image/jpeg;base64,..."
}
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export interface UseMutationMiddlewaresResult<
unregisterMutationMiddleware: (callback: Middleware<MutateFunc>) => void;
}

export type Middleware<MutateFunc> = MutateFunc extends (...a: any[]) => infer R
export type Middleware<
MutateFunc = (...args: any[]) => any
> = MutateFunc extends (...a: any[]) => infer R
? (...a: [...U: Parameters<MutateFunc>, next: MutateFunc]) => R
: never;
32 changes: 22 additions & 10 deletions packages/ra-core/src/dataProvider/useCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,34 @@ export type UseCreateOptions<
Partial<UseCreateMutateParams<RecordType>>
> & { returnPromise?: boolean };

export type CreateMutationFunction<
RecordType extends Omit<RaRecord, 'id'> = any,
TReturnPromise extends boolean = boolean,
MutationError = unknown,
ResultRecordType extends RaRecord = RecordType & { id: Identifier }
> = (
resource?: string,
params?: Partial<CreateParams<Partial<RecordType>>>,
options?: MutateOptions<
ResultRecordType,
MutationError,
Partial<UseCreateMutateParams<RecordType>>,
unknown
> & { returnPromise?: TReturnPromise }
) => Promise<TReturnPromise extends true ? ResultRecordType : void>;

export type UseCreateResult<
RecordType extends Omit<RaRecord, 'id'> = any,
TReturnPromise extends boolean = boolean,
MutationError = unknown,
ResultRecordType extends RaRecord = RecordType & { id: Identifier }
> = [
(
resource?: string,
params?: Partial<CreateParams<Partial<RecordType>>>,
options?: MutateOptions<
ResultRecordType,
MutationError,
Partial<UseCreateMutateParams<RecordType>>,
unknown
> & { returnPromise?: TReturnPromise }
) => Promise<TReturnPromise extends true ? ResultRecordType : void>,
CreateMutationFunction<
RecordType,
TReturnPromise,
MutationError,
ResultRecordType
>,
UseMutationResult<
ResultRecordType,
MutationError,
Expand Down
26 changes: 16 additions & 10 deletions packages/ra-core/src/dataProvider/useUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,21 +458,27 @@ export type UseUpdateOptions<
Partial<UseUpdateMutateParams<RecordType>>
> & { mutationMode?: MutationMode; returnPromise?: boolean };

export type UpdateMutationFunction<
RecordType extends RaRecord = any,
TReturnPromise extends boolean = boolean,
MutationError = unknown
> = (
resource?: string,
params?: Partial<UpdateParams<RecordType>>,
options?: MutateOptions<
RecordType,
MutationError,
Partial<UseUpdateMutateParams<RecordType>>,
unknown
> & { mutationMode?: MutationMode; returnPromise?: TReturnPromise }
) => Promise<TReturnPromise extends true ? RecordType : void>;

export type UseUpdateResult<
RecordType extends RaRecord = any,
TReturnPromise extends boolean = boolean,
MutationError = unknown
> = [
(
resource?: string,
params?: Partial<UpdateParams<RecordType>>,
options?: MutateOptions<
RecordType,
MutationError,
Partial<UseUpdateMutateParams<RecordType>>,
unknown
> & { mutationMode?: MutationMode; returnPromise?: TReturnPromise }
) => Promise<TReturnPromise extends true ? RecordType : void>,
UpdateMutationFunction<RecordType, TReturnPromise, MutationError>,
UseMutationResult<
RecordType,
MutationError,
Expand Down
Loading

0 comments on commit 4be368a

Please sign in to comment.