I implemented my own way to handle access/refresh token. Basically when accessToken is expired, it awaits the dispatch of another action and, if it is successful, it dispatch again itself. The code below explains it better:
export const refresh = () => async (dispatch) => {
dispatch({
type: REFRESH_USER_FETCHING,
});
try {
const user = await api.refresh();
dispatch({
type: REFRESH_USER_SUCCESS,
payload: user,
});
return history.push("/");
} catch (err) {
const { code } = err;
if (code !== "ACCESS_TOKEN_EXPIRED") {
dispatch({
type: REFRESH_USER_ERROR,
payload: err,
});
const pathsToRedirect = ["/signup"];
const {
location: { pathname },
} = history;
const path = pathsToRedirect.includes(pathname) ? pathname : "/login";
return history.push(path);
}
try {
await dispatch(refreshToken());
return dispatch(refresh());
} catch (subErr) {
dispatch({
type: REFRESH_USER_ERROR,
payload: err,
});
return history.push("/login");
}
}
};
export const refreshToken = () => async (dispatch) => {
dispatch({
type: REFRESH_TOKEN_FETCHING,
});
try {
await api.refreshToken();
dispatch({
type: REFRESH_TOKEN_SUCCESS,
});
} catch (err) {
dispatch({
type: REFRESH_TOKEN_ERROR,
payload: err,
});
}
};
the issue is that I am finding it really difficult to test with Jest. In fact, I have implemented this test:
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import * as actionCreators from "./actionCreators";
import * as actions from "./actions";
import api from "../../api";
jest.mock("../../api");
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe("authentication actionCreators", () => {
it("runs refresh, both token expired, should match the whole flow", async () => {
api.refresh.mockRejectedValue({
code: "ACCESS_TOKEN_EXPIRED",
message: "jwt expired",
});
api.refreshToken.mockRejectedValue({
code: "REFRESH_TOKEN_EXPIRED",
message: "jwt expired",
});
const expectedActions = [
{ type: actions.REFRESH_USER_FETCHING },
{ type: actions.REFRESH_TOKEN_FETCHING },
{ type: actions.REFRESH_TOKEN_ERROR },
{ type: actions.REFRESH_USER_ERROR },
];
const store = mockStore({ auth: {} });
await store.dispatch(actionCreators.refresh());
expect(store.getActions()).toEqual(expectedActions);
});
});
but instead of completing, the test runs indefenitely. This issue is not happening when I am testing it manually, so I think there is something missing in Jest, so my question is: is there a way to test this recursive behaviour?
Thanks
The problem is await you use with dispatch, dispatch returns an action, not a Promise, use Promise.resolve instead.
Related
I'm trying to get the id of the generated firebase document, and I'm using addDoc to create a new doc.
I'm generating a new document on button click and that button calls the initializeCodeEditor function.
Anyone please help me with this!
Button Code:
import { useNavigate } from "react-router-dom"
import { useAuthContext } from "../../hooks/useAuthContext"
import { useFirestore } from "../../hooks/useFirestore"
import Button from "./Button"
const StartCodingButton = ({ document, setIsOpen }) => {
const { user } = useAuthContext()
const { addDocument, response } = useFirestore("solutions")
const navigate = useNavigate()
const initializeCodeEditor = async () => {
await addDocument({
...document,
author: user.name,
userID: user.uid,
})
if (!response.error) {
console.log(response.document) // null
const id = response?.document?.id; // undefined
navigate(`/solution/${id}`, { state: true })
}
}
return (
<Button
className="font-medium"
variant="primary"
size="medium"
onClick={initializeCodeEditor}
loading={response.isPending}
>
Start coding online
</Button>
)
}
export default StartCodingButton
addDocument code
import { useReducer } from "react"
import {
addDoc,
collection,
doc,
Timestamp,
} from "firebase/firestore"
import { db } from "../firebase/config"
import { firestoreReducer } from "../reducers/firestoreReducer"
const initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
export const useFirestore = (c) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
// add a document
const addDocument = async (doc) => {
dispatch({ type: "IS_PENDING" })
try {
const createdAt = Timestamp.now()
const addedDocument = await addDoc(collection(db, c), {
...doc,
createdAt,
})
dispatch({ type: "ADDED_DOCUMENT", payload: addedDocument })
} catch (error) {
dispatch({ type: "ERROR", payload: error.message })
}
}
return {
addDocument,
response,
}
}
firestoreReducer
export const firestoreReducer = (state, action) => {
switch (action.type) {
case "IS_PENDING":
return { isPending: true, document: null, success: false, error: null }
case "ADDED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
}
throw Error("Unknown action: " + action.type)
}
I have recreated this issue and found out this is happening because the response object in the useFirestore hook is not being updated until the next render cycle.
In order to get the updated response object, you can use the useEffect hook to trigger an update to the component whenever the response object changes.
So I recommend you to call initializeCodeEditor and make your app wait until response object change I used useEffect here
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
})
//skip following if block it's just for understanding
if (!response.error) {
console.log(response.document) // will obviously be null here as at first it is set null
const id = response?.document?.id; // will obviously be undefined
navigate(`/solution/${id}`, { state: true })
}
}
useEffect(() => {
if (!response.error) {
setId(response?.document?.id);
console.log("From App.js useEffect: " + response?.document?.id); // getting the document id here too
}
}, [response])
//and in firestoreReducer
case "ADDED_DOCUMENT":{
console.log("from Reducer: " + action.payload.id); //getting the document id here
return { isPending: false, document: action.payload, success: true, error: null }
}
OR you can use callback also without introducing useEffect like this:
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
}, (response) => {
console.log("From App: " + response?.document?.id); //Will run as callback
if (!response.error) {
setId(response?.document?.id);
}
})
}
This way, the callback function will be called after the addDocument function has completed and the response object will have the updated document id.
When useEffect is executed, I want to get the token through AsyncStorage, then get the data value through the axios.post ('/auth/me') router and execute the KAKAOLOG_IN_REQUEST action with disaptch.
As a result of checking the data value with console.log, the data value came in well. But when I run my code, this error occurs.
Possible Unhandled Promise Rejection (id: 1):
Error: Actions may not have an undefined "type" property. Have you misspelled a constant?
Error: Actions may not have an undefined "type" property. Have you misspelled a constant?
how can i fix my code?....
this is my code
(index.js)
const App = ({}) => {
const dispatch = useDispatch();
useEffect(() => {
async function fetchAndSetUser() {
const token = await AsyncStorage.getItem('tokenstore', (err, result) => {
});
var {data} = await axios.post(
'/auth/me',
{},
{
headers: {Authorization: `Bearer ${token}`},
},
);
console.log("data:",data);
dispatch({
type: KAKAOLOG_IN_REQUEST,
data: data,
});
}
fetchAndSetUser();
}, []);
return <Navigator />;
};
export {App};
(reducer/user.js)
import {
KAKAOLOG_IN_FAILURE,
KAKAOLOG_IN_REQUEST,
KAKAOLOG_IN_SUCCESS,
} from '../reducers/user';
function* watchkakaoLogIn() {
yield takeLatest(KAKAOLOG_IN_REQUEST, kakaologIn);
}
function* kakaologIn(action) {
try {
// const result = yield call(kakaologInAPI, action.data);
yield put({
type: KAKAOLOG_IN_SUCCESS,
data: action.data,
});
} catch (err) {
console.error(err);
yield put({
type: KAKAOLOG_IN_FAILURE,
error: err.response.data,
});
}
}
export default function* userSaga() {
yield all([
fork(watchkakaoLogIn),
]);
}
(reducer/index.js)
import { combineReducers } from 'redux';
import user from './user';
import post from './post';
// (이전상태, 액션) => 다음상태
const rootReducer = (state, action) => {
switch (action.type) {
// case HYDRATE:
// // console.log('HYDRATE', action);
// return action.payload;
default: {
const combinedReducer = combineReducers({
user,
post,
});
return combinedReducer(state, action);
}
}
};
export default rootReducer;
(src/index.js)
import {KAKAOLOG_IN_REQUEST} from '../sagas/user';
const App = ({}) => {
const dispatch = useDispatch();
useEffect(() => {
async function fetchAndSetUser() {
try {
const token = await AsyncStorage.getItem('tokenstore');
const {data} = await axios.post(
'/auth/me',
{},
{
headers: {Authorization: `Bearer ${token}`},
},
);
console.log('data::::::', data);
dispatch({
type: 'KAKAOLOG_IN_REQUEST',
data: data,
});
} catch (error) {
}
}
fetchAndSetUser();
}, []);
return <Navigator />;
};
export {App};
Issue
The error message is saying your code can throw an error and it isn't handled. It is also saying that KAKAOLOG_IN_REQUEST is undefined for some reason (perhaps you forgot to import it, or it is really a string).
Solution
Surround your asynchronous code in a try/catch. Define KAKAOLOG_IN_REQUEST or pass as a string "KAKAOLOG_IN_REQUEST".
useEffect(() => {
async function fetchAndSetUser() {
try {
const token = await AsyncStorage.getItem('tokenstore');
const {data} = await axios.post(
'/auth/me',
{},
{
headers: { Authorization: `Bearer ${token}` },
},
);
console.log("data:",data);
dispatch({
type: 'KAKAOLOG_IN_REQUEST',
data: data,
});
} catch(error) {
// handle error, logging, etc...
}
}
fetchAndSetUser();
}, []);
I have React app which have page with Post. But now I rework this app to Redux.
And when in file Post.js I rework this small part:
useEffect(() => {
fetchListCategory();
}, []);
async function fetchListCategory() {
const dataCategories = await api(`${listRoute}?limit=100`, {
method: 'GET',
});
setValue(prev => ({
...prev,
listCategories: dataCategories.data,
}));
}
to Redux:
useEffect(() => {
fetchListCategory()(dispatch);
}, []);
function fetchListCategory() {
return async (dispatch) => {
dispatch({ type: "LOAD_DATA_START_SELECT" });
const dataCategories = await api(`${listRoute}?limit=100`, {
method: 'GET',
});
dispatch({ type: "LOAD_DATA_END_SELECT", payload: dataCategories });
}
I get error in file Post.js in line export default Post;:
'import' and 'export' may only appear at the top level
How to fix this error?
P.S.Now below in question I will write some part of file Post.js, and his children components. If you need more information write in the comments, I send for you screenshot or I add more information in question.
Post.js:
const Post = () => {
const dispatch = useDispatch();
const listImage = useSelector(state => state.filterImageReducer.listImage);
useEffect(() => {
fetchListCategory()(dispatch);
}, []);
function fetchListCategory() {
return async (dispatch) => {
dispatch({ type: "LOAD_DATA_START_SELECT" });
const dataCategories = await api(`${listRoute}?limit=100`, {
method: 'GET',
});
dispatch({ type: "LOAD_DATA_END_SELECT", payload: dataCategories });
}
useEffect(() => {
fetchDataPost()(dispatch);
}, []);
function fetchDataPost() {
return async (dispatch) => {
dispatch({ type: "LOAD_DATA_START_POSTS" });
const data = await apiImage(`${imageRoute}?orderBy=created_at`, {
method: 'GET',
});
dispatch({ type: "LOAD_DATA_END_POSTS", payload: data });
}
}
return (
<div>
<DisplayPost dataAttribute={listImage}/>
<AddPost fetchDataPost={fetchDataPost}/>
<EditPost fetchDataPost={fetchDataPost}/>
</div>
);
};
export default Post;
You have a missing } where fetchListCategory function is not properly terminated which is why you get this error.
function fetchListCategory() {
return async (dispatch) => {
dispatch({ type: "LOAD_DATA_START_SELECT" });
const dataCategories = await api(`${listRoute}?limit=100`, {
method: 'GET',
});
dispatch({ type: "LOAD_DATA_END_SELECT", payload: dataCategories });
}
} <- an extra bracket here
Also assuming you are using redux-thunk as a middleware, you do can simply dispatch fetchListCategory like
dispatch(fetchListCategory());
instead of
fetchListCategory()(dispatch);
Also you can take out the implementation of fetchListCategory outside of your component
I have store structure like this
/store
/entities
users.js
sessions.js
All of my entities looks like this
import { FORM_ERROR } from 'react-final-form';
import { normalize, schema } from 'normalizr';
import { SCHEMA as USER_SHEMA } from './users';
export const SCHEMA = new schema.Entity(
'sessions',
{ user: USER_SHEMA },
{ idAttribute: 'sessionGuid' },
);
export const CREATE_SESSION_REQUEST = 'CREATE_SESSION_REQUEST';
export const CREATE_SESSION_SUCCESS = 'CREATE_SESSION_SUCCESS';
export const CREATE_SESSION_FAILURE = 'CREATE_SESSION_FAILURE';
export const create = session => async (dispatch, getState, api) => {
dispatch({ type: CREATE_SESSION_REQUEST });
try {
const { data } = await api.post('/sessions', session);
const payload = normalize(data, SCHEMA);
dispatch({ type: CREATE_SESSION_SUCCESS, payload });
} catch (error) {
dispatch({ type: CREATE_SESSION_FAILURE, payload: error, error: true });
return { [FORM_ERROR]: error };
}
return null;
};
export const FETCH_ALL_SESSIONS_REQUEST = 'FETCH_ALL_SESSIONS_REQUEST';
export const FETCH_ALL_SESSIONS_SUCCESS = 'FETCH_ALL_SESSIONS_SUCCESS';
export const FETCH_ALL_SESSIONS_FAILURE = 'FETCH_ALL_SESSIONS_FAILURE';
export const fetchAll = params => async (dispatch, getState, api) => {
dispatch({ type: FETCH_ALL_SESSIONS_REQUEST });
try {
const { data } = await api.get('/sessions', { params });
const payload = normalize(data, [SCHEMA]);
dispatch({ type: CREATE_SESSION_SUCCESS, payload });
} catch (error) {
dispatch({ type: FETCH_ALL_SESSIONS_FAILURE, payload: error, error: true });
}
};
export const GET_ONE_SESSION_REQUEST = 'GET_ONE_SESSION_REQUEST';
export const GET_ONE_SESSION_SUCCESS = 'GET_ONE_SESSION_SUCCESS';
export const GET_ONE_SESSION_FAILURE = 'GET_ONE_SESSION_FAILURE';
export const fetchOne = sessionId => async (dispatch, getState, api) => {
dispatch({ type: GET_ONE_SESSION_REQUEST });
try {
const { data } = await api.get(`/sessions/${sessionId}`);
const payload = normalize(data, SCHEMA);
dispatch({ type: GET_ONE_SESSION_SUCCESS, payload });
} catch (error) {
dispatch({ type: GET_ONE_SESSION_FAILURE, payload: error, error: true });
}
};
export const UPDATE_SESSION_REQUEST = 'UPDATE_SESSION_REQUEST';
export const UPDATE_SESSION_SUCCESS = 'UPDATE_SESSION_SUCCESS';
export const UPDATE_SESSION_FAILURE = 'UPDATE_SESSION_FAILURE';
export const update = (sessionId, session) => async (dispatch, getState, api) => {
dispatch({ type: UPDATE_SESSION_REQUEST, meta: { id: sessionId } });
try {
await api.put(`/sessions/${sessionId}`, session);
dispatch({ type: UPDATE_SESSION_SUCCESS, meta: { id: sessionId } });
} catch (error) {
dispatch({
type: UPDATE_SESSION_FAILURE,
error: true,
payload: error,
meta: { id: sessionId },
});
return { [FORM_ERROR]: error };
}
return null;
};
export const DESTROY_SESSION_REQUEST = 'DESTROY_SESSION_REQUEST';
export const DESTROY_SESSION_SUCCESS = 'DESTROY_SESSION_SUCCESS';
export const DESTROY_SESSION_FAILURE = 'DESTROY_SESSION_FAILURE';
export const destroy = sessionId => async (dispatch, getState, api) => {
dispatch({ type: DESTROY_SESSION_REQUEST, meta: { id: sessionId } });
try {
await api.delete(`/sessions/${sessionId}`);
dispatch({ type: DESTROY_SESSION_SUCCESS, meta: { id: sessionId } });
} catch (error) {
dispatch({
type: DESTROY_SESSION_FAILURE,
error: true,
payload: error,
meta: { id: sessionId },
});
}
};
I wanna use import * as ns from './ns' syntaxys in my components, so i export my actionTypes like as constants. In users.js the same code, but all the all words session are replaced with users. My project now has 12 entities, and when I replaced all the keywords in the 13th file, I remembered the DRY principle. But I did not find the right way to reduce the template code in such a way that would not change the imports.
import * as sessionsActions from 'store/modules/entities/sessions';
How to minimize the boilerplate by saving the current code?
I'm having trouble getting the correct output from an async redux action. I am using Jest, redux-mock-adapter, and thunk as the tools.
According to redux's documentation on testing async thunks (https://redux.js.org/docs/recipes/WritingTests.html#async-action-creators), my tests should be returning an array of two actions. However, my test is only returning the first action, and not the second one that should return on a successful fetch. I think I'm just missing something small here, but it has been bothersome to say the least.
Redux Action
export const getRemoveFileMetrics = cacheKey => dispatch => {
dispatch({ type: IS_FETCHING_DELETE_METRICS });
return axios
.get("GetRemoveFileMetrics", { params: { cacheKey } })
.then(response => dispatch({ type: GET_REMOVE_FILE_METRICS, payload: response.data }))
.catch(err => err);
};
Test
it("getRemoveFileMetrics() should dispatch GET_REMOVE_FILE_METRICS on successful fetch", () => {
const store = mockStore({});
const cacheKey = "abc123doremi";
const removeFileMetrics = {
cacheKey,
duplicateFileCount: 3,
uniqueFileCount: 12,
};
const expectedActions = [
{
type: MOA.IS_FETCHING_DELETE_METRICS,
},
{
type: MOA.GET_REMOVE_FILE_METRICS,
payload: removeFileMetrics,
}
];
mockRequest.onGet(`/GetRemoveFileMetrics?cacheKey=${cacheKey}`).reply(200, removeFileMetrics);
return store.dispatch(MOA.getRemoveFileMetrics(cacheKey)).then(() => {
const returnedActions = store.getActions();
expect(returnedActions).toEqual(expectedActions);
});
});
The Output
Expected value to equal:
[{ "type": "IS_FETCHING_DELETE_METRICS" }, { "payload": { "cacheKey": "abc123doremi", "duplicateFileCount": 3, "uniqueFileCount": 12 }, "type": "GET_REMOVE_FILE_METRICS" }]
Received:
[{ "type": "IS_FETCHING_DELETE_METRICS" }]
I am using jest-fetch-mock and no axios. The following is working for me with the actions. You could refactor to async await as first step. For me it only worked that way.
I am now trying to figure out how to test the side effect (showErrorAlert(jsonResponse);). If I mock out the showErrorAlert implementation at the top of the test file (commented out in my example) then I get the same problem just like you. Actions that uses fetch won't get triggered for some reason.
export const submitTeammateInvitation = (data) => {
const config = {
//.....
};
return async (dispatch) => {
dispatch(submitTeammateInvitationRequest());
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess());
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
// jest.mock('../../../../_helpers/alerts', ()=> ({ showAlertError: jest.fn() }));
const middlewares = [thunk];
const createMockStore = configureMockStore(middlewares);
......
it('dispatches the correct actions on a failed fetch request', () => {
fetch.mockResponse(
JSON.stringify(error),
{ status: 500, statusText: 'Internal Server Error' }
);
const store = createMockStore({});
const expectedActions = [
{
type: 'SUBMIT_TEAMMATE_INVITATION_REQUEST',
},
{
type: 'SUBMIT_TEAMMATE_INVITATION_FAILURE',
payload: { error }
}
];
return store.dispatch(submitTeammateInvitation(data))
.then(() => {
// expect(alerts.showAlertError).toBeCalled();
expect(store.getActions()).toEqual(expectedActions);
});
});