Why does my child component render multiple times? - reactjs

I am using Stripe API on my project and when the payment is successful I want to add the order placed to my collection. When I run the below code it adds the order multiple times and there is no specific pattern. It also adds multiple orders to the collection:
Success.js:
import React from "react";
import AddOrder from "../orders/AddOrder";
import { useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery } from "#apollo/client";
import queries from "../../queries";
import { Alert, Button } from "react-bootstrap";
function Success() {
const logo = require("../../assets/delivery-package.gif");
const navigate = useNavigate();
const { secret } = useParams();
const { loading, error, data } = useQuery(queries.GET_SESSION, { variables: { id: secret } });
const [deleteSession] = useMutation(queries.DELETE_SESSION);
if (loading) {
return <div>Loading...</div>;
} else if (error) {
navigate("/notfound");
} else if (data.session) {
deleteSession({
variables: {
id: secret,
},
});
return (
<div>
<AddOrder />
<Alert variant="success" style={{ fontSize: "25px" }}>
Order Placed Successfully
</Alert>
<img alt="order success" id="logo" src={logo} style={{ width: "70%", height: "70%", marginTop: "30px" }} />
<br />
<br />
<Button onClick={() => navigate("/")}>Home</Button>
</div>
);
} else {
navigate("/notfound");
}
}
export default Success;
AddOrder:
import { useMutation, useQuery } from "#apollo/client";
import { AuthContext } from "../../Firebase/Auth";
import queries from "../../queries";
import { useContext, useEffect } from "react";
import { reactLocalStorage } from "reactjs-localstorage";
let add = reactLocalStorage.getObject("addressDetails");
function AddOrder() {
const d = new Date();
let text = d.toString();
const [addOrder] = useMutation(queries.ADD_ORDER);
const [editUser] = useMutation(queries.EDIT_USER_CART);
const { currentUser } = useContext(AuthContext);
const { data } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "network-only",
variables: {
id: currentUser.uid,
},
});
const getUserOrders = useQuery(queries.GET_USER_ORDERS, {
fetchPolicy: "network-only",
variables: {
userId: currentUser.uid,
},
});
useEffect(() => {
if (data && getUserOrders.data && currentUser && data.getUser.cart.length > 0) {
let newCart = [];
let total = 0;
for (let i = 0; i < data.getUser.cart.length; i++) {
total += data.getUser.cart[i].price * data.getUser.cart[i].quantity;
newCart.push({
orderedQuantity: data.getUser.cart[i].quantity,
_id: data.getUser.cart[i]._id,
name: data.getUser.cart[i].name,
image: data.getUser.cart[i].image,
price: data.getUser.cart[i].price,
});
}
addOrder({
variables: {
userId: currentUser.uid,
userEmail: currentUser.email,
status: "ordered",
createdAt: text,
products: newCart,
total: total,
flag: getUserOrders.data.userOrders.length + 1,
zip: add.zip.val ? add.zip.val : add.zip,
state: add.state.val ? add.state.val : add.state,
city: add.city.val ? add.city.val : add.city,
apt: add.apt.val ? add.apt.val : add.apt,
addressStreet: add.addressStreet.val ? add.addressStreet.val : add.addressStreet,
},
});
editUser({
variables: {
id: currentUser.uid,
cart: [],
},
});
}
}, [addOrder, currentUser, data, editUser, getUserOrders, text]);
}
export default AddOrder;
There is only one return so don't know if the issue is with the success page or the Addorder function

Possible reason:
Your component must be being rendered by some parent component, for example, by calls to setState, and when the component is re-rendered the Success child component (which in turn does not return a JSX.Element) is also re-rendered
Cause:
Inside your AddOrder component there is a useEffect that serves to prevent re-renders when the passed parameters were not changed, and among them it has a "new Date().getTime()", I believe this is one of the main reasons why useEffect is always returning true, and requesting a re-render. Not only that, but also:
new Date().toString as parameter to useEffect
Passing functions (addOrder, editOrder) to useEffect params that are likely to always be new functions when reredendering.
currentUser is probably a new object every render, luckily you only need the userid, which in turn is a primitive type, and will always be the same regardless of re-rendering.
Solution:
There is no need to analyze if the addOrder or editOrder function has changed, as they are just functions, and do not add any specific "value", for example, if your intention is to rederize (to be able to add) only when the items change, you can just leave the getUserOrders.data and data (useQuery(queries.GET_USER_BY_ID)...)
New code:
import { useMutation, useQuery } from "#apollo/client";
import { AuthContext } from "../../Firebase/Auth";
import queries from "../../queries";
import { useContext, useEffect } from "react";
import { reactLocalStorage } from "reactjs-localstorage";
let add = reactLocalStorage.getObject("addressDetails");
function AddOrder() {
const d = new Date();
let text = d.toString();
const [addOrder] = useMutation(queries.ADD_ORDER);
const [editUser] = useMutation(queries.EDIT_USER_CART);
const { currentUser } = useContext(AuthContext);
const { data } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "network-only",
variables: {
id: currentUser.uid,
},
});
const getUserOrders = useQuery(queries.GET_USER_ORDERS, {
fetchPolicy: "network-only",
variables: {
userId: currentUser.uid,
},
});
useEffect(() => {
if (data && getUserOrders.data && currentUser && data.getUser.cart.length > 0) {
let newCart = [];
let total = 0;
for (let i = 0; i < data.getUser.cart.length; i++) {
total += data.getUser.cart[i].price * data.getUser.cart[i].quantity;
newCart.push({
orderedQuantity: data.getUser.cart[i].quantity,
_id: data.getUser.cart[i]._id,
name: data.getUser.cart[i].name,
image: data.getUser.cart[i].image,
price: data.getUser.cart[i].price,
});
}
addOrder({
variables: {
userId: currentUser.uid,
userEmail: currentUser.email,
status: "ordered",
createdAt: text,
products: newCart,
total: total,
flag: getUserOrders.data.userOrders.length + 1,
zip: add.zip.val ? add.zip.val : add.zip,
state: add.state.val ? add.state.val : add.state,
city: add.city.val ? add.city.val : add.city,
apt: add.apt.val ? add.apt.val : add.apt,
addressStreet: add.addressStreet.val ? add.addressStreet.val : add.addressStreet,
},
});
editUser({
variables: {
id: currentUser.uid,
cart: [],
},
});
}
}, [currentUser.uid, data, getUserOrders.data]);
}
export default AddOrder;
Please check if the code works, anything please comment so I can correct my answer (I haven't tested it yet, but I believe it will work).
Although it is not recommended, as the good practice recommends the use of memos, you can try in the last case (only to verify if this is in fact the problem) to use as follows:
[currentUser.uid, JSON.stringify(data), JSON.stringify(getUserOrders.data)]

In AddOrder component, you are not returning any JSX. You are actually sending order request in the body of the component ( during rendering, which is super bad since it is a side-effect).
Don't return addOrder, editUser calls. Place them inside of useEffect or event handlers depending on your business logic.

Related

React component data from GraphQL api result undefined

I'm working on a React component that uses gql and useQuery from GraphQL to fetch data from an API.
My problem is that the data resulting is undefined because probably the state is not handled correctly and the component didn't load the data requested to the API.
I'm new to React and cannot resolve this situation probably a way to wait until the data is loaded and become defined but I have no idea how to do it.
I was trying even to add a useEffect not present in this version but didn't give any differences :( maybe I just don't know how to use it
The component as follows
import { gql, useQuery } from '#apollo/client';
import { useConfiguration } from '#lib/hooks/useConfiguration';
import { useSetConfiguration } from '#lib/hooks/useSetConfiguration';
import { useUnSetConfiguration } from '#lib/hooks/useUnSetConfiguration';
import { FormControlLabel, FormGroup, Switch, Typography } from '#mui/material';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
const QUERY = gql`
query WEB_useConfiguration($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
configurationOverrideChain {
value
scope
}
}
}
`;
const beforeQuery = ({ studyId }) => ({
variables: { name: 'messaging.email.sender.address', scope: { studyId } },
fetchPolicy: 'network-only',
});
const afterQuery = data => ({
studyConfigurationOverrides:
data?.studyConfiguration?.configurationOverrideChain ?? {},
});
const StudyConfiguration = ({ studyId }) => {
const intl = useIntl();
const [studyConf, setStudyConf] = useState({});
const { data } = useQuery(QUERY, beforeQuery({ studyId }));
console.log('data: ', data); // undefined
const { studyConfigurationOverrides } = afterQuery(data);
// !TODO: make one smart useConfiguration hook instead of 3
// Getting the study config
const smsEnabled = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
const emailSender = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [valueEmailReplay, setValueEmailReplay] = useState(emailSender);
const [valueSmsConf, setValueSmsConf] = useState(smsEnabled);
useEffect(() => {
if (valueSmsConf !== smsEnabled) {
setValueSmsConf(smsEnabled);
}
}, [smsEnabled]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (valueEmailReplay !== emailSender) {
setValueEmailReplay(emailSender);
}
}, [emailSender]); // eslint-disable-line react-hooks/exhaustive-deps
let suffix;
const globalValue = studyConfigurationOverrides.find(
e => e.scope === 'GLOBAL',
);
const defaultValue = studyConfigurationOverrides.find(
e => e.scope === 'DEfAULT',
);
if (globalValue) {
suffix = globalValue.value;
}
if (defaultValue) {
suffix = defaultValue.value;
}
const emailDomain = suffix?.substring(suffix.indexOf('#'));
const noReplyEmail = `noreply${emailDomain}`;
// Set study config
const [setNoReplyStudyEmail] = useSetConfiguration({
name: 'messaging.email.sender.address',
value: noReplyEmail,
scope: { studyId },
});
const [setSmsMessagingDisable] = useSetConfiguration({
name: 'messaging.recruitment.sms.enable',
value: 'false',
scope: { studyId },
});
// unSet study config
const [setDefaultStudyEmail] = useUnSetConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [setSmsMessagingDefault] = useUnSetConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
});
const handleReplayEmailChange = async event => {
setValueEmailReplay(event.target.checked ? suffix : noReplyEmail);
event.target.checked
? await setDefaultStudyEmail()
: await setNoReplyStudyEmail();
};
const handleSmsConf = async event => {
setValueSmsConf(event.target.checked);
event.target.checked
? await setSmsMessagingDefault()
: await setSmsMessagingDisable();
};
const isEmailEnabled = emailSender === suffix;
return (
<FormGroup>
<FormControlLabel
control={
<Switch checked={isEmailEnabled} onChange={handleReplayEmailChange} />
}
label={
<Typography color="textPrimary">
{intl.formatMessage(
{
defaultMessage:
'Allow candidates to reply to emails (send from {replyEmailTxt} instead of {noReplyTxt})',
},
{ replyEmailTxt: suffix, noReplyTxt: 'noReplyEmail' },
)}
</Typography>
}
/>
<FormControlLabel
control={<Switch checked={valueSmsConf} onChange={handleSmsConf} />}
label={
<Typography color="textPrimary">
{intl.formatMessage({
defaultMessage: `SMS messaging`,
})}
</Typography>
}
/>
</FormGroup>
);
};
export default StudyConfiguration;

Testing zustand state changes caused by a component in Jest

I am pretty new to using jest and Im trying to test a component that makes a state change which acts upon my global state (using Zustand). Basically im clicking a button and its adding an item to my state.traits. Here is my component code:
import { Flex, useToast } from '#chakra-ui/react'
import { FC } from 'react'
import { useProfileStore } from 'stores/profileStore'
interface DataTrait {
name: string,
id: string
}
type Props = {
trait: DataTrait
}
export const ChipItem: FC<Props> = ({ trait }) => {
const { traits, setTraits } = useProfileStore()
const toast = useToast()
const traitNames = traits.map((trait) => trait.name)
const emptyTraits = traits.filter((trait) => trait.name === "")
const handleClick = (trait: DataTrait) => {
if (!traitNames.includes(trait.name) && emptyTraits.length !== 0) {
let currentItem = traits.filter(trait => trait.name === "")[0]
let items = [...traits]
let item = {position: currentItem.position, id: trait.id, name: trait.name}
items[items.indexOf(currentItem)] = item
setTraits(items)
} else if (emptyTraits.length === 0){
toast({
title: 'Error',
status: 'error',
description: 'Only 5 traits can be selected',
isClosable: true,
duration: 5000
})
} else {
toast({
title: 'Error',
status: 'error',
description: 'Please select unique traits',
isClosable: true,
duration: 5000
})
}
}
return (
traitNames.includes(trait.name) ? (
<Flex mx={4} p={2} cursor="pointer" borderRadius="20px" backgroundColor="green" borderWidth="1px" borderColor="white" textColor="white" onClick={() => handleClick(trait)}>{trait.name}</Flex>
) : (
<Flex mx={4} p={2} cursor="pointer" borderRadius="20px" borderWidth="1px" borderColor="grey" onClick={() => handleClick(trait)}>{trait.name}</Flex>
)
)
}
here is my store code:
import create from 'zustand'
export interface Trait {
position: string,
name: string,
id: string,
}
export type Traits = Trait[]
const initialTraits = [
{position: "0", name: "", id: ""},
{position: "1", name: "", id: ""},
{position: "2", name: "", id: ""},
{position: "3", name: "", id: ""},
{position: "4", name: "", id: ""},
]
export type ProfileStore = {
traits: Traits;
setTraits: (traits: Traits) => void;
clearTraits: () => void;
}
export const useProfileStore = create<ProfileStore>((set) => ({
traits: initialTraits,
setTraits: (traits) => set({ traits }),
clearTraits: () => set({ traits: initialTraits })
}))
and here is my test code:
import React from 'react';
import { ChipItem } from "../../ChipList/ChipItem";
import { act, render, renderHook } from "#testing-library/react";
import { useProfileStore } from "../../../stores/profileStore";
const stubbedTrait = {
name: "Doing Work",
id: "efepofkwpeok"
}
it("displays the trait chip", () => {
const { queryByText } = render(<ChipItem trait={stubbedTrait} />);
expect(queryByText("Doing Work")).toBeTruthy();
})
it("sets the chip information in the store", () => {
act(() => {
const { traits } = renderHook(() => useProfileStore())
const { getByText } = render(<ChipItem trait={stubbedTrait}/>);
getByText(stubbedTrait.name).click()
expect(traits.includes(stubbedTrait)).toBeTruthy()
})
})
whats happening, is that it keeps telling me that renderHook is not a function and traits always comes back undefined. any help would be greatly appreciated!
Currently you must install and import React Testing Hooks separately
The best way to unit test Zustand state changes inside and specific component is not by using Zustand but by mocking the store hook with Jest.
You should create a test case for the Zustand Store using React Hook Testing library and once you verify the hook behaves as expected, then you mock the store with manual traits and setTraits changes.
Once you have the unit tests then you should test the behaviour of the real hook and components together with integration tests.

DetailsList is getting re-rendered and hence is loosing its state

So I have been trying to accomplish the idea that whenever the user clicks any row in the DetailsList the Button gets enabled and when the user clicks outside the selectionzone the Button gets disabled.
This is my code
import { DetailsList, SelectionMode, Selection, ISelection, initializeIcons, PrimaryButton } from '#fluentui/react'
import {useMemo } from 'react'
import { useBoolean } from '#uifabric/react-hooks'
interface ICurrency {
type: string,
amount: number
}
function App() {
initializeIcons()
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(true)
const items: ICurrency[] = [
{
type: 'INR',
amount: 20
},
{
type: 'USD',
amount: 50
},
{
type: 'GBP',
amount: 70
}
]
const selection: ISelection = useMemo(() => new Selection(
{
onSelectionChanged: ()=>{
if(selection.getSelectedCount() > 0){
enableBtn()
}else{
disableBtn()
}
}
}
), [items])
return (
<div className="App">
<PrimaryButton text="Button" disabled={isBtn}/>
<DetailsList
items={items} selectionMode={SelectionMode.single}
selection={selection}
/>
</div>
);
}
export default App;
I even used useMemo and kept on banging my head but the problem persists where clicking any row the state is lost and the button is not enabled. I have already tried storing the state of selection also, count, everything but it seems I'm missing out on something essential or fundamental for the implementation
You need to memoize items because on each render it's being re-assigned and considered a new array which causes your selection to change because it relies on items as a useMemo dependency. On each state update the selection will reset.
So one way you can fix this is by moving the items out of the function so that it holds reference instead of creating a new items array on each render.
const items = [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
];
function App() {
// code
}
or by using useMemo on those items:
const items = useMemo(() => [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
],[]);
Also I see you have an error, the initializeIcons should only be called once. So that should probably be placed in useEffect:
useEffect(() => {
initializeIcons();
},[])
The final code sample should look like this:
import {
DetailsList,
SelectionMode,
Selection,
ISelection,
initializeIcons,
PrimaryButton
} from "#fluentui/react";
import { useMemo, useEffect } from "react";
import { useBoolean } from "#uifabric/react-hooks";
const items = [
{
type: "INR",
amount: 20
},
{
type: "USD",
amount: 50
},
{
type: "GBP",
amount: 70
}
];
function App() {
useEffect(() => {
initializeIcons();
}, []);
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(
true
);
const selection = useMemo(
() =>
new Selection({
onSelectionChanged: () => {
if (selection.getSelectedCount() > 0) {
enableBtn();
} else {
disableBtn();
}
}
}),
[items]
);
return (
<div className="App">
<PrimaryButton text="Button" disabled={isBtn} />
<DetailsList
items={items}
selectionMode={SelectionMode.single}
selection={selection}
/>
</div>
);
}
export default App;
The Accepted answer has the proper reasoning for the issue. I just wanted to post my solution too. I just had to store the state of the items
import { DetailsList, SelectionMode, Selection, initializeIcons, PrimaryButton } from '#fluentui/react'
import { useEffect, useState } from 'react'
import { useBoolean } from '#uifabric/react-hooks'
interface ICurrency {
type: string,
amount: number
}
function App() {
useEffect(() => {
initializeIcons();
}, []);
const [isBtn, { setTrue: disableBtn, setFalse: enableBtn }] = useBoolean(true)
let _selection = new Selection({
onSelectionChanged: () => {
if (_selection.getSelectedCount() > 0) {
enableBtn()
} else {
disableBtn()
}
}
});
let _initialItems: ICurrency[] = [
{
type: 'INR',
amount: 20
},
{
type: 'USD',
amount: 50
},
{
type: 'GBP',
amount: 70
}
]
const [items, setItems] = useState(_initialItems)
return (
<>
<PrimaryButton text="Button" disabled={isBtn} />
<DetailsList
items={items}
selection={_selection}
selectionMode={SelectionMode.single}
/>
</>
);
}
export default App;
Now say if the items are coming from some props or some state management, then just use setItems inside a useEffect and set the dependency as that source

React, Apollo - Update & Remove Mutation Issues

I am currently working and study on "CRUD" mutation with GraphQL. Using Apollo Client and React to control mutation at front-end. Using Express/mongoose as back-end to run server and fetch/store data from local database.
Working/Test with GraphQL (Middle/Back-end). I am able to fetch and mutate (create, update, and remove) data from local server (mongodb) through localhost:4000/graphiql tool.
Here's mutation schema...
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
addUser: {
...
},
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
name: { type: new GraphQLNonNull(GraphQLString) },
age: { type: new GraphQLNonNull(GraphQLInt) }
},
resolve(parent, args) {
let updateIser = User.findByIdAndUpdate(
args.id,
{
$set: { name: args.name, age: args.age }
},
{ new: true }
).catch(err => new Error(err));
return updateUser;
}
},
deleteUser: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLString) } },
resolve(parent, args) {
let removeUser = User.findByIdAndRemove(args.id).exec();
if (!removeUser) {
throw new Error('Error, Cannot Delete!');
}
return removeUser;
}
}
}
});
Now on the the Front-End part using React and Apollo Client, fetching data and rendering iteration map was a success!
I also use react-router for page transition, When clicking on Add button from user list, it will bring me to form page to fill in user name and age. After complete, it will Submit and redirect (react-router redirect method) back to list page with a newly created user into list (refetch). This process was also a success for me!
On the Update (Edit) or Remove (Delete) part, It was unfortunate. I am facing several issues...Unable to update (Edit) user
I am guessing... It appear having something to do with not be able to fetch specific ID after clicking on Edit Button, or could it be some conflicting issue between fetching and react-router? As much as I have tried to find solutions, I ran out of ideas. So I was hoping I could use some help from you to solve on what I am facing.
If this helps... Here's React-Apollo syntax called List.js - This display the list of users including Edit and Remove button (unsuccessful attempts)
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import { Link } from 'react-router-dom';
import { getUsersQuery } from '../queries/queries';
class UserList extends Component {
state = { selected: null };
displayUser = () => {
let data = this.props.data;
if (data.loading) {
return <div>Loading...</div>;
} else {
return data.examples.map(user => (
<li
key={user.id}
onClick={e => { this.setState({ selected: user.id });}}
>
{user.name} {user.age}
<div className="choices">
<Link
to="/updateuser"
onClick={e => {
this.setState({ selected: user.id });
}}
>
<button>Edit</button>
</Link>
<Link to="/removeuser">
<button>Remove</button>
</Link>
</div>
</li>
));
}
};
render() {
console.log(this.state.selected);
return (
<div id="listContent">
<h3>List</h3>
<ul id="userList"> {this.displayUser()}</ul>
<Link to="/adduser">
<button>+</button>
</Link>
<div>{this.state.selected}</div>
</div>
);
}
}
export default graphql(getUsersQuery)(UserList);
The UpdateForm.js - A syntax that render update form. But couldn't succeed mutate it. possibility fetching issues?
import React, { Component } from 'react';
import { graphql, compose } from 'react-apollo';
import { Redirect, Link } from 'react-router-dom';
import { getUsersQuery, updateUserMutation } from '../queries/queries';
class UpdateUserForm extends Component {
state = {
name: '',
age: '',
redirect: false
};
onChangeName = e => {
this.setState({ name: e.target.value });
};
onChangeAge = e => {
this.setState({ age: e.target.value });
};
onSubmitForm = e => {
e.preventDefault();
//console.log(this.state);
this.props.updateUserMutation({
variables: {
id: this.state.id,
name: this.state.name,
age: this.state.age
},
refetchQueries: [ { query: getUsersQuery } ]
});
this.setState({ redirect: true });
};
render() {
const user = this.props.getUsersQuery;
if (this.state.redirect) {
return <Redirect to="/" />;
}
return (
<div id="formContainer">
<form id="form" onSubmit={this.onSubmitForm}>
<div className="field">
<label>Name:</label>
<input
type="text"
onChange={this.onChangeName}
placeholder={user.name}
/>
</div>
<div className="field">
<label>Age:</label>
<input
type="text"
onChange={this.onChangeAge}
placeholder={user.age}
/>
</div>
<button>Update</button>
<Link to="/">Cancel</Link>
</form>
</div>
);
}
}
export default compose(
graphql(getUsersQuery, { name: 'getUsersQuery' }),
graphql(updateUserMutation, { name: 'updateUserMutation' })
)(UpdateUserForm);
Not sure about remove page -- Hoping just simply hit remove button, from list page will fetch specific ID and mutate (remove) user.
Here's the Apollo Client query code called queries.js.
// Fetch Users
const getUsersQuery = gql`
{
users {
name
age
id
}
}
`;
// Update User Mutation
const updateUserMutation = gql`
mutation($id: ID!, $name: String!, $age: Int!) {
updateUser(id: $id, name: $name, age: $age) {
id
name
age
}
}
`;
// Remove User Mutation
const removeUserMutation = gql`
mutation($id: ID!) {
deleteUser(id: $id) {
id
}
}
`;
Thanks in advance...

How to generate snapshot after all life cycle methods have called in React Jest

snapshot file has created before componentDidMount() is being called. In my situation, I fetch data from server inside the componentDidMount(). Based on the results, I draw the table. But in my test case, it doesn't show those received mock results.
Test file
import React from 'react';
import renderer from 'react-test-renderer';
import { fakeRequestLibrary } from '../../../__mocks__/fakeRequestLibrary';
import ReportAsTableView from '../../../components/reports/common/ReportAsTableView';
const FAKE_RESPONSE = {
dataSets: [
{
metadata: {
columns: [
{
name: "username",
label: "username"
},
{
name: "date_created",
label: "date_created"
}
]
},
rows: [
{
date_created: "2010-04-26T13:25:00.000+0530",
username: "daemon"
},
{
date_created: "2017-06-08T21:37:18.000+0530",
username: "clerk"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "nurse"
},
{
date_created: "2017-07-08T21:37:19.000+0530",
username: "doctor"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "sysadmin"
}
]
}
]
};
describe('<ReportAsTableView /> ', () => {
it('renders correctly with success data received from server', () => {
const params = {
"startDate": "2017-05-05",
"endDate": "2017-10-05"
};
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary('openmrs-fake-server.org', {}, true, FAKE_RESPONSE)} />
);
expect(rendered.toJSON()).toMatchSnapshot();
});
});
Targeted component class
import React, { Component } from 'react';
import { ApiHelper } from '../../../helpers/apiHelper';
import * as ReportConstants from '../../../helpers/ReportConstants';
import ReactDataGrid from 'react-data-grid';
import DataNotFound from './DataNotFound';
import moment from 'moment';
import './ReportAsTableView.css';
class ReportAsTableView extends Component {
constructor(props) {
super();
this.state = {
report: {
definition: {
name: ''
}
},
reportColumnNames: Array(),
reportRowData: Array()
};
this.resolveResponse = this.resolveResponse.bind(this);
this.rowGetter = this.rowGetter.bind(this);
this.init = this.init.bind(this);
}
componentDidMount() {
this.init(this.props.reportParameters);
}
componentWillReceiveProps(nextProps) {
this.init(nextProps.reportParameters);
}
init(params) {
if(this.props.fetchData != null){
//Test Path
this.props.fetchData
.then((response) => {
console.log('>>>>>'+JSON.stringify(response.body));
this.resolveResponse(response.body);
});
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
resolveResponse(data) {
this.setState({ report: data });
this.setState({ reportColumnNames: data.dataSets[0].metadata.columns });
this.setState({ reportRowData: data.dataSets[0].rows });
}
// ... there are some other methods as well
render() {
return (
<div style={{ border: '1px solid black' }}>
{this.getColumns().length > 0 ? (
<ReactDataGrid
columns={this.getColumns()}
rowGetter={this.rowGetter}
rowsCount={this.state.reportRowData.length} />
) : (
<DataNotFound componentName="Report Table"/>
)}
</div>
);
}
}
export default ReportAsTableView;
Snapshot file
// Jest Snapshot v1,
exports[`<ReportAsTableView /> renders correctly with success data received from server 1`] = `
<div
style={
Object {
"border": "1px solid black",
}
}
>
<div
className="NotFoundWrapper"
>
<div
className="attentionSign"
>
<img
src="./warning.png"
width="300"
/>
</div>
<div>
No Data found
<span>
for
Report Table
</span>
</div>
</div>
</div>
`;
Update:
fakeRequestLibrary
import Response from 'http-response-object';
export const fakeRequestLibrary = (requestUrl, requestOptions, shouldPass = true, responseData = null) => {
return new Promise((resolve, reject) => {
if (shouldPass) {
resolve(new Response(200, {}, responseData || { message: `You called ${requestUrl}` }, requestUrl));
} else {
reject(new Response(404, {}, responseData || { message: `The page at ${requestUrl} was not found` }, requestUrl));
}
});
};
Instead of passing an http end point what you can do for fix your problem is changing your init method and passing the data if no data are passed fetch them. Like this
init(params) {
if(this.props.fetchData != null){
this.resolveResponse(this.props.fetchData);
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
Then in your test you will have
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={FAKE_RESPONSE} />
);
expect(rendered.toJSON()).toMatchSnapshot();
This solution works for my own project. It might also work for this question as well, but I haven't tested it. Add an await wait(); statement to wait for the async function in componentDidMount to complete.
const wait = async () => 'foo'; // a dummy function to simulate waiting
describe('<ReportAsTableView /> ', async () => {
it('renders correctly with success data received from server', async () => {
const params = {
startDate: '2017-05-05',
endDate: '2017-10-05',
};
var rendered = renderer.create(
<ReportAsTableView
reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary(
'openmrs-fake-server.org',
{},
true,
FAKE_RESPONSE,
)}
/>,
);
await wait(); // wait for <ReportAsTableView> to finish async data fetch in componentDidMount()
expect(rendered.toJSON()).toMatchSnapshot(); // shall render the component AFTER componentDidMount() is called
});
});

Resources