Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Fix query component does not fetch again when updated #3146

Merged
merged 2 commits into from
Apr 19, 2019
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
123 changes: 113 additions & 10 deletions packages/ra-core/src/util/Query.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
cleanup,
fireEvent,
// @ts-ignore
waitForDomChange
waitForDomChange,
} from 'react-testing-library';
import expect from 'expect';
import Query from './Query';
Expand Down Expand Up @@ -36,7 +36,11 @@ describe('Query', () => {
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
return (
<Query type="mytype" resource="myresource" payload={myPayload}>
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
{() => <div>Hello</div>}
</Query>
);
Expand All @@ -55,8 +59,16 @@ describe('Query', () => {
const { getByText } = render(
<TestContext>
{() => (
<Query type="mytype" resource="myresource" payload={myPayload}>
{({ loading }) => <div className={loading ? 'loading' : 'idle'}>Hello</div>}
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
{({ loading }) => (
<div className={loading ? 'loading' : 'idle'}>
Hello
</div>
)}
</Query>
)}
</TestContext>
Expand All @@ -66,11 +78,16 @@ describe('Query', () => {

it('should update the data state after a success response', async () => {
const dataProvider = jest.fn();
dataProvider.mockImplementationOnce(() => Promise.resolve({ data: { foo: 'bar' } }));
dataProvider.mockImplementationOnce(() =>
Promise.resolve({ data: { foo: 'bar' } })
);
const Foo = () => (
<Query type="mytype" resource="foo">
{({ loading, data }) => (
<div data-testid="test" className={loading ? 'loading' : 'idle'}>
<div
data-testid="test"
className={loading ? 'loading' : 'idle'}
>
{data ? data.foo : 'no data'}
</div>
)}
Expand All @@ -91,12 +108,17 @@ describe('Query', () => {

it('should return the total prop if available', async () => {
const dataProvider = jest.fn();
dataProvider.mockImplementationOnce(() => Promise.resolve({ data: [{ foo: 'bar' }], total: 42 }));
dataProvider.mockImplementationOnce(() =>
Promise.resolve({ data: [{ foo: 'bar' }], total: 42 })
);

const Foo = () => (
<Query type="mytype" resource="foo">
{({ loading, data, total }) => (
<div data-testid="test" className={loading ? 'loading' : 'idle'}>
<div
data-testid="test"
className={loading ? 'loading' : 'idle'}
>
{loading ? 'no data' : total}
</div>
)}
Expand All @@ -120,11 +142,16 @@ describe('Query', () => {

it('should update the error state after an error response', async () => {
const dataProvider = jest.fn();
dataProvider.mockImplementationOnce(() => Promise.reject({ message: 'provider error' }));
dataProvider.mockImplementationOnce(() =>
Promise.reject({ message: 'provider error' })
);
const Foo = () => (
<Query type="mytype" resource="foo">
{({ loading, error }) => (
<div data-testid="test" className={loading ? 'loading' : 'idle'}>
<div
data-testid="test"
className={loading ? 'loading' : 'idle'}
>
{error ? error.message : 'no data'}
</div>
)}
Expand All @@ -142,4 +169,80 @@ describe('Query', () => {
expect(testElement.textContent).toEqual('provider error');
expect(testElement.className).toEqual('idle');
});

it('should dispatch a new fetch action when updating', () => {
let dispatchSpy;
const myPayload = {};
const { rerender } = render(
<TestContext>
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
return (
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
{() => <div>Hello</div>}
</Query>
);
}}
</TestContext>
);
const mySecondPayload = { foo: 1 };
rerender(
<TestContext>
{() => (
<Query
type="mytype"
resource="myresource"
payload={mySecondPayload}
>
{() => <div>Hello</div>}
</Query>
)}
</TestContext>
);
expect(dispatchSpy.mock.calls.length).toEqual(2);
const action = dispatchSpy.mock.calls[1][0];
expect(action.type).toEqual('CUSTOM_FETCH');
expect(action.payload).toEqual(mySecondPayload);
expect(action.meta.fetch).toEqual('mytype');
expect(action.meta.resource).toEqual('myresource');
});

it('should not dispatch a new fetch action when updating with the same query props', () => {
let dispatchSpy;
const myPayload = {};
const { rerender } = render(
<TestContext>
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
return (
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
{() => <div>Hello</div>}
</Query>
);
}}
</TestContext>
);
rerender(
<TestContext>
{() => (
<Query
type="mytype"
resource="myresource"
payload={myPayload}
>
{() => <div>Hello</div>}
</Query>
)}
</TestContext>
);
expect(dispatchSpy.mock.calls.length).toEqual(1);
});
});
31 changes: 26 additions & 5 deletions packages/ra-core/src/util/Query.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Component, ReactNode } from 'react';
import { shallowEqual } from 'recompose';
import withDataProvider from './withDataProvider';

type DataProviderCallback = (type: string, resource: string, payload?: any, options?: any) => Promise<any>;
type DataProviderCallback = (
type: string,
resource: string,
payload?: any,
options?: any
) => Promise<any>;

interface ChildrenFuncParams {
data?: any;
Expand Down Expand Up @@ -72,27 +78,42 @@ class Query extends Component<Props, State> {
data: null,
total: null,
loading: true,
error: null
error: null,
};

componentDidMount = () => {
callDataProvider = () => {
const { dataProvider, type, resource, payload, options } = this.props;
dataProvider(type, resource, payload, options)
.then(({ data, total }) => {
this.setState({
data,
total,
loading: false
loading: false,
});
})
.catch(error => {
this.setState({
error,
loading: false
loading: false,
});
});
};

componentDidMount = () => {
this.callDataProvider();
};

componentDidUpdate = prevProps => {
if (
prevProps.type !== this.props.type ||
prevProps.resource !== this.props.resource ||
!shallowEqual(prevProps.payload, this.props.payload) ||
!shallowEqual(prevProps.options, this.props.options)
) {
this.callDataProvider();
}
};

render() {
const { children } = this.props;
return children(this.state);
Expand Down
52 changes: 30 additions & 22 deletions packages/ra-core/src/util/TestContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { Component } from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { reducer as formReducer } from 'redux-form';
Expand Down Expand Up @@ -47,29 +47,37 @@ interface Props {
* </TestContext>
* );
*/
const TestContext: SFC<Props> = ({
store = {},
enableReducers = false,
children,
}) => {
const storeWithDefault = enableReducers
? createAdminStore({
initialState: merge(defaultStore, store),
dataProvider: () => Promise.resolve({}),
history: createMemoryHistory(),
})
: createStore(() => merge(defaultStore, store));
class TestContext extends Component<Props> {
storeWithDefault = null;

const renderChildren = () =>
typeof children === 'function'
? children({ store: storeWithDefault })
constructor(props) {
super(props);
const { store = {}, enableReducers = false } = props;
this.storeWithDefault = enableReducers
? createAdminStore({
initialState: merge(defaultStore, store),
dataProvider: () => Promise.resolve({}),
history: createMemoryHistory(),
})
: createStore(() => merge(defaultStore, store));
}

renderChildren = () => {
const { children } = this.props;
return typeof children === 'function'
? children({ store: this.storeWithDefault })
: children;
};

return (
<Provider store={storeWithDefault}>
<TranslationProvider>{renderChildren()}</TranslationProvider>
</Provider>
);
};
render() {
return (
<Provider store={this.storeWithDefault}>
<TranslationProvider>
{this.renderChildren()}
</TranslationProvider>
</Provider>
);
}
}

export default TestContext;