Skip to content

Commit 68d4fb8

Browse files
authored
Merge pull request #10857 from marmelab/use-mutation-refs
Make all mutations react to their declaration time options changes
2 parents e850b2e + 0bab27a commit 68d4fb8

18 files changed

+1152
-46
lines changed

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
2-
import { render, waitFor, screen } from '@testing-library/react';
2+
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
33
import expect from 'expect';
4+
import { QueryClient, useMutationState } from '@tanstack/react-query';
45

56
import { RaRecord } from '../types';
67
import { testDataProvider } from './testDataProvider';
@@ -25,7 +26,7 @@ import {
2526
WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable,
2627
WithMiddlewaresError as WithMiddlewaresErrorUndoable,
2728
} from './useCreate.undoable.stories';
28-
import { QueryClient, useMutationState } from '@tanstack/react-query';
29+
import { MutationMode, Params } from './useCreate.stories';
2930

3031
describe('useCreate', () => {
3132
it('returns a callback that can be used with create arguments', async () => {
@@ -76,6 +77,44 @@ describe('useCreate', () => {
7677
});
7778
});
7879

80+
it('uses the latest declaration time mutationMode', async () => {
81+
jest.spyOn(console, 'error').mockImplementation(() => {});
82+
// This story uses the pessimistic mode by default
83+
render(<MutationMode />);
84+
fireEvent.click(screen.getByText('Change mutation mode to optimistic'));
85+
fireEvent.click(screen.getByText('Create post'));
86+
// Should display the optimistic result right away if the change was handled
87+
await waitFor(() => {
88+
expect(screen.queryByText('success')).not.toBeNull();
89+
expect(screen.queryByText('Hello World')).not.toBeNull();
90+
expect(screen.queryByText('mutating')).not.toBeNull();
91+
});
92+
await waitFor(() => {
93+
expect(screen.queryByText('mutating')).toBeNull();
94+
});
95+
expect(screen.queryByText('success')).not.toBeNull();
96+
expect(screen.queryByText('Hello World')).not.toBeNull();
97+
});
98+
99+
it('uses the latest declaration time params', async () => {
100+
jest.spyOn(console, 'error').mockImplementation(() => {});
101+
// This story sends the Hello World title by default
102+
render(<Params />);
103+
fireEvent.click(screen.getByText('Change params'));
104+
fireEvent.click(screen.getByText('Create post'));
105+
// Should have changed the title to Goodbye World
106+
await waitFor(() => {
107+
expect(screen.queryByText('success')).not.toBeNull();
108+
expect(screen.queryByText('Goodbye World')).not.toBeNull();
109+
expect(screen.queryByText('mutating')).not.toBeNull();
110+
});
111+
await waitFor(() => {
112+
expect(screen.queryByText('mutating')).toBeNull();
113+
});
114+
expect(screen.queryByText('success')).not.toBeNull();
115+
expect(screen.queryByText('Goodbye World')).not.toBeNull();
116+
});
117+
79118
it('uses call time params over hook time params', async () => {
80119
const dataProvider = testDataProvider({
81120
create: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)),
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import * as React from 'react';
2+
import { QueryClient, useIsMutating } from '@tanstack/react-query';
3+
4+
import { CoreAdminContext } from '../core';
5+
import { useCreate } from './useCreate';
6+
import { useGetOne } from './useGetOne';
7+
import type { MutationMode as MutationModeType } from '../types';
8+
9+
export default { title: 'ra-core/dataProvider/useCreate' };
10+
11+
export const MutationMode = ({ timeout = 1000 }) => {
12+
const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }];
13+
const dataProvider = {
14+
getOne: (resource, params) => {
15+
return new Promise((resolve, reject) => {
16+
const data = posts.find(p => p.id === params.id);
17+
setTimeout(() => {
18+
if (!data) {
19+
reject(new Error('nothing yet'));
20+
}
21+
resolve({ data });
22+
}, timeout);
23+
});
24+
},
25+
create: (resource, params) => {
26+
return new Promise(resolve => {
27+
setTimeout(() => {
28+
posts.push(params.data);
29+
resolve({ data: params.data });
30+
}, timeout);
31+
});
32+
},
33+
} as any;
34+
return (
35+
<CoreAdminContext
36+
queryClient={
37+
new QueryClient({
38+
defaultOptions: {
39+
queries: { retry: false },
40+
mutations: { retry: false },
41+
},
42+
})
43+
}
44+
dataProvider={dataProvider}
45+
>
46+
<MutationModeCore />
47+
</CoreAdminContext>
48+
);
49+
};
50+
51+
const MutationModeCore = () => {
52+
const isMutating = useIsMutating();
53+
const [success, setSuccess] = React.useState<string>();
54+
const [mutationMode, setMutationMode] =
55+
React.useState<MutationModeType>('pessimistic');
56+
57+
const {
58+
isPending: isPendingGetOne,
59+
data,
60+
error,
61+
refetch,
62+
} = useGetOne('posts', { id: 2 });
63+
const [create, { isPending }] = useCreate(
64+
'posts',
65+
{
66+
data: { id: 2, title: 'Hello World' },
67+
},
68+
{
69+
mutationMode,
70+
onSuccess: () => setSuccess('success'),
71+
}
72+
);
73+
const handleClick = () => {
74+
create();
75+
};
76+
return (
77+
<>
78+
{isPendingGetOne ? (
79+
<p>Loading...</p>
80+
) : error ? (
81+
<p>{error.message}</p>
82+
) : (
83+
<dl>
84+
<dt>id</dt>
85+
<dd>{data?.id}</dd>
86+
<dt>title</dt>
87+
<dd>{data?.title}</dd>
88+
<dt>author</dt>
89+
<dd>{data?.author}</dd>
90+
</dl>
91+
)}
92+
<div>
93+
<button onClick={handleClick} disabled={isPending}>
94+
Create post
95+
</button>
96+
&nbsp;
97+
<button
98+
onClick={() => setMutationMode('optimistic')}
99+
disabled={isPending}
100+
>
101+
Change mutation mode to optimistic
102+
</button>
103+
&nbsp;
104+
<button onClick={() => refetch()}>Refetch</button>
105+
</div>
106+
{success && <div>{success}</div>}
107+
{isMutating !== 0 && <div>mutating</div>}
108+
</>
109+
);
110+
};
111+
112+
export const Params = ({ timeout = 1000 }) => {
113+
const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }];
114+
const dataProvider = {
115+
getOne: (resource, params) => {
116+
return new Promise((resolve, reject) => {
117+
const data = posts.find(p => p.id === params.id);
118+
setTimeout(() => {
119+
if (!data) {
120+
reject(new Error('nothing yet'));
121+
}
122+
resolve({ data });
123+
}, timeout);
124+
});
125+
},
126+
create: (resource, params) => {
127+
return new Promise(resolve => {
128+
setTimeout(() => {
129+
posts.push(params.data);
130+
resolve({ data: params.data });
131+
}, timeout);
132+
});
133+
},
134+
} as any;
135+
return (
136+
<CoreAdminContext
137+
queryClient={
138+
new QueryClient({
139+
defaultOptions: {
140+
queries: { retry: false },
141+
mutations: { retry: false },
142+
},
143+
})
144+
}
145+
dataProvider={dataProvider}
146+
>
147+
<ParamsCore />
148+
</CoreAdminContext>
149+
);
150+
};
151+
152+
const ParamsCore = () => {
153+
const isMutating = useIsMutating();
154+
const [success, setSuccess] = React.useState<string>();
155+
const [params, setParams] = React.useState<any>({ title: 'Hello World' });
156+
157+
const {
158+
isPending: isPendingGetOne,
159+
data,
160+
error,
161+
refetch,
162+
} = useGetOne('posts', { id: 2 });
163+
const [create, { isPending }] = useCreate(
164+
'posts',
165+
{
166+
data: { id: 2, ...params },
167+
},
168+
{
169+
mutationMode: 'optimistic',
170+
onSuccess: () => setSuccess('success'),
171+
}
172+
);
173+
const handleClick = () => {
174+
create();
175+
};
176+
return (
177+
<>
178+
{isPendingGetOne ? (
179+
<p>Loading...</p>
180+
) : error ? (
181+
<p>{error.message}</p>
182+
) : (
183+
<dl>
184+
<dt>id</dt>
185+
<dd>{data?.id}</dd>
186+
<dt>title</dt>
187+
<dd>{data?.title}</dd>
188+
<dt>author</dt>
189+
<dd>{data?.author}</dd>
190+
</dl>
191+
)}
192+
<div>
193+
<button onClick={handleClick} disabled={isPending}>
194+
Create post
195+
</button>
196+
&nbsp;
197+
<button
198+
onClick={() => setParams({ title: 'Goodbye World' })}
199+
disabled={isPending}
200+
>
201+
Change params
202+
</button>
203+
&nbsp;
204+
<button onClick={() => refetch()}>Refetch</button>
205+
</div>
206+
{success && <div>{success}</div>}
207+
{isMutating !== 0 && <div>mutating</div>}
208+
</>
209+
);
210+
};

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useRef } from 'react';
1+
import { useEffect, useMemo, useRef } from 'react';
22
import {
33
useMutation,
44
UseMutationOptions,
@@ -97,9 +97,16 @@ export const useCreate = <
9797
...mutationOptions
9898
} = options;
9999
const mode = useRef<MutationMode>(mutationMode);
100+
useEffect(() => {
101+
mode.current = mutationMode;
102+
}, [mutationMode]);
100103

101104
const paramsRef =
102105
useRef<Partial<CreateParams<Partial<RecordType>>>>(params);
106+
useEffect(() => {
107+
paramsRef.current = params;
108+
}, [params]);
109+
103110
const snapshot = useRef<Snapshot>([]);
104111

105112
// Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted

packages/ra-core/src/dataProvider/useDelete.optimistic.stories.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@ export const SuccessCase = () => {
1414
{ id: 2, title: 'World' },
1515
];
1616
const dataProvider = {
17-
getList: (resource, params) => {
18-
console.log('getList', resource, params);
17+
getList: () => {
1918
return Promise.resolve({
2019
data: posts,
2120
total: posts.length,
2221
});
2322
},
24-
delete: (resource, params) => {
25-
console.log('delete', resource, params);
23+
delete: (_, params) => {
2624
return new Promise(resolve => {
2725
setTimeout(() => {
2826
const index = posts.findIndex(p => p.id === params.id);
@@ -82,15 +80,13 @@ export const ErrorCase = () => {
8280
{ id: 2, title: 'World' },
8381
];
8482
const dataProvider = {
85-
getList: (resource, params) => {
86-
console.log('getList', resource, params);
83+
getList: () => {
8784
return Promise.resolve({
8885
data: posts,
8986
total: posts.length,
9087
});
9188
},
92-
delete: (resource, params) => {
93-
console.log('delete', resource, params);
89+
delete: () => {
9490
return new Promise((resolve, reject) => {
9591
setTimeout(() => {
9692
reject(new Error('something went wrong'));

packages/ra-core/src/dataProvider/useDelete.pessimistic.stories.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@ export const SuccessCase = () => {
1616
{ id: 2, title: 'World' },
1717
];
1818
const dataProvider = {
19-
getList: (resource, params) => {
20-
console.log('getList', resource, params);
19+
getList: () => {
2120
return Promise.resolve({
2221
data: posts,
2322
total: posts.length,
2423
});
2524
},
26-
delete: (resource, params) => {
27-
console.log('delete', resource, params);
25+
delete: (_, params) => {
2826
return new Promise(resolve => {
2927
setTimeout(() => {
3028
const index = posts.findIndex(p => p.id === params.id);
@@ -181,15 +179,13 @@ export const ErrorCase = () => {
181179
{ id: 2, title: 'World' },
182180
];
183181
const dataProvider = {
184-
getList: (resource, params) => {
185-
console.log('getList', resource, params);
182+
getList: () => {
186183
return Promise.resolve({
187184
data: posts,
188185
total: posts.length,
189186
});
190187
},
191-
delete: (resource, params) => {
192-
console.log('delete', resource, params);
188+
delete: () => {
193189
return new Promise((resolve, reject) => {
194190
setTimeout(() => {
195191
reject(new Error('something went wrong'));

0 commit comments

Comments
 (0)