export default graphql(queryShippingRates, {
options: props => ({
variables: {
id: props.navigation.state.params.checkout.id
},
pollInterval: 5000,
client: {
apolloclient: props.navigation.state.params.apolloclient
}
}),
props: ({
data
}) => {
if (data.loading) {
return {
fetchNextPage: () => {}
};
} else if (!data.node.availableShippingRates.ready) {
return {
fetchNextPage: () => {}
};
} else if (!data) {
return {
fetchNextPage: () => {}
};
} else if (data.error) {
console.warn('error', data.error);
}
const fetchNextPage = () => {
return data.fetchMore({
updateQuery: (previousResult, {
fetchMoreResult
}) => {
return {
node: fetchMoreResult.data.node,
};
},
});
};
console.log(fetchNextPage);
return {
data: data,
};
},
})(CheckoutShippingMethodScreen);
I am using the code above to send a graphQL query via react-apollo. As you can see above, I placed a prop within options to specify which client to use when sending the Query. When I run the code, I get an error saying that this.client.watchQuery is not a function.
I have the app wrapped by an ApolloProvider Component with a specified client, but I read that you can place a custom client as a prop. Can someone please help me with this?
Related
I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?
import { act, renderHook } from "#testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");
describe('Testing custom hooks of react query', () => {
it('Should add a new color', async () => {
axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
await act(() => {
result.current.mutate({ label: 'Grey' })
})
await waitFor(() => result.current.isSuccess);
})
})
export const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
}
});
export function createWrapper() {
const testQueryClient = createTestQueryClient();
return ({ children }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
export const useAddColorHook = (page) => {
const queryClient = useQueryClient()
return useMutation(addColor, {
// onSuccess: () => {
// queryClient.invalidateQueries(['colors', page])
// }
onMutate: async color => {
// newHero refers to the argument being passed to the mutate function
await queryClient.cancelQueries(['colors', page])
const previousHeroData = queryClient.getQueryData(['colors', page])
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
return { previousHeroData }
},
onSuccess: (response, variables, context) => {
queryClient.setQueryData(['colors', page], (oldQueryData) => {
console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
return {
...oldQueryData,
data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
}
})
},
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['colors', page], context.previousHeroData)
},
onSettled: () => {
queryClient.invalidateQueries(['colors', page])
}
})
}
The error that you are getting actually shows a bug in the way you've implemented the optimistic update:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.
This is what happens in your test because you start with a fresh query cache for every test.
An easy way out would be, since you have previousHeroData already:
const previousHeroData = queryClient.getQueryData(['colors', page])
if (previousHeroData) {
queryClient.setQueryData(['colors', page], {
...previousHeroData,
data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
}
}
If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return oldQueryData ? {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
} : undefined
})
This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.
I'm trying to implement backpressure logic in my react application.
I find a nice post about this here and trying to add this to my app.
Now i have some code:
// epic.ts
import { ofType } from 'redux-observable';
import { mapTo, tap, delay, switchMap } from 'rxjs/operators';
import { createIteratorSubject } from './createIteratorSubject';
// fake generator which in real application is supposed to pull data from a server
function* generator() {
yield 1;
yield 2;
yield Promise.resolve(3);
yield Promise.resolve(4);
yield 5;
}
const iterator$ = createIteratorSubject(generator());
export function epic(action$: any): any {
return action$.pipe(
ofType('TAKE'),
switchMap(() => {
return iterator$
.pipe(
tap((value) => console.info('INCOMING VALUE', value)),
delay(1000), // Some hard calculations here
tap((value) => console.info('DONE PROCESSING VALUE', value))
)
.subscribe({
next: iterator$.push,
complete: () => {
console.info('DONE PROCESSING ALL VALUES');
},
});
}),
mapTo((value: number) => {
return { type: 'PUT', payload: value };
})
);
}
// createIteratorSubject.ts
import { BehaviorSubject } from 'rxjs';
export function createIteratorSubject(iterator: any) {
const iterator$ = new BehaviorSubject();
const pushNextValue = async ({ done, value }: any) => {
if (done && value === undefined) {
iterator$.complete();
} else {
iterator$.next(await value);
}
};
iterator$.push = (value: any) => {
return pushNextValue(iterator.next(value));
};
iterator$.push();
return iterator$;
}
The problem i'm faced with is i don't know how to dispatch result value to redux. And now i have following error.
You're returning a Subscription in your switchMap where an ObservableInput is expected. You can change your code like this to make it work:
export function epic(action$: any): any {
return action$.pipe(
ofType('TAKE'),
switchMap(() => {
return iterator$
.pipe(
tap((value) => console.info('INCOMING VALUE', value)),
delay(1000), // Some hard calculations here
tap((value) => console.info('DONE PROCESSING VALUE', value)),
tap({
next: iterator$.push,
complete: () => {
console.info('DONE PROCESSING ALL VALUES');
}
})
);
}),
map((value: number) => {
return { type: 'PUT', payload: value };
})
);
}
I currently have a context provider.
componentDidMount() {
if (this.state.memberID === null) {
try {
this.checkAuthUser();
} catch (e) {
console.error(e);
}
}
}
checkAuthUser = () => {
new Promise((resolve, reject) => {
this.props.firebase.auth.onAuthStateChanged(authUser => {
if(authUser) {
resolve(authUser);
} else {
reject(new Error("Not authorized"));
}
})
})
.then( authDetails => {
this.props.firebase.getOrgID(authDetails.uid).on('value', snapshot => {
const setSnapshot = snapshot.val();
const getOrganizationID = Object.keys(setSnapshot)[0];
this.setState({ memberID: authDetails.uid, orgID: getOrganizationID })
})
})
.catch(err => console.log(err))
}
When I try to use this in another component:
static contextType = AuthDetailsContext;
componentDidMount() {
console.log('here is context: ' + this.context.orgID);
if(this.context.orgID) {
this.setState({currentOrganization: this.context.orgID, loading: true}, () => {
this.getMembersInDB('1');
})
}
}
My console.log is null. Means the context isn't registering yet. Any idea what I'm doing wrong?
Your design here seems flawed i.e. when your provider is mounted you send the API request and then when your descendant component is mounted you try to use it - these operations will happen in quick succession, far quicker than it would take for an API call to return from a server.
In your provider, if you must have a user before the component mounts then you need to delay rendering the child components until your API response completes i.e.
const AuthDetailsContext = React.createContext(null);
class AuthDetailsProvider extends PureComponent {
...
componentDidMount() {
const { firebase } = this.props;
firebase.auth.onAuthStateChanged(authUser => {
if (!authUser) {
// Maybe set some other state state to inform the user?
this.setState({ authError: new Error('Not Authorised') });
return;
}
firebase.getOrgID(authUser.uid)
.on('value', snapshot => {
const setSnapshot = snapshot.val();
const getOrganizationID = Object.keys(setSnapshot)[0];
this.setState({
authError: null,
memberID: authUsermemberID.uid,
orgID: getOrganizationID
});
});
})
}
render() {
if (this.state.authError) return <b style={{ color: red }}>{this.state.error.message}</b>;
if (!this.state.memberID) return <b>Authenticating...</b>
return (
<AuthDetailsContext.Provider value={this.state}>
{this.props.children}
</AuthDetailsContext.Provider>
);
}
}
I have a list of todo items and I would like to receive notifications when items are added or deleted from the list.
So far I have implemented item addition notification:
<Connect
query={graphqlOperation(listTodos)}
subscription={graphqlOperation(onCreateTodo)}
onSubscriptionMsg={(prev, { onCreateTodo }) => {
return addItem(prev, onCreateTodo)
}}
>
{({ data: { listTodos }, loading, error }) => {
if (loading) return "Loading"
if (error) return "Error"
return listTodos.items
.map(({ id, name }) => <div key={id}>{name}</div>)
}}
</Connect>
Now I am wondering, how can I add item deletion notification to this component? Does subscription attribute accept an array of graphql operations?
Thanks!
You can use multiple Connect components in your component.
<Connect query={graphqlOperation(listTodos)}>
{({ data: { listTodos }, loading, error }) => {
if (loading) return "Loading"
if (error) return "Error"
return listTodos.items.map(({ id, name }) => <div key={id}>{name}</div>)
}}
</Connect>
<Connect
subscription={graphqlOperation(subscriptions.onCreateTodo)}
onSubscriptionMsg={(prev, { onCreateTodo }) => {
// Do something
return prev;
}}
>
{() => {}}
</Connect>
<Connect
subscription={graphqlOperation(subscriptions.onUpdateTodo)}
onSubscriptionMsg={(prev, { onUpdateTodo }) => {
// Do something
return prev;
}}
>
{() => {}}
</Connect>
Looks like the current solution is to create your own implementation of the Connect component as described in this github issue: https://github.com/aws-amplify/amplify-js/issues/4813#issuecomment-582106596
I tried the Connect.ts version above and got the same errors reported by others in this thread. So I created a version whereby you can pass in multiple subscriptions as an array - you can still pass in single a subscription too - as per the original version. Note: this version takes only a single query and a single onSubscriptionMessage - however your onSubscriptionMessage can be a wrapper function that examines the newData passed into it and calls the appropriate update depending on this data like this:
const onSubscriptionMessage = (prevQuery, newData) => {
if(newData && newData.onDeleteItem) {
return onRemoveItem(prevQuery, newData);
}
if(newData && newData.onCreateItem) {
return onAddItem(prevQuery, newData);
}
};
Connect.ts for multiple subscriptions given a single query and a single onSubscriptionMessage handler that switches handling according to the newData.
import * as React from 'react';
import { API, GraphQLResult } from '#aws-amplify/api';
import Observable from 'zen-observable-ts';
export interface IConnectProps {
mutation?: any;
onSubscriptionMsg?: (prevData: any) => any;
query?: any;
subscription?: any;
}
export interface IConnectState {
loading: boolean;
data: any;
errors: any;
mutation: any;
}
export class Connect extends React.Component<IConnectProps, IConnectState> {
public subSubscriptions: Array<Promise<GraphQLResult<object>> | Observable<object>>;
private mounted: boolean = false;
constructor(props:any) {
super(props);
this.state = this.getInitialState();
this.subSubscriptions = [];
}
getInitialState() {
const { query } = this.props;
return {
loading: query && !!query.query,
data: {},
errors: [],
mutation: () => console.warn('Not implemented'),
};
}
getDefaultState() {
return {
loading: false,
data: {},
errors: [],
mutation: () => console.warn('Not implemented'),
};
}
async _fetchData() {
this._unsubscribe();
this.setState({ loading: true });
const {
// #ts-ignore
query: { query, variables = {} } = {},
//#ts-ignore
mutation: { query: mutation,
//eslint-disable-next-line
mutationVariables = {} } = {},
subscription,
onSubscriptionMsg = (prevData:any) => prevData,
} = this.props;
let { data, mutation: mutationProp, errors } = this.getDefaultState();
if (
!API ||
typeof API.graphql !== 'function' ||
typeof API.getGraphqlOperationType !== 'function'
) {
throw new Error(
'No API module found, please ensure #aws-amplify/api is imported'
);
}
const hasValidQuery =
query && API.getGraphqlOperationType(query) === 'query';
const hasValidMutation =
mutation && API.getGraphqlOperationType(mutation) === 'mutation';
const validSubscription = (subscription:any) => subscription
&& subscription.query
&& API.getGraphqlOperationType(subscription.query) === 'subscription';
const validateSubscriptions = (subscription:any) => {
let valid = false;
if(Array.isArray(subscription)) {
valid = subscription.map(validSubscription).indexOf(false) === -1;
} else {
valid = validSubscription(subscription)
}
return valid;
};
const hasValidSubscriptions = validateSubscriptions(subscription);
if (!hasValidQuery && !hasValidMutation && !hasValidSubscriptions) {
console.warn('No query, mutation or subscription was specified correctly');
}
if (hasValidQuery) {
try {
// #ts-ignore
data = null;
const response = await API.graphql({ query, variables });
// #ts-ignore
data = response.data;
} catch (err) {
data = err.data;
errors = err.errors;
}
}
if (hasValidMutation) {
// #ts-ignore
mutationProp = async variables => {
const result = await API.graphql({ query: mutation, variables });
this.forceUpdate();
return result;
};
}
if (hasValidSubscriptions) {
// #ts-ignore
const connectSubscriptionToOnSubscriptionMessage = (subscription) => {
// #ts-ignore
const {query: subsQuery, variables: subsVars} = subscription;
try {
const observable = API.graphql({
query: subsQuery,
variables: subsVars,
});
// #ts-ignore
this.subSubscriptions.push(observable.subscribe({
// #ts-ignore
next: ({value: {data}}) => {
const {data: prevData} = this.state;
// #ts-ignore
const newData = onSubscriptionMsg(prevData, data);
if (this.mounted) {
this.setState({data: newData});
}
},
error: (err:any) => console.error(err),
}));
} catch (err) {
errors = err.errors;
}
};
if(Array.isArray(subscription)) {
subscription.forEach(connectSubscriptionToOnSubscriptionMessage);
} else {
connectSubscriptionToOnSubscriptionMessage(subscription)
}
}
this.setState({ data, errors, mutation: mutationProp, loading: false });
}
_unsubscribe() {
const __unsubscribe = (subSubscription:any) => {
if (subSubscription) {
subSubscription.unsubscribe();
}
};
this.subSubscriptions.forEach(__unsubscribe);
}
async componentDidMount() {
this._fetchData();
this.mounted = true;
}
componentWillUnmount() {
this._unsubscribe();
this.mounted = false;
}
componentDidUpdate(prevProps:any) {
const { loading } = this.state;
const { query: newQueryObj, mutation: newMutationObj, subscription: newSubscription} = this.props;
const { query: prevQueryObj, mutation: prevMutationObj, subscription: prevSubscription } = prevProps;
// query
// #ts-ignore
const { query: newQuery, variables: newQueryVariables } = newQueryObj || {};
// #ts-ignore
const { query: prevQuery, variables: prevQueryVariables } =
prevQueryObj || {};
const queryChanged =
prevQuery !== newQuery ||
JSON.stringify(prevQueryVariables) !== JSON.stringify(newQueryVariables);
// mutation
// #ts-ignore
const { query: newMutation, variables: newMutationVariables } =
newMutationObj || {};
// #ts-ignore
const { query: prevMutation, variables: prevMutationVariables } =
prevMutationObj || {};
const mutationChanged =
prevMutation !== newMutation ||
JSON.stringify(prevMutationVariables) !==
JSON.stringify(newMutationVariables);
// subscription
// #ts-ignore
const { query: newSubsQuery, variables: newSubsVars } = newSubscription || {};
// #ts-ignore
const { query: prevSubsQuery, variables: prevSubsVars } = prevSubscription || {};
const subscriptionChanged =
prevSubsQuery !== newSubsQuery ||
JSON.stringify(prevSubsVars) !==
JSON.stringify(newSubsVars);
if (!loading && (queryChanged || mutationChanged || subscriptionChanged)) {
this._fetchData();
}
}
render() {
const { data, loading, mutation, errors } = this.state;
// #ts-ignore
return this.props.children({ data, errors, loading, mutation }) || null;
}
}
/**
* #deprecated use named import
*/
export default Connect;
Usage: an example of onSubscriptionMessage is at the top of this post.
<Connect
query={graphqlOperation(listTopics)}
subscription={[graphqlOperation(onCreateTopic), graphqlOperation(onDeleteTopic)]}
onSubscriptionMsg={onSubscriptionMessage}>
{.....}
</Connect>
need test below code, how can i mock the setTimeout callback
import { message } from 'antd';
const showMessage = ({ type = 'info', text }) => {
message.destroy();
setTimeout(() => {
message[type](text); // this line can't covered
}, 10);
};
export function error(text) {
showMessage({ type: 'error', text });
}
export function info(text) {
showMessage({ type: 'info', text });
}
You can use jest.runAllTimers() :
jest.mock('antd');
const { message } = require('antd');
const { error } = require('../message');
describe('src/shared/utils/message', () => {
describe('#error', () => {
it('should call message.error', () => {
error('dummy');
jest.runAllTimers();
expect(message.error).toBeCalledWith('dummy');
}
}
}