An update inside a test was not wrapped in act(...) - reactjs

I get not wrapped in act error while testing my component. Has someone any idea how to solve it? I've already tried many things like wrapping findByTestAttr in waitFor but it didn't work. And btw my test doesn't fail, assertion is correct, i only get these warnings. I wonder if it's actually my fault or it's jest fault that this error shows at all?
// CurrencyConverter.tsx
const HAVE = 'have';
const RECEIVE = 'receive';
const CurrencyConverter: React.FC = () => {
const [haveInputValues, setHaveInputValues] = React.useState<CurrencyInputValues>({ currency: Currencies.PLN, value: 100 });
const [receiveInputValues, setReceiveInputValues] = React.useState<CurrencyInputValues>({ currency: Currencies.USD, value: '' });
const [inputsSwapped, setInputsSwapped] = React.useState<boolean>(false);
const [rate, setRate] = React.useState<number>(0.0);
const [status, setStatus] = React.useState<Status>({ wasChangedByUser: true, last: HAVE });
const iconsStyle = useStyles();
const getNewCurrencies = (currency: Currencies, type: string) => {
const isHaveType = type === HAVE;
const notChangedCurrency = isHaveType ? receiveInputValues.currency : haveInputValues.currency;
let newHaveCurrency: Currencies, newReceiveCurrency: Currencies;
if (currency === Currencies.PLN && notChangedCurrency === Currencies.PLN) {
newHaveCurrency = isHaveType ? Currencies.PLN : receiveInputValues.currency;
newReceiveCurrency = isHaveType ? haveInputValues.currency : Currencies.PLN;
} else {
newHaveCurrency = isHaveType ? currency : Currencies.PLN;
newReceiveCurrency = isHaveType ? Currencies.PLN : currency;
}
return { newHaveCurrency, newReceiveCurrency };
};
const changeCurrency = (currency: Currencies, type: string) => {
const { newHaveCurrency, newReceiveCurrency } = getNewCurrencies(currency, type);
setReceiveInputValues((currState) => ({ ...currState, currency: newReceiveCurrency }));
setHaveInputValues((currState) => ({ ...currState, currency: newHaveCurrency }));
setStatus({ last: type, wasChangedByUser: true });
};
const changeValue = (value: number | '', type: string) => {
const setter = type === HAVE ? setHaveInputValues : setReceiveInputValues;
setter((currState) => ({ ...currState, value }));
setStatus({ last: type, wasChangedByUser: true });
};
const swapInputs = () => {
setHaveInputValues(receiveInputValues);
setReceiveInputValues(haveInputValues);
setInputsSwapped((currState) => !currState);
setStatus({ last: status.last === HAVE ? HAVE : RECEIVE, wasChangedByUser: true });
};
React.useEffect(() => {
if (!status.wasChangedByUser) return;
const getPropsToCompare = () => {
const isHaveStatus = status.last === HAVE;
const value = isHaveStatus ? haveInputValues.value : receiveInputValues.value;
const fromCurrency = isHaveStatus ? haveInputValues.currency : receiveInputValues.currency;
const toCurrency = isHaveStatus ? receiveInputValues.currency : haveInputValues.currency;
return { value, fromCurrency, toCurrency };
};
const getNewComparisonData = async () => {
const { value, fromCurrency, toCurrency } = getPropsToCompare();
if (value) return await axios.get<CurrencyComparison>(`${Endpoints.COMAPRE_CURRENCIES}/${value}/${fromCurrency}/${toCurrency}/`);
const onEmptyInputData = {
data: {
result: {
exchangeAmount: 0,
exchangeRate: rate,
},
},
};
return onEmptyInputData;
};
const updateComparison = async () => {
const { data } = await getNewComparisonData();
setRate(+data.result.exchangeRate);
const setter = status.last === HAVE ? setReceiveInputValues : setHaveInputValues;
setter((currState: CurrencyInputValues) => ({ ...currState, value: +data.result.exchangeAmount }));
};
updateComparison();
setStatus((currState) => ({ ...currState, wasChangedByUser: false }));
}, [status.wasChangedByUser]);
return (
<div className={classes.currencyConverter}>
<div className={classes.inputsWithConnector}>
<CurrencyInput label={HAVE} values={{ ...haveInputValues, changeCurrency, changeValue }} />
<div className={classes.inputsConnector}>
<div className={classes.connectorLine}></div>
<SwapHorizIcon className={classnames(iconsStyle.swap, inputsSwapped && iconsStyle.swapRotated)} onClick={swapInputs} />
</div>
<CurrencyInput label={RECEIVE} values={{ ...receiveInputValues, changeCurrency, changeValue }} />
</div>
<p className={classes.rate}>
Current rate:{' '}
<span className={classes.rateValue} data-test='currency-rate'>
{rate}
</span>
</p>
</div>
);
};
export default CurrencyConverter;
// test
const setup = () => {
return mount(<CurrencyConverter />);
};
describe('<CurrencyConverter />', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
moxios.install(axiosInstance);
});
afterEach(() => {
moxios.uninstall(axiosInstance);
});
it('displays value in receive input and correct rate on page init', (done) => {
wrapper = setup();
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request
.respondWith({
status: 200,
response: {
result: {
exchangeRate: '4',
exchangeAmount: '25',
},
},
})
.then(async () => {
wrapper.update();
const receiveValueInput = findByTestAttr(wrapper, 'receive-value-input');
const rate = findByTestAttr(wrapper, 'currency-rate');
expect(rate.text()).toEqual('4');
expect(receiveValueInput.prop('value')).toEqual(25);
done();
});
});
});
});
// error
console.error
Warning: An update to CurrencyConverter inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at CurrencyConverter (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\src\components\home\infoCard\currencyConverter\CurrencyConverter.tsx:47:55)
at WrapperComponent (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\node_modules\#wojtekmaj\enzyme-adapter-utils\src\createMountWrapper.jsx:46:26)
127 | const { data } = await getNewComparisonData();
128 |
> 129 | setRate(+data.result.exchangeRate);
| ^
130 |
131 | const setter = status.last === HAVE ? setReceiveInputValues : setHaveInputValues;
132 | setter((currState: CurrencyInputValues) => ({ ...currState, value: +data.result.exchangeAmount }));
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-dom/cjs/react-dom.development.js:24064:9)
at setRate (node_modules/react-dom/cjs/react-dom.development.js:16135:9)
at _callee2$ (src/components/home/infoCard/currencyConverter/CurrencyConverter.tsx:129:7)
at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
console.error
Warning: An update to CurrencyConverter inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at CurrencyConverter (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\src\components\home\infoCard\currencyConverter\CurrencyConverter.tsx:47:55)
at WrapperComponent (C:\Users\jacek\Desktop\moje-strony\CurrencyCenter\node_modules\#wojtekmaj\enzyme-adapter-utils\src\createMountWrapper.jsx:46:26)
130 |
131 | const setter = status.last === HAVE ? setReceiveInputValues : setHaveInputValues;

Ok, it seems like I solved this problem by wrapping request.respondWith with waitFor. This entire act error is very annoying :/
it('displays value in receive input and correct rate on page init', (done) => {
wrapper = setup();
moxios.wait(async () => {
const request = moxios.requests.mostRecent();
await waitFor(() => {
request
.respondWith({
status: 200,
response: {
result: {
exchangeRate: '4',
exchangeAmount: '25',
},
},
})
.then(() => {
wrapper.update();
const receiveValueInput = findByTestAttr(wrapper, 'receive-value-input');
const rate = findByTestAttr(wrapper, 'currency-rate');
expect(rate.text()).toEqual('4');
expect(receiveValueInput.prop('value')).toEqual(25);
done();
});
});
});
});

Related

TypeScript TypeError: create is not a function

New to React and I have this problem in typeScript.
Original class:
const [isItDownResponse, setIsItDownResponse] = React.useState<IsItDownResponse | null>(null);
fetch(IS_IT_DOWN_API)
.then((response) => response.json())
.then((result: IsItDownResponse) => {
setIsItDownResponse(result);
});
let bannerType = MessageBannerType.Error;
if (isItDownResponse != null) {
switch (isItDownResponse.level) {
case 0:
bannerType = MessageBannerType.Success;
break;
case 1:
bannerType = MessageBannerType.Informational;
break;
case 2:
bannerType = MessageBannerType.Warning;
break;
}
}
return (
<View className={styles.content}>
{isItDownResponse === null || isItDownResponse.message === IS_IT_DOWN_GENERAL_MESSAGE ? (
<></>
) : (
<MessageBanner data-testid="isItDownBannerId" type={bannerType} isDismissible>
{isItDownResponse.message}
</MessageBanner>
)}
</View>
);
};
export default Banner;
The real banner set works fine in testing.
But in unit test, i got ‘TypeError: create is not a function’
Unit test i have:
describe('IsItDownBanner', () => {
let setIsItDownResponse;
let useStateSpy;
beforeEach(() => {
setIsItDownResponse = jest.fn();
useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation(() => [null, setIsItDownResponse]);
});
test('renders nothing when there is no is-it-down response', () => {
const { queryByTestId } = renderWithBasicProviders(<IsItDownBanner />);
const description = queryByTestId('isItDownBannerId');
expect(description).not.toBeInTheDocument();
});
test('renders correct information when there is override in is-it-down response', () => {
useStateSpy.mockImplementation(() => [{message: 'test', level: 1}, setIsItDownResponse]);
//fetch.mockResolvedValue({message: 'test', level: 1});
const { queryByTestId } = renderWithBasicProviders(<IsItDownBanner />);
const description = queryByTestId('isItDownBannerId');
expect(description).not.toBeInTheDocument();
});
});
The interesting thing is the first test (with null in response) can pass. But when i tried to pass in some value into the response, it fail with error
TypeError: create is not a function
> 77 | return render(node, {
| ^
78 | wrapper: BasicProvidersWrapper,
79 | });
80 | };

How to solve a situation when a component calls setState inside useEffect but the dependencies changes on every render?

I have this component:
const updateUrl = (url: string) => history.replaceState(null, '', url);
// TODO: Rename this one to account transactions ATT: #dmuneras
const AccountStatement: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { virtual_account_number: accountNumber, '*': transactionPath } =
useParams();
const [pagination, setPagination] = useState<PaginatorProps>();
const [goingToInvidualTransaction, setGoingToInvidualTransaction] =
useState<boolean>(false);
const SINGLE_TRANSACTION_PATH_PREFIX = 'transactions/';
// TODO: This one feels fragile, just respecting what I found, but, we could
// investigate if we can jsut rely on the normal routing. ATT. #dmuneras
const transactionId = transactionPath?.replace(
SINGLE_TRANSACTION_PATH_PREFIX,
''
);
const isFirst = useIsFirstRender();
useEffect(() => {
setGoingToInvidualTransaction(!!transactionId);
}, [isFirst]);
const {
state,
queryParams,
dispatch,
reset,
setCursorAfter,
setCursorBefore
} = useLocalState({
cursorAfter: transactionId,
includeCursor: !!transactionId
});
const {
filters,
queryParams: globalQueryParams,
setDateRange
} = useGlobalFilters();
useUpdateEffect(() => {
updateUrl(
`${location.pathname}?${prepareSearchParams(location.search, {
...queryParams,
...globalQueryParams
}).toString()}`
);
}, [transactionId, queryParams]);
useUpdateEffect(() => dispatch(reset()), [globalQueryParams]);
const account_number = accountNumber;
const requestParams = accountsStateToParams({
account_number,
...state,
...filters
});
const { data, isFetching, error, isSuccess } =
useFetchAccountStatementQuery(requestParams);
const virtualAccountTransactions = data && data.data ? data.data : [];
const nextPage = () => {
dispatch(setCursorAfter(data.meta.cursor_next));
};
const prevPage = () => {
dispatch(setCursorBefore(data.meta.cursor_prev));
};
const onRowClick = (_event: React.MouseEvent<HTMLElement>, rowData: any) => {
if (rowData.reference) {
if (rowData.id == transactionId) {
navigate('.');
} else {
const queryParams = prepareSearchParams('', {
reference: rowData.reference,
type: rowData.entry_type,
...globalQueryParams
});
navigate(
`${SINGLE_TRANSACTION_PATH_PREFIX}${rowData.id}?${queryParams}`
);
}
}
};
const checkIfDisabled = (rowData: TransactionData): boolean => {
return !rowData.reference;
};
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
const showTransactionsTable: boolean =
Array.isArray(virtualAccountTransactions) && isSuccess && data?.data;
const onTransactionSourceLoaded = (
transactionSourceData: PayoutDetailData
) => {
const isIncludedInPage: boolean = virtualAccountTransactions.some(
(transaction: TransactionData) => {
if (transactionId) {
return transaction.id === parseInt(transactionId, 10);
}
return false;
}
);
if (!goingToInvidualTransaction || isIncludedInPage) {
return;
}
const fromDate = dayjs(transactionSourceData.timestamp);
const toDate = fromDate.clone().add(30, 'day');
setDateRange({
type: 'custom',
to: toDate.format(dateFormat),
from: fromDate.format(dateFormat)
});
setGoingToInvidualTransaction(false);
};
const fromDate = requestParams.created_after || dayjs().format('YYYY-MM-DD');
const toDate = requestParams.created_before || dayjs().format('YYYY-MM-DD');
const routes = [
{
index: true,
element: (
<BalanceWidget
virtualAccountNumber={account_number}
fromDate={fromDate}
toDate={toDate}
/>
)
},
{
path: `${SINGLE_TRANSACTION_PATH_PREFIX}:transaction_id`,
element: (
<TransactionDetails
onTransactionSourceLoaded={onTransactionSourceLoaded}
/>
)
}
];
return (........
I get this error: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
The useEffect where the issue is, it is this one:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
Considering previous answers, would the solution be to make sure I return a new object each time? But I am not sure what would be the best approach. Any clues ?
did you want the useEffect to start every changes of 'data?.meta' ?
Without reading all the code, I believe the data.meta object changes on every render. There is a way to change the useEffect to narrow done its execution conditions:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [!data?.meta, data?.meta?.has_previous_page, data?.meta?.has_next_page]);
Please note the ! before data.?.meta which makes the hook test only for presence or absence of the object, since your code doesn't need more than that information.

How to set the first option of the select as default value

I would like to set the selectedSubscriber value with first item from subscriberOptions array (subscriberOptions[0].value). What is the best way to do it?
const defaultFormInput = {
subscriberOptions: [],
selectedSubscriber: "",
subscriberOptionsIsLoading: true,
};
const [formInput, setFormInput] = useState(defaultFormInput);
useEffect(() => {
//load subscribers from API
async function loadSubscribers() {
const response = await fetch("https://myapi/subscribers");
const body = await response.json();
setFormInput({
...formInput,
subscriberOptions: body.map((x) => ({ value: x.id, name: x.name })),
subscriberOptionsIsLoading: false,
});
// not working
//setFormInput({
// ...formInput,
// selectedSubscriber: formInput.subscriberOptions[0].value,
//});
}
loadSubscribers();
}, []);
JSX
<Select
disabled={formInput.subscriberOptionsIsLoading}
value={formInput.selectedSubscriber}
name="selectedSubscriber"
onChange={handleChange}
>
{formInput.subscriberOptions &&
formInput.subscriberOptions.map((item) => {
return (
<MenuItem key={item.value} value={item.name}>
{item.name}
</MenuItem>
);
})}
</Select>
you need to map your data and set at the same state, it should work.
const defaultFormInput = {
subscriberOptions: [],
selectedSubscriber: "",
subscriberOptionsIsLoading: true,
};
const [formInput, setFormInput] = useState(defaultFormInput);
useEffect(() => {
//load subscribers from API
async function loadSubscribers() {
const response = await fetch("https://myapi/subscribers");
const body = await response.json();
const mappedData = (body || []).map((x) => ({ value: x.id, name: x.name }));
const [defaultSelect] = mappedData || [];
setFormInput({
...formInput,
subscriberOptions: mappedData,
selectedSubscriber: defaultSelect.value,
subscriberOptionsIsLoading: false,
});
}
loadSubscribers();
}, []);

Why does my React Context runs twice, and returns `undefined` first before the expect output?

I'm trying to use React context to decide whether to have a button or not, and the problem is my context always runs twice and returns the first value as undefined -- the second time the output is correct.
Context:
const defaultMyContextValue = null;
export const myContext = createContext<GenericResponseFromEndpoint | null>(
defaultMyContextValue
);
export const fetcher = async (
endpoint: 'an_endpoint',
id: string
) => {
const client = new Client().ns('abcdef');
try {
return await client.rpc(
endpoint,
{
id
},
{}
);
} catch (err) {
// log error
return undefined;
}
};
export const MyContextProvider: React.FC = ({children}) => {
const context = useAnotherContext();
const {id} = useMatch();
const fetchMyContext = useCallback(
(endpoint: 'an_endppoint', id: string) =>
fetcher(endpoint, id),
[id]
);
const {data = null} = useSWR(
context?.name && ['an_endpoint', id],
fetchMyContext
);
return <myContext.Provider value={data}>{children}</myContext.Provider>;
};
export const useMyContext = () => {
const context = useContext(myContext);
if (context === undefined) {
throw new Error('------------------------');
}
return context;
};
Conditional render:
export const contextElement: React.FC = () => {
const info = useMYContext()?.info;
if (!info) {
return null;
// console.log('null');
}
console.log('not null');
// when i run it, it always returns two output scenarios:
// undefined - undefined
// undefine - expected value
// the front-end works as expected
return (
<div className="some-class-name">
<Button
variant="primary"
data-testid="button-123"
onClick={() => window.open('_blank')}
>'Do Something',
})}
</Button>
</div>
);
};
Test: my tests failed and the error is the system couldn't find the element. If I set the style={display:none} instead of returning null in the above conditional rendering -- it passes
describe('contextElement', () => {
const renderSomething = () => render(<contextElement />);
afterEach(() => {
myMock.mockClear();
});
it('renders button', () => {
myMock.mockResolvedValue({
info: 'some info'
});
const {getByTestId} = renderSomething();
expect(getByTestId('button-123')).toBeTruthy();
});
it('does not render button', () => {
myMock.mockResolvedValue(undefined);
const {getByTestId} = renderSomething();
expect(getByTestId('button-123')).not.toBeTruthy();
});
});
It's a really long post I know. Thanks for making it till the end. And yes, I check and there's no strictMode in my set-up. Thank you.

useRef Hook issue - ref.current.link.click() firing continuously

const csvLinkRef = useRef(null)
const exportFile = () => {
let data= {data:'info'}
//Async Action
dispatch(
getFileData(data,
()=>{
csvLinkRef.current.link.click()
}
))
}
<Button onClick={exportFile}> //run exportFile on click
<CSVLink
filename={'file.csv'}
data={someData}
ref={csvLinkRef}//set ref here
>
Export
</CSVLink>
</Button>
//Async API call using redux thunk
export const getFileData = (data,cb) => async dispatch => {
try{
const res = await callApi('ever.php',data,'POST')// make api call
if(res.status==="00"){
dispatch({
type:GET_ALL_DATA,
data: res.data,
})
}
}catch(error){
console.log(error)
}finally{
cb()
}
}
My issue is csvLinkRef.current.link.click() fires non-stop after the API call. How can I make it fire just once? Is there a way I can 'unset' the ref? Please help.(I'm using react-csv library).
The getFileData action API call is successful and the data is in redux state.
if csvLinkRef.current.link.click fired only once, everything would be fine
I had the same problem. Now it is solved. You can try it...
function CsvExample({ manufacturerList }: IdLinkCreateProps) {
const httpRequests = new HttpRequests()
const csvInstance = useRef<any | null>(null);
const [alertOpt, setAlertOpt] = useState({ isOpen: false, message: '', variant: '' })
const [manufacturer, setManufacturer] = useState<IManufacturerItem | null>(null)
const [quantity, setQuantity] = useState<number>(0)
const [idList, setIdList] = useState<{ idList: string }[]>([])
const header = [{ label: "Digital-Links", key: "idLink" }];
const createIdLink = async () => {
let data: IIdLinkCreate = { manufacturer: manufacturer!, quantity: quantity! }
try {
const result = await httpRequests.idLinkCreate(data)
setSuccessOptions(result)
} catch (error) {
console.log('error on getting trusted manufacturer list: ', error);
}
return
}
const setSuccessOptions = (result: IBackendResult) => {
if (result.success) {
setIdList(result.data);
} else {
setAlertOpt({ isOpen: true, message: 'FAILED', variant: 'danger' })
console.log('error on updating data: ', result.error);
}
}
useEffect(() => {
if (idList && idList.length > 0 && csvInstance?.current?.link) {
csvInstance.current.link.click();
}
}, [idList]);
return (
<div className='id_link_wrapper'>
<AppHeader />
<Alert className="alert_error" variant={alertOpt.variant} show={alertOpt.isOpen} >
{alertOpt.message}
</Alert>
<IdLinkTitlePart />
<IdLinkInputPart
manufacturerList={manufacturerList}
setManufacturer={setManufacturer}
setQuantity={setQuantity}
quantity={quantity}
/>
<Button
onClick={createIdLink}
className='button_element'> CSV EXPORTIEREN
</Button>
<CSVLink
asyncOnClick={true}
headers={header}
data={idList!}
filename="bsedata.csv"
data-interception='off'
ref={csvInstance}
/>
</div>
)
}
export default CsvExample

Resources