Mocking the fetching state of URQL in Jest - reactjs

I'm trying to mock the fetching state in my tests. The state with data and state with an error were successfully mocked. Below you can find an example:
const createMenuWithGraphQL = (graphqlData: MockGraphQLResponse): [JSX.Element, MockGraphQLClient] => {
const mockGraphQLClient = {
executeQuery: jest.fn(() => fromValue(graphqlData)),
executeMutation: jest.fn(() => never),
executeSubscription: jest.fn(() => never),
};
return [
<GraphQLProvider key="provider" value={mockGraphQLClient}>
<Menu {...menuConfig} />
</GraphQLProvider>,
mockGraphQLClient,
];
};
it('displays submenu with section in loading state after clicking on an item with children', async () => {
const [Component, client] = createMenuWithGraphQL({
fetching: true,
error: false,
});
const { container } = render(Component);
const itemConfig = menuConfig.links[0];
fireEvent.click(screen.getByText(label));
await waitFor(() => {
const alertItem = screen.getByRole('alert');
expect(client.executeQuery).toBeCalledTimes(1);
expect(container).toContainElement(alertItem);
expect(alertItem).toHaveAttribute('aria-busy', 'false');
});
});
The component looks like the following:
export const Component: FC<Props> = ({ label, identifier }) => {
const [result] = useQuery({ query });
return (
<div role="group">
{result.fetching && (
<p role="alert" aria-busy={true}>
Loading
</p>
)}
{result.error && (
<p role="alert" aria-busy={false}>
Error
</p>
)}
{!result.fetching && !result.error && <p>Has data</p>}
</div>
);
};
I have no idea what am I doing wrong. When I run the test it says the fetching is false. Any help would be appreciated.

maintainer here,
I think the issue here is that you are synchronously returning in executeQuery. Let's look at what happens.
Your component mounts and checks whether or not there is synchronous state in cache.
This is the case since we only do executeQuery: jest.fn(() => fromValue(graphqlData)),
We can see that this actually comes down to the same result as if the result would just come out of cache (we never need to hit the API so we never hit fetching).
We could solve this by adding a setTimeout, ... but Wonka (the streaming library used by urql) has built-in solutions for this.
We can utilise the delay operator to simulate a fetching state:
const mockGraphQLClient = {
executeQuery: jest.fn(() => pipe(fromValue(data), delay(100)),
};
Now we can check the fetching state correctly in the first 100ms and after that we are able to utilise waitFor to check the subsequent state.

Related

Refetching queries does not work - React Query

I'm trying to refetch some queries after one success but it's not working!
I used two ways to handle it by using refetchQueries() / invalidateQueries()
1- onSuccess callback
export const useMutateAcceptedOrder = () => {
const queryClient = useQueryClient();
return useMutation(
['AcceptedOrder'],
(bodyQuery: AcceptedOrderProps) => acceptOrder(bodyQuery),
{
onSuccess: () => {
console.log('success, refetch now!');
queryClient.invalidateQueries(['getNewOrders']); // not work
queryClient.refetchQueries(['getNewOrders']); // not work
},
onError: () => {
console.error('err');
queryClient.invalidateQueries(['getNewOrders']); // not work
},
},
);
};
second way
const {mutateAsync: onAcceptOrder, isLoading} = useMutateAcceptedOrder();
const acceptOrder = async (orderId: string) => {
const body = {
device: 'iPhone',
version: '1.0.0',
location_lat: '10.10',
location_lng: '10.10',
orderId: orderId,
os: Platform.OS,
source: 'mobile',
token: userInfo.token,
};
await onAcceptOrder(body);
queryClient.refetchQueries(['getNewOrders']); // not work
queryClient.invalidateQueries(['getActiveOrders']); // not work
handleClosetModalPress();
};
sample of query I wanted to refetch after the success
export const useNewOrders = (bodyQuery: {token: string | null}) => {
console.log('token>>', bodyQuery.token);
return useQuery(['getNewOrders'], () => getNewOrders(bodyQuery),
{
enabled: bodyQuery.token != null,
});
};
App.tsx
const App: React.FC<AppProps> = ({}) => {
const queryClient = new QueryClient();
if (__DEV__) {
import('react-query-native-devtools').then(({addPlugin}) => {
console.log('addPlugin');
addPlugin({queryClient});
});
}
useEffect(() => {
RNBootSplash.hide({fade: true}); // fade
}, []);
return (
<GestureHandlerRootView style={{flex: 1}}>
<QueryClientProvider client={queryClient}>
<BottomSheetModalProvider>
<AppContainer />
</BottomSheetModalProvider>
</QueryClientProvider>
</GestureHandlerRootView>
);
};
export default App;
--
EDIT
So after using the react-query-native-devtools Debug tool, I can't see any query in the first tab recorded in the debugger! Although the data fetched well.
So I guess that's why the refetch did not work in this case!
Any query in the first tab I can't refetch it again
Steps to reproduce:
open App - Active Tab (first tab)
check the status of the queries
nothing recorded in the debugger
Navigate to any other screen/tab
Check the status of queries
all screen queries recorded in the debugger
Per https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable:
If you move the client creation into the App component, and your component re-renders for some other reason (e.g. a route change), your cache will be thrown away:
Need to init queryClient like that:
const [queryClient] = React.useState(() => new QueryClient())
I was facing the same issue and haven't been able to find a proper solution for this, however I have found a hacky workaround. You just call your refetch function or invalidate queries inside a setTimeout.

useEffect to rerender this component

I am trying to use useEffect to rerender postList (to make it render without the deleted post) when postsCount change, but I can't get it right. I tried to wrap everything inside useEffect but I couldn't execute addEventListener("click", handlePost) because I am using useEffect to wait for this component to mount first, before attaching the evenListener.
Parent component:
function Tabs() {
const [posts, setPosts] = useState([]);
const dispatch = useDispatch();
const postsCount = useSelector((state) => state.posts.count);
useEffect(() => {
document.getElementById("postsTab").addEventListener("click", handlePost);
}, [handlePost]);
const handlePost = async (e) => {
const { data: { getPosts: postData }} = await refetchPosts();
setPosts(postData);
dispatch(postActions.getPostsReducer(postData));
};
const { data: FetchedPostsData, refetch: refetchPosts } = useQuery( FETCH_POSTS_QUERY, { manual: true });
const [postList, setPostsList] = useState({});
useEffect(() => {
setPostsList(
<Tab.Pane>
<Grid>
<Grid.Column>Title</Grid.Column>
{posts.map((post) => (
<AdminPostsList key={post.id} postId={post.id} />
))}
</Grid>
</Tab.Pane>
);
console.log("changed"); //it prints "changed" everytime postCount changes (or everytime I click delete), but the component doesn't remount
}, [postsCount]);
const panes = [
{ menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
];
return (<Tab panes={panes} />);
}
child/AdminPostsList component:
function AdminPostsList(props) {
const { postId } = props;
const [deletePost] = useMutation(DELETE_POST_MUTATION, {variables: { postId } });
const dispatch = useDispatch();
const deletePostHandler = async () => {
dispatch(postActions.deletePost(postId));
await deletePost();
};
return (
<>
<Button icon="delete" onClick={deletePostHandler}></Button>
</>
);
}
The Reducers
const PostSlice = createSlice({
name: "storePosts",
initialState: {
content: [],
count: 0,
},
reducers: {
getPostsReducer: (state, action) => {
state.content = action.payload;
state.count = action.payload.length
},
deletePost: (state, action) => {
const id = action.payload
state.content = current(state).content.filter((post) => (post.id !== id))
state.count--
}
},
});
Okay, let discuss this in separate comment. Key point is to decouple posts logic from wrapper component(Tabs). You should create component dedicated only to posts and render it in wrapper. Like that you can easily isolate all posts-related logic in posts-related component, for example to avoid attaching some listeners from wrapper(because it is not intuitive what you are doing and who listens for what because button is not in that same component). In separated component you will have only one useEffect, to fetch posts, and you will have one selector(to select posts from redux), and then just use that selection to output content from component.
That part <Tab panes={...} /> was the source of most of your problems, because like that you are forced to solve everything above <Tab../> and then just to pass it, which is not best practice in you case since it can be too complicated(especially in case when you could have multiple tabs). That is why you need to decouple and to create tab-specific components.
This would be an idea of how you should refactor it:
function PostsTab() {
const posts = useSelector((state) => state.posts?.content ?? []);
useEffect(() => {
// Here dispatch action to load your posts
// With this approach, when you have separated component for PostsTab no need to attach some weird event listeners, you can do everything here in effect
// This should be triggered only once
// You can maybe introduce 'loading' flag in your reducer so you can display some loaders for better UX
}, []);
return (
<div>
{/* Here use Tab components in order to create desired tab */}
<Tab.Pane>
<Grid>
<Grid.Column>Title</Grid.Column>
{posts.map((post) => (
<AdminPostsList key={post.id} postId={post.id} />
))}
</Grid>
</Tab.Pane>
</div>
);
}
function Tabs() {
return (
<div>
<PostsTab/>
{/** HERE you can add more tabs when you need to
* Point is to create separate component per tab so you can isolate and maintain tab state in dedicated component
and to avoid writing all logic here in wrapper component
* As you can see there is no need to attach any weird listener, everything related to posts is moved to PostsTab component
*/}
</div>
);
}
Ok, let's discuss what I did wrong for the future reader:
There is no need to use this weird spaghetti
useEffect(() => {
document.getElementById("postsTab").addEventListener("click", handlePost);
}, [handlePost]);
const panes = [
{ menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
];
for I could've used a <Menu.Item onClick={handleClick}>Posts</Menu.Item> to attach the onClick directly.
I had to use useEffect to monitor posts dependency, but .map() will automatically update its content if the array I am mapping had any changes so there is no need to use it use useEffect in this context.
I think I can use lifting state to setPosts from the child component and the change will trigger .map() to remap and pop the deleted element, but I couldn't find a way to so, so I am using a combination of redux (to store the posts) and useEffect to dispatch the posts to the store than I am mapping over the stored redux element, idk if this is the best approach but this is all I managed to do.
The most important thing I didn't notice when I almost tried everything is, I must update apollo-cache when adding/deleting a post, by using proxy.readQuery
this is how I did it
const [posts, setPosts] = useState([]);
const handlePosts = async () => {
const { data: { getPosts: postData } } = await refetchPosts();
setPosts(postData);
};
const handlePosts = async () => {
const { data } = await refetchPosts();
setPosts(data.getPosts);
};
// Using useEffect temporarily to make it work.
// Will replace it with an lifting state when refactoring later.
useEffect(() => {
posts && dispatch(postsActions.PostsReducer(posts))
}, [posts]);
const [deletePost] = useMutation(DELETE_POST_MUTATION, {
update(proxy) {
let data = proxy.readQuery({
query: FETCH_POSTS_QUERY,
});
// Reconstructing data, filtering the deleted post
data = { getPosts: data.getPosts.filter((post) => post.id !== postId) };
// Rewriting apollo-cache
proxy.writeQuery({ query: FETCH_POSTS_QUERY, data });
},
onError(err) {
console.log(err);
},
variables: { postId },
});
const deletePostHandler = async () => {
deletePost();
dispatch(postsActions.deletePost(postId))
};
Thanks to #Anuj Panwar #Milos Pavlovic for helping out, kudos to #Cptkrush for bringing the store idea into my attention

Why does my state affect the emitted events from a click handler?

I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}

React-Query: How to useQuery when button is clicked

I am new to this react-query library.
I know that when I want to fetch data, with this library I can do something like this:
const fetchData = async()=>{...}
// it starts fetching data from backend with this line of code
const {status, data, error} = useQuery(myKey, fetchData());
It works. But how to trigger the data fetching only when a button is clicked? , I know I probably could do something like <Button onPress={() => {useQuery(myKey, fetchData())}}/> , but how to manage the returned data and status...
According to the API Reference, you need to change the enabled option to false to disable a query from automatically running. Then you refetch manually.
// emulates a fetch (useQuery expects a Promise)
const emulateFetch = _ => {
return new Promise(resolve => {
resolve([{ data: "ok" }]);
});
};
const handleClick = () => {
// manually refetch
refetch();
};
const { data, refetch } = useQuery("my_key", emulateFetch, {
refetchOnWindowFocus: false,
enabled: false // disable this query from automatically running
});
return (
<div>
<button onClick={handleClick}>Click me</button>
{JSON.stringify(data)}
</div>
);
Working sandbox here
 
Bonus: you can pass anything that returns a boolean to enabled.
That way you could create Dependant/Serial queries.
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', user.id],
getProjectsByUser,
{
// `user` would be `null` at first (falsy),
// so the query will not execute until the user exists
enabled: user,
}
)
You have to pass the manual: true parameter option so the query doesn't fetch on mount. Also, you should pass fetchData without the parentheses, so you pass the function reference and not the value.
To call the query you use refetch().
const {status, data, error, refetch} = useQuery(myKey, fetchData, {
manual: true,
});
const onClick = () => { refetch() }
Refer to the manual querying section on the react-query docs for more info
https://github.com/tannerlinsley/react-query#manual-querying
Looks like the documentation changed and is missing the manual querying section right now. Looking at the useQuery API however, you'd probably need to set enabled to false, and then use refetch to manually query when the button is pressed. You also might want to use force: true to have it query regardless of data freshness.
You can try this version:
const fetchData = async()=>{...}
// it starts fetching data from backend with this line of code
const {status, data, error, refetch } = useQuery(
myKey,
fetchData(),
{
enabled: false,
}
);
const onClick = () => { refetch() }
// then use onClick where you need it
From documentation Doc:
enabled: boolean
Set this to false to disable this query from automatically running.
Can be used for Dependent Queries.
refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>
A function to manually refetch the query.
If the query errors, the error will only be logged. If you want an error to be thrown, pass the throwOnError: true option
If cancelRefetch is true, then the current request will be cancelled before a new request is made
There is another way to do this that also works if you want to trigger multiple refetches.
const [fetch, setFetch] = useState(null);
const query = useQuery(["endpoint", fetch], fetchData);
const refetch = () => setFetch(Date.now());
// call the refetch when handling click.
If you want to refetch multiple entities you could have a top level useState that is called for instance fetchAll and:
...
const query = useQuery(["endpoint", fetch, fetchAll], fetchData);
...
and this code will also trigger if you press a button to fetch all.
At first react query gives us enabled option and by default it is true
const fetchData = async()=>{...}
const {status, data, error , refetch} = useQuery(myKey, fetchData() , {
enabled : false
}
);
<button onClick={() => refetch()}>Refetch</button>
If the key is the same, then use refetch(), if the key is different then use useState to trigger the query.
For example:
const [productId, setProductId] = useState<string>('')
const {status, data, error, refetch} = useQuery(productId, fetchData, {
enable: !!productId,
});
const onClick = (id) => {
if(productId === id) {
refetch()
}
else {
setProductId(id)
}
}
you can use useLazyQuery()
import React from 'react';
import { useLazyQuery } from '#apollo/client';
function DelayedQuery() {
const [getDog, { loading, error, data }] = useLazyQuery(GET_DOG_PHOTO);
if (loading) return <p>Loading ...</p>;
if (error) return `Error! ${error}`;
return (
<div>
{data?.dog && <img src={data.dog.displayImage} />}
<button onClick={() => getDog({ variables: { breed: 'bulldog' } })}>Click me!</button>
</div>
);
}
reference: https://www.apollographql.com/docs/react/data/queries/#manual-execution-with-uselazyquery

Setting state with React Hooks + Fetch API does not trigger re-render

I'm trying to learn React Hooks by building a simple Domain Availability checker. Specifically, I'm playing with useState()
The aim is just to have an input field where the user types a keyword, hits Enter, and then the app will run a fetch request for that keyword with a number of different domain endings.
Here is my App component (or check codesandbox here: https://codesandbox.io/s/5410rkrq1p)
const App = () => {
const domainEndings = [
".ws",
".ga",
".cf",
".tk",
".ml",
".gq",
".kz",
".st",
".fm",
".je"
];
const [domainString, setDomainString] = useState("");
const [domainsArray, setDomainsArray] = useState([]);
const [lookedUpDomainsArray, setLookedUpDomainsArray] = useState([]);
const handleDomainChange = event => {
setDomainString(event.target.value);
setDomainsArray(
domainEndings.map(ending => event.target.value.trim() + ending)
);
};
let testArray = [];
const runDomainLookup = url => {
return fetch(
`https://domainr.p.rapidapi.com/v2/status?domain=${url}&mashape-key=${myAPIKEY}`
)
.then(res => res.json())
.then(data => {
testArray.push({
url: data.status[0].domain,
status: data.status[0].status
});
setLookedUpDomainsArray(testArray);
});
};
const handleSubmit = e => {
e.preventDefault();
setLookedUpDomainsArray([]);
testArray = [];
domainsArray.map(eachDomain => runDomainLookup(eachDomain));
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={domainString}
placeholder="type keyword then hit enter"
onChange={e => handleDomainChange(e)}
/>
</form>
{lookedUpDomainsArray &&
lookedUpDomainsArray.map((domain, index) => {
return (
<div key={index}>
{domain.url} is {domain.status}
</div>
);
})}
</div>
);
};
The bug that I'm experiencing is:
the state seems to be being set correctly (checked in React Dev Tools).
the first response from the mapped fetch requests is rendered correctly to the DOM
a re-render for the newly added state (from fetch request) is not triggered until the user presses a key in the input field
Here is a video demonstrating it : https://streamable.com/klshu
You will notice that the rest of the results don't appear until I start typing other characters into the input
Thanks in advance 🙏
Thanks! Yes I had tried Promise.all() however I wanted each fetch request to run separately (for example, if one fetch request times out, it would hold up ALL of the others).
Found a solution on Reddit
Essentially, when setting the state via Hooks, I should have passed in the previous state as so:
const runDomainLookup = url => {
return fetch(
`https://domainr.p.rapidapi.com/v2/status?domain=${url}&mashape-key=${myAPIKEY}`
)
.then(res => res.json())
.then(data => {
setLookedUpDomainsArray(prevArray => [
...prevArray,
{
url: data.status[0].domain,
status: data.status[0].status
}
]);
});
};
Working solution on CodeSandbox: https://codesandbox.io/s/51ky3r63x

Resources