I have a component that uses a hook that fetches data from the server, and I have mocked that hook to return my testing data.
Now if the mutate function (returned by the hook) is called, the normal implementation fetches the data again and causes a re-render (I'm using swr, here the mutate reference).
How to I trigger a re-render / setState on a mocked hook?
What I want to test: simply, if the user creates an item the item list should be re-fetched and displayed.
Code to illustrate the issue:
const existing = [...]
const newlyCreated = [...];
useData.mockReturnValue({ data: [existing] });
const { getByRole, findByText } = render(<MyComponent />);
const form = getByRole("form");
const createButton = within(form).getByText("Create");
useData.mockReturnValue({ data: [existing, newlyCreated] });
// createButton.click();
// Somehow trigger re-render???
for (const { name } of [existing, newlyCreated]) await findByText(name);
You don't need to trigger a re-render in your test.
The issue is: the button on your UI mutates the data, but since you're mocking useData that mutation isn't happening.
You can simply add mutate() to your mock and assign it a mock function.
You don't need to unit test the inner working of SWR's own mutate() - that's already covered by their own project.
const existing = ["Mercury", "Venus", "Earth", "Mars"];
const newItem = "Jupiter";
test("Create button should call mutate() with correct value", async () => {
const mutate = jest.fn();
jest.spyOn(useData, "default").mockImplementation(() => ({
data: existing,
mutate
}));
let result;
act(() => {
result = render(<Form />);
});
await waitFor(() => {
existing.forEach((item) => {
expect(result.getByText(item)).toBeInTheDocument();
});
});
const input = result.container.querySelector("input");
fireEvent.change(input, { target: { value: newItem } });
const createButton = result.getByText("Create");
createButton.click();
expect(mutate).toBeCalledWith([...existing, newItem], false);
});
Example on CodeSandbox
Related
In a react functional component, I setState from an api call. I then create an EventSource that will update the state each time an event is received.
The problem is that in the EventSource callback, the state is not updated.
My guess is, state is printed at the creation of the callback, so it can't be updated.
Is there any other way to do it ?
function HomePage() {
const [rooms, setRooms] = useState<Room[]>([]);
const getRooms = async () => {
const data = await RoomService.getAll(currentPage);
setRooms(data.rooms);
}
const updateRooms = (data: AddRoomPayload | DeleteRoomPayload) => {
console.log(rooms); // rooms is always empty
// will append or remove a room via setRooms but i need actual rooms first
}
useEffect(() => {
// setState from api response
getRooms();
// set up EventSource
const url = new URL(process.env.REACT_APP_SSE_BASE_URL ?? '');
url.searchParams.append('topic', '/room');
const eventSource = new EventSource(url);
eventSource.onmessage = e => updateRooms(JSON.parse(e.data));
}, [])
...
}
Try using a functional update when using setRooms like this:
const updateRooms = (data: AddRoomPayload | DeleteRoomPayload) => {
setRooms((rooms) => {
if (data.type === 'add') {
return [...rooms, data.room];
} else if (data.type === 'remove') {
return rooms.filter(/* ... */);
}
});
}
Here is a reference to the React Docs on functional updates in useState: https://reactjs.org/docs/hooks-reference.html#functional-updates
If that doesn't work then try checking the React Developer Tools to make sure that the component's state rooms is being updated.
I am trying to use Firestore Snapchats to get real time changes on a database. I am using react-native-cli: 2.0.1 and react-native: 0.64.1 .
export const WelcomeScreen = observer(function WelcomeScreen() {
const [listData, setListData] = useState([]);
const onResult = (querySnapshot) => {
const items = []
firestore()
.collection("Test")
.get()
.then((querySnapshot) => {
querySnapshot.forEach(function(doc) {
const tempData = {key: doc.id, data: doc.data}
items.push(tempData);
});
setListData(items)
});
}
const onError = (error) => {
console.error(error);
}
firestore().collection('Test').onSnapshot(onResult, onError);
}
Every thing is working perfectly, until I use setListData to update the data list. The App does not respond anymore and I get a warning message each time I try to add or delete data from the database
Please report: Excessive number of pending callbacks: 501. Some pending callbacks that might have leaked by never being called from native code
I am creating a deadlock by setting the state this way?
First, you don't want to set up a snapshot listener in the body of your component. This results in a growing number of listeners, because every time you render you add a new listener, but every listener results in rendering again, etc. So set up the listener just once in a useEffect:
const [listData, setListData] = useState([]);
useEffect(() => {
function onResult(querySnapshot) {
// ...
}
function onError(error) {
console.log(error);
}
const unsubscribe = firestore().collection('Test').onSnapshot(onResult, onError);
return unsubscribe;
}, []);
In addition, your onResult function is going to get called when you get the result, and yet you're having it turn around and immediately doing a get to re-request the data it already has. Instead, just use the snapshot you're given:
function onResult(querySnapshot) {
const items = []
querySnapshot.forEach(function(doc) {
const tempData = {key: doc.id, data: doc.data()}
items.push(tempData);
});
setListData(items);
}
This is how my component look likes.
Inside the component I am making a mutation and the handleOnComplete function is invoked once the mutation query is completed.
This code is working fine.
function BrandingSearch() {
const [searchList, setSearchList] = useState([]);
const [searchQuery, { loading, error }] = useMutation(SEARCH_NO_INDEX, {
onCompleted: handleOnComplete,
variables: {
rootType: 'branding',
input: {}
}
});
useEffect(() => {
searchQuery();
}, [])
function handleOnComplete(res) {
if (res && res.searchNoIndex && res.searchNoIndex.payload) {
const payload = res.searchNoIndex.payload;
const dataList = payload.map((item) => ({ ...item, id: item.name })); //Get requests require name instead of id
setSearchList(dataList)
}
}
const component = (
<CardSearch
searchList={searchList}
/>
)
return component;
}
export default BrandingSearch;
Below is my testcase, useEffect is being invoked in the testcase, but handleOnComplete is not being invoked.
How can I fix this.
Testcase:
describe('Test BrandingSearch', () => {
it('', async () => {
let deleteMutationCalled = false;
const mocks = [
{
request: {
query: SEARCH_NO_INDEX,
variables: {
rootType: 'branding',
input: {}
}
},
result: () => {
deleteMutationCalled = true;
return { data:{} };
}
}
];
let wrapper;
act(() => {
wrapper = create(
<MockedProvider mocks={mocks} addTypename={false}>
<BrandingSearch />
</MockedProvider>
)
})
let component = wrapper.root;
expect(deleteMutationCalled).toBe(true);
//Expecting this to be set true, once the mutation is fired.
})
});
Any help is appreciated.
After hours of reading through articles and threads, I finally figured the issue out with some hit & trial.
Cause: As we know, the useQuery, useLazyQuery and useMutation are async calls. So when these are called, they make the API call, wait for the response, and then process the onCompleted or onError callbacks.
Let's consider a useQuery hook. When we call render() in our test cases, the hook is called upon mounting but the test terminates way before the async call gets finished and the onCompleted doesn't even get the chance to execute. The test hence uses the initially added states and doesn't update based on the data we provide in the mocks.
Solution: There needs to be a gap between the call of the hook and the assertion, in order to give room to onCompleted() for getting executed. Add the async keyword before the test case and add a delay after the render(), like this:
await act(() => new Promise((resolve) => setTimeout(resolve, 2000)));
This can also be added when useLazyQuery or useMutation is called (say on the click of a button).
Although this is kind of a miss from the devs end, I feel this should be highlighted somewhere in the documentation for MockedProvider.
I have a react hook style component in typescript. I'm using office uifabric as ui framework. I want to get the following pattern to work in a "best practice" manner:
I have a component with an onClick event (in my case a CommandBar)
User clicks on the action
I make an async call to a backend api (the async part is whats causing me trouble here I think, and this is unfortunately a requirement).
When the async call is complete, or fails. I want to show a MessageBar with information.
All in all using as little code as possible, react hooks seems to have the ability to produce nice and condence code together with TypeScript. But I'm new to both things so I may be out on a limb here...
Creating a dummy fetch method that is NOT async causes my example code to work as expected. But when I have the async call, something gets lost and I do not get a re-render and visibility of the MessageBar. The api call is made though!
const UserProfile = () => {
const [apiStatusCode, setApiResponseCode] = useState(0);
const [showMessageBar, setShowMessageBar] = useState(false);
const startAsync = () => {
// With await here I get a compiler
// error. Without await, the api call is made but
// the messagebar is never rendered. If the call is not async, things
// work fine.
myAsyncMethod();
};
const toggleMessageBar = (): (() => void) => (): void => setShowMessageBar(!showMessageBar);
const myAsyncMethod = async () => {
try {
const responseData = (await get('some/data'))!.status;
setApiResponseCode(responseData);
toggleMessageBar();
} catch (error) {
console.error(error);
setApiResponseCode(-1);
toggleMessageBar();
}
};
const getItems = () => {
return [
{
key: 'button1',
name: 'Do something',
cacheKey: 'button1', //
onClick: () => startAsync()
}
];
};
return (
<Stack>
<Stack.Item>
<CommandBar
items={getItems()}
/>
</Stack.Item>
{showMessageBar &&
<MessageBar messageBarType={MessageBarType.success} onDismiss={toggleMessageBar()} dismissButtonAriaLabel="Close">
Successfully retrieved info with status code: {apiStatusCode}
</MessageBar>
}
// Other ui parts go here
</Stack>
)
};
export default UserProfile;
Assigning an async method to the onClick event gives compiler error:
Type 'Promise<void>' is not assignable to type 'void'. TS2322
Not awaiting the async call causes react not re-rendering when call is complete.
A common pattern for this sort of thing it to update your state in the callback as you are doing, and then respond to the new data with useEffect:
useEffect(()=>{
setShowMessageBar(!showMessageBar)
},[apiStatusCode, showMessageBar])
const myAsyncMethod = async () => {
try {
const responseData = (await get('some/data'))!.status;
setApiResponseCode(responseData);
// toggleMessageBar(); <-- delete me
} catch (error) {
console.error(error);
setApiResponseCode(-1);
// toggleMessageBar(); <-- delete me
}
};
Found the answer eventually, the key was to use reacts useEffect mechanism. This answer led me to the solution:
Executing async code on update of state with react-hooks
Key changes in my code:
Create a state to track execution
const [doAsyncCall, setDoAsyncCall] = useState(false);
Set the state in the onClick event:
const getItems = () => {
return [
{
key: 'button1',
name: 'Do something',
cacheKey: 'button1', //
onClick: () => setDoAsyncCall(true)
}
];
};
Wrap the async functionality in a useEffect section that depends on the state:
useEffect(() => {
async function myAsyncMethod () {
try {
const responseData = (await get('some/data'))!.status;
setApiResponseCode(responseData);
setShowMessageBar(true);
} catch (error) {
console.error(error);
setApiResponseCode(-1);
setShowMessageBar(true);
}
}
if (doAsyncCall) {
myAsyncMethod();
}
}, [doAsyncCall]);
How can I get my test to reflect the new state of my component after calling updateItem on it. I did try rendered.update() but I must be missing on how something works. Thanks.
Method on class:
updateItem (id) {
return updateItem(id)
.then((result) => {
this.setState({
item: {blah:'blah'}
});
}, (error) => {
throw error;
});
}
Test:
it("should update item accordingly", () => {
const rendered = shallow(React.createElement(Item));
const result = rendered.instance().updateItem();
expect(rendered.state('item')).toEqual({blah:'blah'});
})
You have to get the function from the childs prop and call it. As it returns a promise you need to use async/await or return the promise from your test, have a look at docs for more infos. Then test the state of the rendered component.
it("should update item accordingly", async() => {
const rendered = shallow(React.createElement(Item));
await rendered.find('someChild).prop('updateItem')('someId')
expect(item.state).toEqual({
item: {blah:'blah'}
})
})