Skip to content

Commit 4be368a

Browse files
authored
Merge pull request #9031 from marmelab/doc-mutation-middlewares
Document useRegisterMutationMiddleware
2 parents 5d742a3 + 5566966 commit 4be368a

File tree

7 files changed

+333
-21
lines changed

7 files changed

+333
-21
lines changed

docs/Reference.md

+1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ title: "Index"
277277
* [`useRedirect`](./useRedirect.md)
278278
* [`useReference`](./useGetOne.md#aggregating-getone-calls)
279279
* [`useRefresh`](./useRefresh.md)
280+
* [`useRegisterMutationMiddleware`](./useRegisterMutationMiddleware.md)
280281
* [`useRemoveFromStore`](./useRemoveFromStore.md)
281282
* [`useResetStore`](./useResetStore.md)
282283
* [`useResourceAppLocation`](https://marmelab.com/ra-enterprise/modules/ra-navigation#useresourceapplocation-access-current-resource-app-location)<img class="icon" src="./img/premium.svg" />

docs/navigation.html

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
<li {% if page.path == 'useEditContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useEditContext.html"><code>useEditContext</code></a></li>
120120
<li {% if page.path == 'useEditController.md' %} class="active" {% endif %}><a class="nav-link" href="./useEditController.html"><code>useEditController</code></a></li>
121121
<li {% if page.path == 'useSaveContext.md' %} class="active" {% endif %}><a class="nav-link" href="./useSaveContext.html"><code>useSaveContext</code></a></li>
122+
<li {% if page.path == 'useRegisterMutationMiddleware.md' %} class="active" {% endif %}><a class="nav-link" href="./useRegisterMutationMiddleware.html"><code>useRegisterMutationMiddleware</code></a></li>
122123
</ul>
123124

124125
<ul><div>Show Page</div>

docs/useRegisterMutationMiddleware.md

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
---
2+
layout: default
3+
title: "useRegisterMutationMiddleware"
4+
---
5+
6+
# `useRegisterMutationMiddleware`
7+
8+
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:
9+
10+
- transforming the data passed to the main mutation,
11+
- updating the mutation parameters before it is called,
12+
- creating, updating or deleting related data,
13+
- adding performances logs,
14+
- etc.
15+
16+
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.
17+
18+
`useRegisterMutationMiddleware` allows to register a mutation middleware function for the current form.
19+
20+
## Usage
21+
22+
Define a middleware function, then use the hook to register it.
23+
24+
For example, a middleware for the create mutation looks like the following:
25+
26+
```tsx
27+
import * as React from 'react';
28+
import {
29+
useRegisterMutationMiddleware,
30+
CreateParams,
31+
MutateOptions,
32+
CreateMutationFunction
33+
} from 'react-admin';
34+
35+
const MyComponent = () => {
36+
const createMiddleware = async (
37+
resource: string,
38+
params: CreateParams,
39+
options: MutateOptions,
40+
next: CreateMutationFunction
41+
) => {
42+
// Do something before the mutation
43+
44+
// Call the next middleware
45+
await next(resource, params, options);
46+
47+
// Do something after the mutation
48+
}
49+
const memoizedMiddleWare = React.useCallback(createMiddleware, []);
50+
useRegisterMutationMiddleware(memoizedMiddleWare);
51+
// ...
52+
}
53+
```
54+
55+
Then, render that component as a descendent of the page controller component (`<Create>` or `<Edit>`).
56+
57+
React-admin will wrap each call to the `dataProvider.create()` mutation with the `createMiddleware` function as long as the `MyComponent` component is mounted.
58+
59+
`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.
60+
61+
## Params
62+
63+
`useRegisterMutationMiddleware` expects a single parameter: a middleware function.
64+
65+
A middleware function must have the following signature:
66+
67+
```jsx
68+
const middlware = async (resource, params, options, next) => {
69+
// Do something before the mutation
70+
71+
// Call the next middleware
72+
await next(resource, params, options);
73+
74+
// Do something after the mutation
75+
}
76+
```
77+
78+
The `params` type depends on the mutation:
79+
80+
- For a `create` middleware, `{ data, meta }`
81+
- For an `update` middleware, `{ id, data, previousData }`
82+
83+
## Example
84+
85+
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:
86+
87+
```tsx
88+
import { useCallback } from 'react';
89+
import {
90+
CreateMutationFunction,
91+
ImageInput,
92+
Middleware,
93+
useRegisterMutationMiddleware
94+
} from 'react-admin';
95+
96+
const ThumbnailInput = () => {
97+
const middleware = useCallback(async (
98+
resource,
99+
params,
100+
options,
101+
next
102+
) => {
103+
const b64 = await convertFileToBase64(params.data.thumbnail);
104+
// Update the parameters that will be sent to the dataProvider call
105+
const newParams = { ...params, data: { ...data, thumbnail: b64 } };
106+
await next(resource, newParams, options);
107+
}, []);
108+
useRegisterMutationMiddleware(middleware);
109+
110+
return <ImageInput source="thumbnail" />;
111+
};
112+
113+
const convertFileToBase64 = (file: {
114+
rawFile: File;
115+
src: string;
116+
title: string;
117+
}) =>
118+
new Promise((resolve, reject) => {
119+
// If the file src is a blob url, it must be converted to b64.
120+
if (file.src.startsWith('blob:')) {
121+
const reader = new FileReader();
122+
reader.onload = () => resolve(reader.result);
123+
reader.onerror = reject;
124+
125+
reader.readAsDataURL(file.rawFile);
126+
} else {
127+
resolve(file.src);
128+
}
129+
});
130+
```
131+
132+
Use the `<ThumbnailInput>` component in a creation form just like any regular Input component:
133+
134+
```jsx
135+
const PostCreate = () => (
136+
<Create>
137+
<SimpleForm>
138+
<TextInput source="title" />
139+
<TextInput source="body" multiline />
140+
<ThumbnailInput />
141+
</SimpleForm>
142+
</Create>
143+
);
144+
145+
With this middleware, given the following form values:
146+
147+
```json
148+
{
149+
"data": {
150+
"thumbnail": {
151+
"rawFile": {
152+
"path": "avatar.jpg"
153+
},
154+
"src": "blob:http://localhost:9010/c925dc18-5918-4782-8087-b2464896b8f9",
155+
"title": "avatar.jpg"
156+
}
157+
}
158+
}
159+
```
160+
161+
The dataProvider `create` function will be called with:
162+
163+
```json
164+
{
165+
"data": {
166+
"thumbnail": {
167+
"title":"avatar.jpg",
168+
"src":"data:image/jpeg;base64,..."
169+
}
170+
}
171+
}
172+
```

packages/ra-core/src/controller/saveContext/useMutationMiddlewares.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export interface UseMutationMiddlewaresResult<
113113
unregisterMutationMiddleware: (callback: Middleware<MutateFunc>) => void;
114114
}
115115

116-
export type Middleware<MutateFunc> = MutateFunc extends (...a: any[]) => infer R
116+
export type Middleware<
117+
MutateFunc = (...args: any[]) => any
118+
> = MutateFunc extends (...a: any[]) => infer R
117119
? (...a: [...U: Parameters<MutateFunc>, next: MutateFunc]) => R
118120
: never;

packages/ra-core/src/dataProvider/useCreate.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -168,22 +168,34 @@ export type UseCreateOptions<
168168
Partial<UseCreateMutateParams<RecordType>>
169169
> & { returnPromise?: boolean };
170170

171+
export type CreateMutationFunction<
172+
RecordType extends Omit<RaRecord, 'id'> = any,
173+
TReturnPromise extends boolean = boolean,
174+
MutationError = unknown,
175+
ResultRecordType extends RaRecord = RecordType & { id: Identifier }
176+
> = (
177+
resource?: string,
178+
params?: Partial<CreateParams<Partial<RecordType>>>,
179+
options?: MutateOptions<
180+
ResultRecordType,
181+
MutationError,
182+
Partial<UseCreateMutateParams<RecordType>>,
183+
unknown
184+
> & { returnPromise?: TReturnPromise }
185+
) => Promise<TReturnPromise extends true ? ResultRecordType : void>;
186+
171187
export type UseCreateResult<
172188
RecordType extends Omit<RaRecord, 'id'> = any,
173189
TReturnPromise extends boolean = boolean,
174190
MutationError = unknown,
175191
ResultRecordType extends RaRecord = RecordType & { id: Identifier }
176192
> = [
177-
(
178-
resource?: string,
179-
params?: Partial<CreateParams<Partial<RecordType>>>,
180-
options?: MutateOptions<
181-
ResultRecordType,
182-
MutationError,
183-
Partial<UseCreateMutateParams<RecordType>>,
184-
unknown
185-
> & { returnPromise?: TReturnPromise }
186-
) => Promise<TReturnPromise extends true ? ResultRecordType : void>,
193+
CreateMutationFunction<
194+
RecordType,
195+
TReturnPromise,
196+
MutationError,
197+
ResultRecordType
198+
>,
187199
UseMutationResult<
188200
ResultRecordType,
189201
MutationError,

packages/ra-core/src/dataProvider/useUpdate.ts

+16-10
Original file line numberDiff line numberDiff line change
@@ -458,21 +458,27 @@ export type UseUpdateOptions<
458458
Partial<UseUpdateMutateParams<RecordType>>
459459
> & { mutationMode?: MutationMode; returnPromise?: boolean };
460460

461+
export type UpdateMutationFunction<
462+
RecordType extends RaRecord = any,
463+
TReturnPromise extends boolean = boolean,
464+
MutationError = unknown
465+
> = (
466+
resource?: string,
467+
params?: Partial<UpdateParams<RecordType>>,
468+
options?: MutateOptions<
469+
RecordType,
470+
MutationError,
471+
Partial<UseUpdateMutateParams<RecordType>>,
472+
unknown
473+
> & { mutationMode?: MutationMode; returnPromise?: TReturnPromise }
474+
) => Promise<TReturnPromise extends true ? RecordType : void>;
475+
461476
export type UseUpdateResult<
462477
RecordType extends RaRecord = any,
463478
TReturnPromise extends boolean = boolean,
464479
MutationError = unknown
465480
> = [
466-
(
467-
resource?: string,
468-
params?: Partial<UpdateParams<RecordType>>,
469-
options?: MutateOptions<
470-
RecordType,
471-
MutationError,
472-
Partial<UseUpdateMutateParams<RecordType>>,
473-
unknown
474-
> & { mutationMode?: MutationMode; returnPromise?: TReturnPromise }
475-
) => Promise<TReturnPromise extends true ? RecordType : void>,
481+
UpdateMutationFunction<RecordType, TReturnPromise, MutationError>,
476482
UseMutationResult<
477483
RecordType,
478484
MutationError,

0 commit comments

Comments
 (0)