Skip to content

Commit 916e89c

Browse files
committed
Make useMutationWithMutationMode work for any dataProvider mutation function signature
1 parent 8c3751c commit 916e89c

File tree

11 files changed

+1105
-879
lines changed

11 files changed

+1105
-879
lines changed

packages/ra-core/src/controller/edit/EditBase.spec.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe('EditBase', () => {
8181
resource: 'posts',
8282
meta: undefined,
8383
},
84-
{ snapshot: [] }
84+
{ snapshot: expect.any(Array) }
8585
);
8686
});
8787
});
@@ -125,7 +125,7 @@ describe('EditBase', () => {
125125
resource: 'posts',
126126
meta: undefined,
127127
},
128-
{ snapshot: [] }
128+
{ snapshot: expect.any(Array) }
129129
);
130130
});
131131
expect(onSuccess).not.toHaveBeenCalled();
@@ -162,7 +162,7 @@ describe('EditBase', () => {
162162
resource: 'posts',
163163
meta: undefined,
164164
},
165-
{ snapshot: [] }
165+
{ snapshot: expect.any(Array) }
166166
);
167167
});
168168
});
@@ -199,7 +199,7 @@ describe('EditBase', () => {
199199
resource: 'posts',
200200
meta: undefined,
201201
},
202-
{ snapshot: [] }
202+
{ snapshot: expect.any(Array) }
203203
);
204204
});
205205
expect(onError).not.toHaveBeenCalled();

packages/ra-core/src/dataProvider/useCreate.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ describe('useCreate', () => {
381381

382382
describe('middlewares', () => {
383383
it('when pessimistic, it accepts middlewares and displays result and success side effects when dataProvider promise resolves', async () => {
384+
jest.spyOn(console, 'error').mockImplementation(() => {});
384385
render(<WithMiddlewaresSuccessPessimistic timeout={10} />);
385386
screen.getByText('Create post').click();
386387
await waitFor(() => {

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

Lines changed: 142 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -89,104 +89,161 @@ export const useCreate = <
8989
const dataProvider = useDataProvider();
9090
const queryClient = useQueryClient();
9191

92-
const { mutationMode = 'pessimistic', ...mutationOptions } = options;
92+
const {
93+
mutationMode = 'pessimistic',
94+
getMutateWithMiddlewares,
95+
...mutationOptions
96+
} = options;
9397

94-
const updateCache = useEvent(
95-
(
96-
resource: string,
97-
{ data, meta }: Partial<CreateParams<RecordType>>,
98-
{ mutationMode }: { mutationMode: MutationMode },
99-
result: ResultRecordType
100-
) => {
101-
const id = mutationMode === 'pessimistic' ? result.id : data?.id;
102-
if (!id) {
103-
return undefined;
104-
}
105-
// hack: only way to tell react-query not to fetch this query for the next 5 seconds
106-
// because setQueryData doesn't accept a stale time option
107-
const now = Date.now();
108-
const updatedAt =
109-
mutationMode === 'undoable' ? now + 5 * 1000 : now;
110-
// Stringify and parse the data to remove undefined values.
111-
// If we don't do this, an update with { id: undefined } as payload
112-
// would remove the id from the record, which no real data provider does.
113-
const clonedData = JSON.parse(
114-
JSON.stringify(mutationMode === 'pessimistic' ? result : data)
115-
);
98+
const dataProviderCreate = useEvent((resource: string, params) =>
99+
dataProvider
100+
.create<
101+
RecordType,
102+
ResultRecordType
103+
>(resource, params as CreateParams<RecordType>)
104+
.then(({ data }) => data)
105+
);
116106

117-
queryClient.setQueryData(
118-
[resource, 'getOne', { id: String(id), meta }],
119-
(record: RecordType) => ({ ...record, ...clonedData }),
120-
{ updatedAt }
121-
);
107+
const [mutate, mutationResult] = useMutationWithMutationMode<
108+
MutationError,
109+
ResultRecordType,
110+
UseCreateMutateParams<RecordType>
111+
>(
112+
{ resource, ...params },
113+
{
114+
...mutationOptions,
115+
mutationKey: [resource, 'create', params],
116+
mutationMode,
117+
mutationFn: ({ resource, ...params }) => {
118+
if (resource == null) {
119+
throw new Error('useCreate mutation requires a resource');
120+
}
121+
if (params == null) {
122+
throw new Error('useCreate mutation requires parameters');
123+
}
124+
return dataProviderCreate(resource, params);
125+
},
126+
updateCache: (
127+
{ resource, ...params },
128+
{ mutationMode },
129+
result
130+
) => {
131+
const id =
132+
mutationMode === 'pessimistic'
133+
? result?.id
134+
: params.data?.id;
135+
if (!id) {
136+
return undefined;
137+
}
138+
// hack: only way to tell react-query not to fetch this query for the next 5 seconds
139+
// because setQueryData doesn't accept a stale time option
140+
const now = Date.now();
141+
const updatedAt =
142+
mutationMode === 'undoable' ? now + 5 * 1000 : now;
143+
// Stringify and parse the data to remove undefined values.
144+
// If we don't do this, an update with { id: undefined } as payload
145+
// would remove the id from the record, which no real data provider does.
146+
const clonedData = JSON.parse(
147+
JSON.stringify(
148+
mutationMode === 'pessimistic' ? result : params.data
149+
)
150+
);
122151

123-
return clonedData;
124-
}
125-
);
152+
queryClient.setQueryData(
153+
[resource, 'getOne', { id: String(id), meta: params.meta }],
154+
(record: RecordType) => ({ ...record, ...clonedData }),
155+
{ updatedAt }
156+
);
126157

127-
const getSnapshot = useEvent(
128-
(
129-
resource: string,
130-
{ data, meta }: Partial<CreateParams<RecordType>>,
131-
{ mutationMode }: { mutationMode: MutationMode }
132-
) => {
133-
const queryKeys: any[] = [
134-
[resource, 'getList'],
135-
[resource, 'getInfiniteList'],
136-
[resource, 'getMany'],
137-
[resource, 'getManyReference'],
138-
];
158+
return clonedData;
159+
},
160+
getSnapshot: ({ resource, ...params }, { mutationMode }) => {
161+
const queryKeys: any[] = [
162+
[resource, 'getList'],
163+
[resource, 'getInfiniteList'],
164+
[resource, 'getMany'],
165+
[resource, 'getManyReference'],
166+
];
139167

140-
if (mutationMode !== 'pessimistic' && data?.id) {
141-
queryKeys.push([
142-
resource,
143-
'getOne',
144-
{ id: String(data.id), meta },
145-
]);
146-
}
168+
if (mutationMode !== 'pessimistic' && params.data?.id) {
169+
queryKeys.push([
170+
resource,
171+
'getOne',
172+
{ id: String(params.data.id), meta: params.meta },
173+
]);
174+
}
147175

148-
/**
149-
* Snapshot the previous values via queryClient.getQueriesData()
150-
*
151-
* The snapshotData ref will contain an array of tuples [query key, associated data]
152-
*
153-
* @example
154-
* [
155-
* [['posts', 'getOne', { id: '1' }], { id: 1, title: 'Hello' }],
156-
* [['posts', 'getList'], { data: [{ id: 1, title: 'Hello' }], total: 1 }],
157-
* [['posts', 'getMany'], [{ id: 1, title: 'Hello' }]],
158-
* ]
159-
*
160-
* @see https://react-query-v3.tanstack.com/reference/QueryClient#queryclientgetqueriesdata
161-
*/
162-
const snapshot = queryKeys.reduce(
163-
(prev, queryKey) =>
164-
prev.concat(queryClient.getQueriesData({ queryKey })),
165-
[] as Snapshot
166-
);
176+
/**
177+
* Snapshot the previous values via queryClient.getQueriesData()
178+
*
179+
* The snapshotData ref will contain an array of tuples [query key, associated data]
180+
*
181+
* @example
182+
* [
183+
* [['posts', 'getOne', { id: '1' }], { id: 1, title: 'Hello' }],
184+
* [['posts', 'getList'], { data: [{ id: 1, title: 'Hello' }], total: 1 }],
185+
* [['posts', 'getMany'], [{ id: 1, title: 'Hello' }]],
186+
* ]
187+
*
188+
* @see https://react-query-v3.tanstack.com/reference/QueryClient#queryclientgetqueriesdata
189+
*/
190+
const snapshot = queryKeys.reduce(
191+
(prev, queryKey) =>
192+
prev.concat(queryClient.getQueriesData({ queryKey })),
193+
[] as Snapshot
194+
);
167195

168-
return snapshot;
196+
return snapshot;
197+
},
198+
getMutateWithMiddlewares: mutationFn => args => {
199+
// This is necessary to avoid breaking changes in useCreate:
200+
// The mutation function must have the same signature as before (resource, params) and not ({ resource, params })
201+
if (getMutateWithMiddlewares) {
202+
const { resource, ...params } = args;
203+
return getMutateWithMiddlewares(
204+
dataProviderCreate.bind(dataProvider)
205+
)(resource, params);
206+
}
207+
return mutationFn(args);
208+
},
209+
onUndo: ({ resource, data, meta }) => {
210+
queryClient.removeQueries({
211+
queryKey: [
212+
resource,
213+
'getOne',
214+
{ id: String(data?.id), meta },
215+
],
216+
exact: true,
217+
});
218+
},
169219
}
170220
);
171221

172-
const onUndo = useEvent(
173-
(resource: string, { data, meta }: CreateParams<RecordType>) => {
174-
queryClient.removeQueries({
175-
queryKey: [resource, 'getOne', { id: String(data.id), meta }],
176-
exact: true,
177-
});
222+
const create = useEvent(
223+
(
224+
callTimeResource: string | undefined = resource,
225+
callTimeParams: Partial<CreateParams<RecordType>> = {},
226+
callTimeOptions: MutateOptions<
227+
ResultRecordType,
228+
MutationError,
229+
Partial<UseCreateMutateParams<RecordType>>,
230+
unknown
231+
> & {
232+
mutationMode?: MutationMode;
233+
returnPromise?: boolean;
234+
} = {}
235+
) => {
236+
return mutate(
237+
{
238+
resource: callTimeResource,
239+
...callTimeParams,
240+
},
241+
callTimeOptions
242+
);
178243
}
179244
);
180245

181-
return useMutationWithMutationMode(resource, params, {
182-
...mutationOptions,
183-
mutationKey: [resource, 'create', params],
184-
mutationMode,
185-
mutationFn: dataProvider.create.bind(dataProvider),
186-
updateCache,
187-
getSnapshot,
188-
onUndo,
189-
});
246+
return [create, mutationResult];
190247
};
191248

192249
export interface UseCreateMutateParams<

0 commit comments

Comments
 (0)