Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions packages/ra-core/src/dataProvider/useCreate.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { render, waitFor, screen } from '@testing-library/react';
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
import expect from 'expect';
import { QueryClient, useMutationState } from '@tanstack/react-query';

import { RaRecord } from '../types';
import { testDataProvider } from './testDataProvider';
Expand All @@ -25,7 +26,7 @@ import {
WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable,
WithMiddlewaresError as WithMiddlewaresErrorUndoable,
} from './useCreate.undoable.stories';
import { QueryClient, useMutationState } from '@tanstack/react-query';
import { MutationMode, Params } from './useCreate.stories';

describe('useCreate', () => {
it('returns a callback that can be used with create arguments', async () => {
Expand Down Expand Up @@ -76,6 +77,44 @@ describe('useCreate', () => {
});
});

it('uses the latest declaration time mutationMode', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
// This story uses the pessimistic mode by default
render(<MutationMode />);
fireEvent.click(screen.getByText('Change mutation mode to optimistic'));
fireEvent.click(screen.getByText('Create post'));
// Should display the optimistic result right away if the change was handled
await waitFor(() => {
expect(screen.queryByText('success')).not.toBeNull();
expect(screen.queryByText('Hello World')).not.toBeNull();
expect(screen.queryByText('mutating')).not.toBeNull();
});
await waitFor(() => {
expect(screen.queryByText('mutating')).toBeNull();
});
expect(screen.queryByText('success')).not.toBeNull();
expect(screen.queryByText('Hello World')).not.toBeNull();
});

it('uses the latest declaration time params', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
// This story sends the Hello World title by default
render(<Params />);
fireEvent.click(screen.getByText('Change params'));
fireEvent.click(screen.getByText('Create post'));
// Should have changed the title to Goodbye World
await waitFor(() => {
expect(screen.queryByText('success')).not.toBeNull();
expect(screen.queryByText('Goodbye World')).not.toBeNull();
expect(screen.queryByText('mutating')).not.toBeNull();
});
await waitFor(() => {
expect(screen.queryByText('mutating')).toBeNull();
});
expect(screen.queryByText('success')).not.toBeNull();
expect(screen.queryByText('Goodbye World')).not.toBeNull();
});

it('uses call time params over hook time params', async () => {
const dataProvider = testDataProvider({
create: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)),
Expand Down
210 changes: 210 additions & 0 deletions packages/ra-core/src/dataProvider/useCreate.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import * as React from 'react';
import { QueryClient, useIsMutating } from '@tanstack/react-query';

import { CoreAdminContext } from '../core';
import { useCreate } from './useCreate';
import { useGetOne } from './useGetOne';
import type { MutationMode as MutationModeType } from '../types';

export default { title: 'ra-core/dataProvider/useCreate' };

export const MutationMode = ({ timeout = 1000 }) => {
const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }];
const dataProvider = {
getOne: (resource, params) => {
return new Promise((resolve, reject) => {
const data = posts.find(p => p.id === params.id);
setTimeout(() => {
if (!data) {
reject(new Error('nothing yet'));
}
resolve({ data });
}, timeout);
});
},
create: (resource, params) => {
return new Promise(resolve => {
setTimeout(() => {
posts.push(params.data);
resolve({ data: params.data });
}, timeout);
});
},
} as any;
return (
<CoreAdminContext
queryClient={
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
}
dataProvider={dataProvider}
>
<MutationModeCore />
</CoreAdminContext>
);
};

const MutationModeCore = () => {
const isMutating = useIsMutating();
const [success, setSuccess] = React.useState<string>();
const [mutationMode, setMutationMode] =
React.useState<MutationModeType>('pessimistic');

const {
isPending: isPendingGetOne,
data,
error,
refetch,
} = useGetOne('posts', { id: 2 });
const [create, { isPending }] = useCreate(
'posts',
{
data: { id: 2, title: 'Hello World' },
},
{
mutationMode,
onSuccess: () => setSuccess('success'),
}
);
const handleClick = () => {
create();
};
return (
<>
{isPendingGetOne ? (
<p>Loading...</p>
) : error ? (
<p>{error.message}</p>
) : (
<dl>
<dt>id</dt>
<dd>{data?.id}</dd>
<dt>title</dt>
<dd>{data?.title}</dd>
<dt>author</dt>
<dd>{data?.author}</dd>
</dl>
)}
<div>
<button onClick={handleClick} disabled={isPending}>
Create post
</button>
&nbsp;
<button
onClick={() => setMutationMode('optimistic')}
disabled={isPending}
>
Change mutation mode to optimistic
</button>
&nbsp;
<button onClick={() => refetch()}>Refetch</button>
</div>
{success && <div>{success}</div>}
{isMutating !== 0 && <div>mutating</div>}
</>
);
};

export const Params = ({ timeout = 1000 }) => {
const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }];
const dataProvider = {
getOne: (resource, params) => {
return new Promise((resolve, reject) => {
const data = posts.find(p => p.id === params.id);
setTimeout(() => {
if (!data) {
reject(new Error('nothing yet'));
}
resolve({ data });
}, timeout);
});
},
create: (resource, params) => {
return new Promise(resolve => {
setTimeout(() => {
posts.push(params.data);
resolve({ data: params.data });
}, timeout);
});
},
} as any;
return (
<CoreAdminContext
queryClient={
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
}
dataProvider={dataProvider}
>
<ParamsCore />
</CoreAdminContext>
);
};

const ParamsCore = () => {
const isMutating = useIsMutating();
const [success, setSuccess] = React.useState<string>();
const [params, setParams] = React.useState<any>({ title: 'Hello World' });

const {
isPending: isPendingGetOne,
data,
error,
refetch,
} = useGetOne('posts', { id: 2 });
const [create, { isPending }] = useCreate(
'posts',
{
data: { id: 2, ...params },
},
{
mutationMode: 'optimistic',
onSuccess: () => setSuccess('success'),
}
);
const handleClick = () => {
create();
};
return (
<>
{isPendingGetOne ? (
<p>Loading...</p>
) : error ? (
<p>{error.message}</p>
) : (
<dl>
<dt>id</dt>
<dd>{data?.id}</dd>
<dt>title</dt>
<dd>{data?.title}</dd>
<dt>author</dt>
<dd>{data?.author}</dd>
</dl>
)}
<div>
<button onClick={handleClick} disabled={isPending}>
Create post
</button>
&nbsp;
<button
onClick={() => setParams({ title: 'Goodbye World' })}
disabled={isPending}
>
Change params
</button>
&nbsp;
<button onClick={() => refetch()}>Refetch</button>
</div>
{success && <div>{success}</div>}
{isMutating !== 0 && <div>mutating</div>}
</>
);
};
9 changes: 8 additions & 1 deletion packages/ra-core/src/dataProvider/useCreate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import {
useMutation,
UseMutationOptions,
Expand Down Expand Up @@ -97,9 +97,16 @@ export const useCreate = <
...mutationOptions
} = options;
const mode = useRef<MutationMode>(mutationMode);
useEffect(() => {
mode.current = mutationMode;
}, [mutationMode]);

const paramsRef =
useRef<Partial<CreateParams<Partial<RecordType>>>>(params);
useEffect(() => {
paramsRef.current = params;
}, [params]);

const snapshot = useRef<Snapshot>([]);

// Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@ export const SuccessCase = () => {
{ id: 2, title: 'World' },
];
const dataProvider = {
getList: (resource, params) => {
console.log('getList', resource, params);
getList: () => {
return Promise.resolve({
data: posts,
total: posts.length,
});
},
delete: (resource, params) => {
console.log('delete', resource, params);
delete: (_, params) => {
return new Promise(resolve => {
setTimeout(() => {
const index = posts.findIndex(p => p.id === params.id);
Expand Down Expand Up @@ -82,15 +80,13 @@ export const ErrorCase = () => {
{ id: 2, title: 'World' },
];
const dataProvider = {
getList: (resource, params) => {
console.log('getList', resource, params);
getList: () => {
return Promise.resolve({
data: posts,
total: posts.length,
});
},
delete: (resource, params) => {
console.log('delete', resource, params);
delete: () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('something went wrong'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ export const SuccessCase = () => {
{ id: 2, title: 'World' },
];
const dataProvider = {
getList: (resource, params) => {
console.log('getList', resource, params);
getList: () => {
return Promise.resolve({
data: posts,
total: posts.length,
});
},
delete: (resource, params) => {
console.log('delete', resource, params);
delete: (_, params) => {
return new Promise(resolve => {
setTimeout(() => {
const index = posts.findIndex(p => p.id === params.id);
Expand Down Expand Up @@ -181,15 +179,13 @@ export const ErrorCase = () => {
{ id: 2, title: 'World' },
];
const dataProvider = {
getList: (resource, params) => {
console.log('getList', resource, params);
getList: () => {
return Promise.resolve({
data: posts,
total: posts.length,
});
},
delete: (resource, params) => {
console.log('delete', resource, params);
delete: () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('something went wrong'));
Expand Down
Loading
Loading