Async thunks cause warnings in tests (wrapped in act) - redux

When trying to test the component which dispatches async thunk I get the following warnings. They are displayed because of updates performed after the test is finished.
console.error
Warning: An update to App 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 App (/home/karlosos/Dev/nokia/playground/testing-redux/src/App.tsx:6:34)
at Provider (/home/karlosos/Dev/nokia/playground/testing-redux/node_modules/react-redux/lib/components/Provider.js:19:3)
at Wrapper (/home/karlosos/Dev/nokia/playground/testing-redux/src/testUtils.tsx:11:22)
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
at warnIfUpdatesNotWrappedWithActDEV (node_modules/react-dom/cjs/react-dom.development.js:27589:9)
at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:25508:5)
at forceStoreRerender (node_modules/react-dom/cjs/react-dom.development.js:16977:5)
at Object.handleStoreChange [as callback] (node_modules/react-dom/cjs/react-dom.development.js:16953:7)
at node_modules/react-redux/lib/utils/Subscription.js:23:20
This is explanation why warnings are visible.
One way of fixing them is to introduce some kind of barrier that will wait for all pending async actions to be finished. But doing that my tests would need to have asserts for logic that I don't want to test.
I have recreated a minimal reproducible project here: https://github.com/karlosos/react-redux-async-warnings/tree/main/src
My example test looks like this:
test('WHEN component rendered THEN counter value is being loaded', () => {
// WHEN
renderWithProviders(<App />)
// THEN
expect(Api.getValue).toHaveBeenCalledTimes(1);
const loadingSpinner = screen.getByTestId('loading-spinner');
expect(loadingSpinner).toBeInTheDocument();
// things will happen to the component here after test is done
// precisely the data fetched from API will be displayed
});
and example thunk:
export const fetchCounterValue = (): AppThunk => async (dispatch, getState) => {
if (getState().counter.fetchValueStatus === "loading") {
return;
}
dispatch(fetchValueStart());
try {
const result = await Api.getValue();
dispatch(fetchValueSuccess(result));
} catch (e) {
dispatch(fetchValueError('Could not fetch the data'));
}
};
Update 1 - No warnings when using waitFor at the end of the test
When I've added await waitFor(() => new Promise(res => setTimeout(res, 0))); at the end of the test then warnings are not visible. But I don't want to edit every single test case. It seems like a hack.
test('WHEN component rendered THEN counter value is being loaded', async () => {
// WHEN
renderWithProviders(<App />)
// THEN
expect(Api.getValue).toHaveBeenCalledTimes(1);
const loadingSpinner = screen.getByTestId('loading-spinner');
expect(loadingSpinner).toBeInTheDocument();
await waitFor(() => new Promise(res => setTimeout(res, 0)));
});

When you first update your tests to use createRoot, you may see this
warning in your test console:
The current testing environment is not configured to support act(…)
https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#configuring-your-testing-environment

Related

How to re-dispatch action in Saga like redux toolkit Automated Re-fetching

I am trying to implement redux-saga action dispatch functioning like Automated Re-fetching in the redux toolkit.
When opening the app, I dispatch some actions to fetch data from the server and update the redux store. When the user stays background and re-enters the app, depending on how long users stay long inactive, then the app would re-dispatch the action to fetch data from the server again. I can save the timestamp whenever fetching from the server and compare the time of the moment of switching from inactive to active. But if there is a well-already-made feature provided, I would definitely want to use that!
There are some libraries like saga-query that do similar thing for you as rtk-query, but as far as I can tell this lib specifically doesn't support refetch on focus out of the box.
Without any lib, it could be implemented like this:
import {delay, put, call, takeEvery, takeLeading, fork} from 'redux-saga/effects';
// Utility function to create a channel that will receive a message
// every time visibility changes
const createVisibilityChannel = () => {
return eventChannel((emit) => {
const handler = () => void emit(!document.hidden);
document.addEventListener('visibilitychange', handler);
return () => document.removeEventListener('visibilitychange', handler);
});
};
// Works as takeLeading except it ignores actions for extra additional time
const takeLeadingWithDelay = (ms, pattern, saga, ...args) => {
return takeLeading(pattern, function* (action) {
yield call(saga, ...args, action);
yield delay(ms);
});
};
// Root saga
function* appSaga() {
// Creates visbility channel
const visibilityChannel = yield call(createVisibilityChannel);
// Limits time between refetches for 1 minute
yield takeLeadingWithDelay(60000, 'FETCH_DATA', fetchData);
// Dispatches fetch action every time page becomes visible
yield takeEvery(visibilityChannel, function* (visible) {
if (visible) yield put({type: 'FETCH_DATA'});
});
// Fetches data on app start and starts the first timer
yield put({type: 'FETCH_DATA'})
}
// Example fetching function
function* fetchData() {
const response = yield fetch(
'https://api.spacexdata.com/v3/launches?limit=5',
);
const data = yield response.json();
console.log({data});
}
This solution assumes that the delay timer isn't specific to the page blur/focus but any refetches including the one on page focus, since in the opposite case I am not sure what the logic for the timer should be when the user switches to page too early.

Redux - Update store with same function from different files

being rather new to react.js + redux, I'm facing the following conundrum:
I have multiple files, which need to update the store in exactly the same way, based on the stores current state. Currently I simply copy-paste the same code (along with the needed mapStateToProps), which goes again DRY.
Similar to something like the below, where getData is an Ajax call living in the actions file and props.timeAttribute is coming from mapStateToProps:
props.getData(props.timeAttribute).then((newState) => {
console.log(newState)
})
Would a function like that go in the actions file? Can the current state be read from within that actions file? Or does one normally create some sort of helperFile.js in which a function like that lives and is being called from other files?
Thanks!
If your file is executing the same action, then yes, you would put the action creator in a separate file and export it. In theory, you can put state in an action by passing the state as a parameter, but the philosophy behind an action is that it announces to your application that SOMETHING HAPPENED (as denoted by the type property on the return value of the action function). The reducer function responsible for handling that type subsequently updates the state.
You can access the current state of the store inside of an action creator like this:
export const testAction = (someParam) => {
return (dispatch, getState) => {
const {
someState,
} = getState(); //getState gets the entire state of your application
//do something with someState and then run the dispatch function like this:
dispatch(() => {type: ACTION_TYPE, payload: updatedState})
}
I like this approach because it encapsulates all the logic for accessing state inside of the one function that will need to access it.
DO NOT modify the state inside of the action creator though! This should be read only. The state of your application should only be updated through your reducer functions.
Yes, it is recommended to maintain a separate file for your actions.
Below is an example of how i use an action to fetch information and dispatch an action.
export const fetchComments = () => (dispatch) => {
console.log("Fetch Comment invoked");
/*you can use your Ajax getData call instead of fetch.
Can also add parameters if you need */
return fetch(baseUrl + 'comments')
.then(response => {
if (response.ok){
return response;
}
else {
var error = new Error('Error ' + response.status + ': ' + response.statusText);
error.response = response;
throw error;
}
},
error => {
var errmess = new Error(error.message);
throw errmess;
})
.then(response => response.json())
.then(comments => dispatch(addComments(comments)))
.catch(error => dispatch(commentsFailed(error.message)));
}
/* Maintain a separate file called ActionTypes.js where you can store all the ActionTypes as Strings. */
export const addComments = (comments) => ({
type : ActionTypes.ADD_COMMENTS,
payload : comments
});
export const comments = (errMess) => ({
type : ActionTypes.COMMENTS_FAILED,
payload : errMess
});
Once, you receive dispatch an action, you need an reducer to capture the action and make changes to your store.
Note that this reducer must be a pure function.
export const comments = (state = { errMess: null, comments:[]}, action) => {
console.log("inside comments");
switch (action.type) {
case ActionTypes.ADD_COMMENTS:
return {...state, errMess: null, comments: action.payload};
case ActionTypes.COMMENTS_FAILED:
return {...state, errMess: action.payload};
default:
return state;
}
};
Don't forget to combine the reducers in the configureStore().
const store = createStore(
combineReducers({
comments
}),
applyMiddleware(thunk,logger)
);
In your components where you use the Actions, use
const mapDispatchToProps = dispatch => ({
fetchComments : () => dispatch(fetchComments()),
})
Note to export the component as
export default connect(mapStateToProps,mapDispatchToProps)(Component);

Perform Ajax Fetch in a Redux Reducer?

I'm trying to wrap my head around accessing the state inside Redux actionCreators; instead did the following (performed ajax operation in the reducer). Why do I need to access the state for this — because I want to perform ajax with a CSRF token stored in the state.
Could someone please tell me if the following is considered bad practice/anti-pattern?
export const reducer = (state = {} , action = {}) => {
case DELETE_COMMENT: {
// back-end ops
const formData = new FormData();
formData.append('csrf' , state.csrfToken);
fetch('/delete-comment/' + action.commentId , {
credentials:'include' ,
headers:new Headers({
'X-Requested-With':'XMLHttpRequest'
}) ,
method:'POST' ,
body:formData
})
// return new state
return {
...state ,
comments:state.comments.filter(comment => comment.id !== action.commentId)
};
}
default: {
return state;
}
}
From the redux documentation:
The only way to change the state is to emit an action, an object describing what happened. Do not put API calls into reducers. Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state.
Actions should describe the change. Therefore, the action should contain the data for the new version of the state, or at least specify the transformation that needs to be made. As such, API calls should go into async actions that dispatch action(s) to update the state. Reducers must always be pure, and have no side effects.
Check out async actions for more information.
An example of an async action from the redux examples:
function fetchPosts(subreddit) {
return (dispatch, getState) => {
// contains the current state object
const state = getState();
// get token
const token = state.some.token;
dispatch(requestPosts(subreddit));
// Perform the API request
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
// Then dispatch the resulting json/data to the reducer
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
As per guidelines of redux.
It's very important that the reducer stays pure. Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
If you are asking whether it is anti-pattern or not then yes it is absolutely.
But if you ask what is the solution.
Here you need to dispatch async-action from your action-creators
Use "redux-thunk" or "redux-saga" for that
You can access the state and create some async action
e.g inside your action-creator ( Just for example )
export function deleteCommment(commentId) {
return dispatch => {
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}
export function updateCommentList(commentList) {
return {
type : UPDATE_COMMENT_LIST,
commentList
};
}
Edit: You can access the state -
export function deleteCommment(commentId) {
return (dispatch, getState) => {
const state = getState();
// use some data from state
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}

Redux - Jest: Testing functions that have void return

New to Jest and Redux and I'm having trouble with testing functions that are dispatching to the store but don't yield a return value. I'm trying to follow the example from the Redux website does this
return store.dispatch(actions.fetchTodos()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
however I have several "fetchtodos" functions that don't return anything which causes the error TypeError:
Cannot read property 'then' of undefined due to returning undefined
I'm wondering what I can do to test that my mock store is correctly updating. Is there a way to dispatch the function, wait for it to finish and then compare the mock store with expected results?
Thanks
Edit: We're using typescript
action from tsx
export function selectTopic(topic: Topic | undefined): (dispatch: Redux.Dispatch<TopicState>) => void {
return (dispatch: Redux.Dispatch<TopicState>): void => {
dispatch({
type: SELECT_Topic,
payload: topic,
});
dispatch(reset(topic));
};
}
test.tsx
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('Select Topic action', () => {
it('should create an action to select .', () => {
const topic: Topic = mockdata.example[0];
const expectedAction = {
type: actions.SELECT_TOPIC,
payload: topic,
};
const store = mockStore(mockdata.defaultState);
return store.dispatch(actions.selectTopic(topic)).then(() => {
expect(store.getState()).toEqual(expectedAction);
});
});
});
The action is what I'm given to test(and there are many other functions similar to it. I'm getting that undefined error when running the test code, as the function isn't returning anything.
In Redux, the store's dispatch method is synchronous unless you attach middleware that changes that behavior, ie: returns a promise.
So this is likely a redux configuration problem. Be sure you are setting up your test store with the same middleware that allows you to use the promise pattern in production.
And as always, be sure to mock any network requests to avoid making api calls in test.

Jest or Mocha: Dynamically create tests based on async initialization

I'm trying to generate tests dynamically by looping over an array returned from an async call. I just cannot figure out how to do this - either using mocha or using jest. To illustrate using code, the following synchronous example works:
describe("Test using various frameworks", () => {
["mocha", "jest"].forEach(framework => {
it(`Should test using ${framework}`, () => {
expect(true).toBe(true);
});
});
});
However, if that array is fetched asynchronously, I cannot get the testing frameworks to wait until the array is fetched before trying to loop over it.
async function getFrameworks() {
//TODO: get it from some async source here
return ["mocha", "jest"];
}
describe("Test using various frameworks", () => {
var frameworks;
//before() instead of beforeAll() if using mocha
beforeAll(async ()=> {
frameworks = await getFrameworks();
});
frameworks.forEach(framework => {
it(`Should test using ${framework}`, () => {
expect(true).toBe(true);
});
});
});
This fails saying Cannot read property 'forEach' of undefined. I've tried all sort of combinations of using async/await and Promise and passing in a done callback but to no avail.
The closest I came to this was using Mocha's --delay flag, but that only solves part of the problem. What I really want to do in my actual use case is to run some async intialization in the before() or beforeAll() hooks which I then use to dynamically generate tests.
Any pointers on how to do this using either mocha or jest?
To answer my own question, I did not find a way to do this using Jest or Mocha, but I could accomplish it using tap - which I used through babel-tap.
import tap from "babel-tap";
async function getFrameworks() {
//TODO: get it from some async source here
return ["mocha", "jest"];
}
getFrameworks().then(frameworks => {
frameworks.forEach(framework => {
tap.test(`Should test using ${framework}`, (tester) => {
tester.ok("It works!");
});
});
});
You can do a lot more though. You can create nested scopes by further calling tester.test() for example. Also, since tap does not have the concept of before, after etc, (unless you use the Mocha-like DSL ), you can simply use imperative code to simulate the equivalent behavior.
Also, you can freely use async/await style calls inside tests.
Mocha has some support async tests. Specifically, your it() callback can be async. Here is how to rearrange your tests to "looping over an array returned from an async call":
const chai = require('chai');
const { expect } = require('chai');
function getMyTestData() {
return new Promise(resolve => setTimeout(() => resolve(['mocha', 'jest']), 1000));
}
describe('Test using various frameworks', () => {
it('Should test by looping over an array returned from an async call', async () => {
const myTestData = await getMyTestData();
for(let datum of myTestData) {
expect(datum.length).greaterThan(4, `Data '${datum}' not long enough`);
}
})
});

Resources