Test useSelector value change post-mount - reactjs

I have a React component which displays a message when a user goes offline and then another message when the user goes back online. The online/offline status is handled by another part of the codebase and the values are stored on the Redux store.
The component uses the useSelector hook to get the offline status like this:
const isOffline = useSelector(deviceSelectors.isOffline);
and I am using a useEffect hook in order to update the status and display the different DOM like this:
useEffect(() => {
if (isOffline) {
setUserHasBeenOffline(true);
if (userWentOnline) {
setUserWentOnline(false);
}
}
if (userHasBeenOffline && !isOffline) {
setUserWentOnline(true);
setUserHasBeenOffline(false);
}
}, [isOffline, userHasBeenOffline, userWentOnline]);
Now I am trying to test the codepath when the user has been offline and then they come back online.
I've got a perfectly working test like this for the initial codepath:
const setupMount = setupWithProvider({
render: () => <NetworkStatus />,
[[deviceSelectors.isOffline, false]],
initialState: {
device: {
offline: false,
},
},
});
it('should render the network status alert for when the user has gone offline', () => {
const { wrapper } = setupMount({
selectors: [[deviceSelectors.isOffline, true]],
});
expect(wrapper.find('...').exists()).toBeTruthy();
});
My question is, how can I update the selector's value after mount? It doesn't seem that this information is easily accessible on the internet, as I've looked a lot.
What I've tried:
I've tried destructuring dispatch too and dispatch the action I need to update the selector's return value indirectly, but setupWithProvider's dispatch is a mock function. And I couldn't find a working way with this.
I checked if the property was available to the wrapper as a prop to update it, but it was not.

Related

How to invalidate react-query whenever state is changed?

I'm trying to refetch my user data with react-query whenever a certain state is changed. But ofcourse I can't use a hook within a hook so i can't figure out how to set a dependency on this state.
Current code to fetch user is:
const {data: userData, error: userError, status: userStatus} = useQuery(['user', wallet], context => getUserByWallet(context.queryKey[1]));
This works fine. But I need this to be invalidated whenever the gobal state wallet is changed. Figured I could make something like
useEffect(
() => {
useQueryClient().invalidateQueries(
{ queryKey: ['user'] }
)
},
[wallet]
)
but this doesn't work because useQueryClient is a hook and can't be called within a callback.
Any thoughts on how to fix this?
General idea is wallet can change in the app at any time which can be connected to a different user. So whenever wallet state is changed this user needs to be fetched.
thanks
useQueryClient returns object, which you can use later.
For example:
const queryClient = useQueryClient()
useEffect(
() => {
queryClient.invalidateQueries(
{ queryKey: ['user'] }
)
},
[wallet]
)

How to use zustand to store the result of a query

I want to put the authenticated user in a zustand store. I get the authenticated user using react-query and that causes some problems. I'm not sure why I'm doing this. I want everything related to authentication can be accessed in a hook, so I thought zustand was a good choice.
This is the hook that fetches auth user:
const getAuthUser = async () => {
const { data } = await axios.get<AuthUserResponse>(`/auth/me`, {
withCredentials: true,
});
return data.user;
};
export const useAuthUserQuery = () => {
return useQuery("auth-user", getAuthUser);
};
And I want to put auth user in this store:
export const useAuthStore = create(() => ({
authUser: useAuthUserQuery(),
}));
This is the error that I get:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons.
you can read about it in the react documentation:
https://reactjs.org/warnings/invalid-hook-call-warning.html
(I changed the name of some functions in this post for the sake of understandability. useMeQuery = useAuthUserQuery)
I understand the error but I don't know how to fix it.
The misunderstanding here is that you don’t need to put data from react query into any other state management solution. React query is in itself a global state manager. You can just do:
const { data } = useAuthUserQuery()
in every component that needs the data. React query will automatically try to keep your data updated with background refetches. If you don’t need that for your resource, consider setting a staleTime.
—-
That being said, if you really want to put data from react-query into zustand, create a setter in zustand and call it in the onSuccess callback of the query:
useQuery(key, queryFn, { onSuccess: data => setToZustand(data) })

React hooks - dependencies rerun hooks

I keep struggling with the same issue with React Hooks. The dependency array.
I have many hooks that should trigger and handle different events.
For instance:
useEffect(() => {
const doSomethingWith = (notification: Notifications.Notification) => {
...
setUser({ notifications: badgeCount });
};
notificationListener.current = Notifications.addNotificationReceivedListener(
(notification) => {
if (user) {
doSomethingWith(notification);
}
}
);
return () => {
Notifications.removeNotificationSubscription(notificationListener);
};
}, [setExpoPushToken, setUser, user]);
In this simplified code, you can see that I track notifications and when I get it, I use the data in the notification and update the user object with it.
However, this whole thing will run whenever the user is updated. Like, if I update the user first name - the notification hook will run. That makes little sense to me. It forces me to add if statements inside these hook functions which is a waste and makes my code ugly.
What am I missing? How do I handle this better?

Jest / React Testing Library: Is it Possible to Spy on and Wait GraphQL Queries as with Cypress?

Most of my testing experience is with Cypress. I'm attempting to test some React components with React Testing Library (henceforth referred to as "RTL"). I'm wondering if I can apply some of the techniques I use with Cypress.
I have a section of my (web) app which displays a table of search results from a DB. The search API is implemented in GraphQL. We use Apollo on the frontend. Searches can be filtered vis-a-vis a <FilterCheckbox/> component. It includes four checkboxes and a "Clear All" button. The checkbox state is stored in the Apollo cache.
I'm writing a spec for this component. I wish to test that the first two checkboxes are selected by default and that clicking "Clear Filters" deselects them all. I have the following:
import React from 'react'
import FilterCheckboxes from './FilterCheckboxes'
import { render, fireEvent } from 'Utils/test-utils'
import {
getByText,
getByTestId
} from '#testing-library/dom'
const props = {
options: [
{ label: 'new', value: 'FRESH' },
{ label: 'active', value: 'ACTIVE' },
{ label: 'closed', value: 'CLOSED' },
{ label: 'removed', value: 'ARCHIVED' },
],
statusArray: [
'FRESH',
'ACTIVE',
],
cacheKey: 'status',
}
describe('FilterCheckboxes component', () => {
const { container } = render(<FilterCheckboxes {...props} />)
const checkNew = getByTestId(container,'checkbox_new')
.querySelector('input[type="checkbox"]')
const checkActive = getByTestId(container, 'checkbox_active')
.querySelector('input[type="checkbox"]')
const checkClosed = getByTestId(container,'checkbox_closed' )
.querySelector('input[type="checkbox"]')
const checkRemoved = getByTestId(container, 'checkbox_removed')
.querySelector('input[type="checkbox"]')
const clearButton = getByText(container, 'clear status filters')
it('should render the checkboxes with the correct default state', () => {
expect(checkNew).toHaveProperty('checked', true)
expect(checkActive).toHaveProperty('checked', true)
expect(checkClosed).toHaveProperty('checked', false)
expect(checkRemoved).toHaveProperty('checked', false)
})
it('Should clear all checkboxes when clear filters is clicked', () => {
fireEvent.click(clearButton)
setTimeout(() => {
expect(checkNew).toHaveProperty('checked', false)
expect(checkActive).toHaveProperty('checked', false)
expect(checkClosed).toHaveProperty('checked', false)
expect(checkRemoved).toHaveProperty('checked', false)
}, 500)
})
})
The tests pass, but the second one only passes if I do this arbitrary delay of 500ms. Without it, the checkboxes are still in the default state when the expect() calls fire.
If this were a Cypress test and the onClick called a REST API, I would do something like this:
cy.server()
cy.route('POST', '/foo/bar/v2', '#someAliasedFixture').as('fooBar')
cy.get('.my-button')
.click()
.then(() => {
cy.wait('#fooBar')
// continue test here as we know that the call completed
})
Is it possible to do something like this with Jest / RTL / GraphQL / Apollo?
Update:
Took another look this morning. It appears that waitFor() is the function intended to be used in async scenarios. However, if I do
it('should deselect checkboxes when Clear Filters is clicked', async () => {
fireEvent.click(clearButton)
waitFor(expect(checkNew).toHaveProperty('checked', false))
})
It just fails because the checkbox is still selected. Surely there is a way to test this with RTL?
Based on the examples I've looked at, the orthodox approach seems to be to pass the click handler function as a prop and expect() it .toHaveBeenCalled() called.
Is that correct? This seems wrong to me. My component isn't what makes onClick handlers fire in response to click events. React.js makes that happen. Checking that foo() gets called when clicking on <MyComponent onClick={foo}/> isn't testing that my code works. It's testing that React.js works.
SOLVED!
Thanks to #flo:
it('should render the checkboxes with the correct default state', () => {
expect(checkNew).toBeChecked()
expect(checkActive).toBeChecked()
expect(checkClosed).not.toBeChecked()
expect(checkRemoved).not.toBeChecked()
})
it('should deselect checkboxes when Clear Filters is clicked', async () => {
fireEvent.click(clearButton)
waitFor(() => {
expect(checkNew).not.toBeChecked()
expect(checkActive).not.toBeChecked()
expect(checkClosed).not.toBeChecked()
expect(checkRemoved).not.toBeChecked()
})
})
I agree, you should test as a real user would, so you should definitely test that the checkbox is in the correct state instead of testing that the click handler was called. This is what promotes react-testing-library.
Are you sure about this ?
expect(checkNew).toHaveProperty('checked', false);
I never used this, but reading the docs I'm not sure if this would do the job (https://jestjs.io/docs/en/expect#tohavepropertykeypath-value). In fact I don't see how it could work.
Have you consider using https://github.com/testing-library/jest-dom, and in particular https://github.com/testing-library/jest-dom#tobechecked ?

React-Router/Redux browser back button functionality

I'm building a 'Hacker News' clone, Live Example using React/Redux and can't get this final piece of functionality to work. I have my entire App.js wrapped in BrowserRouter, and I have withRouter imported into my components using window.history. I'm pushing my state into window.history.pushState(getState(), null, `/${getState().searchResponse.params}`) in my API call action creator. console.log(window.history.state) shows my entire application state in the console, so it's pushing in just fine. I guess. In my main component that renders the posts, I have
componentDidMount() {
window.onpopstate = function(event) {
window.history.go(event.state);
};
}
....I also tried window.history.back() and that didn't work
what happens when I press the back button is, the URL bar updates with the correct previous URL, but after a second, the page reloads to the main index URL(homepage). Anyone know how to fix this? I can't find any real documentation(or any other questions that are general and not specific to the OP's particular problem) that makes any sense for React/Redux and where to put the onpopstate or what to do insde of the onpopstate to get this to work correctly.
EDIT: Added more code below
Action Creator:
export const searchQuery = () => async (dispatch, getState) => {
(...)
if (noquery && sort === "date") {
// DATE WITH NO QUERY
const response = await algoliaSearch.get(
`/search_by_date?tags=story&numericFilters=created_at_i>${filter}&page=${page}`
);
dispatch({ type: "FETCH_POSTS", payload: response.data });
}
(...)
window.history.pushState(
getState(),
null,
`/${getState().searchResponse.params}`
);
console.log(window.history.state);
};
^^^ This logs all of my Redux state correctly to the console through window.history.state so I assume I'm implementing window.history.pushState() correctly.
PostList Component:
class PostList extends React.Component {
componentDidMount() {
window.onpopstate = () => {
window.history.back();
};
}
(...)
}
I tried changing window.history.back() to this.props.history.goBack() and didn't work. Does my code make sense? Am I fundamentally misunderstanding the History API?
withRouter HOC gives you history as a prop inside your component, so you don't use the one provided by the window.
You should be able to access the window.history even without using withRouter.
so it should be something like:
const { history } = this.props;
history.push() or history.goBack()

Resources