I am using React with react-redux, redux and redux-actions.
I have one action that takes the current token stored in localStorage and ensures that it is not expired, like so:
export const verifyLogin = () => {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: `${
localStorage.getItem("token")
? localStorage.getItem("token")
: "not_valid_token"
}`
},
onSuccess: verifiedLogin,
onFailure: failedLogin
});
};
function verifiedLogin(data) {
const user = {
...data.user
}
setUser(user);
return {
type: IS_LOGGED_IN,
payload: true
};
}
function failedLogin(data) {
return {
type: IS_LOGGED_IN,
payload: false
};
}
When it verifies the token it returns a response like so:
{
token: "token_data",
user: {
username: "this",
is_staff: true,
(etc...)
}
}
As you can see in verifiedLogin(), it is calling another function (in this case an action creator) to set the user to the user object returned by my API. the setUser is defined like this:
const setUser = createAction(SET_USER);
which should create an Action like this:
{
type: SET_USER,
payload: {
userdata...
}
}
The reducer is defined like this:
import { handleActions } from "redux-actions";
import { SET_USER } from "../constants/actionTypes";
export default handleActions(
{
[SET_USER]: (state, action) => action.payload
},
{}
);
I know the action creator is correct, as I have verified by console.log(setUser(user)); but all that is in the state is an empty object for users. I am unable to determine why it is not working successfully. I am new to React and Redux so it may be something I misunderstood.
Edit:
This is apiPayloadCreator:
const noOp = () => ({ type: "NO_OP" });
export const apiPayloadCreator = ({
url = "/",
method = "GET",
onSuccess = noOp,
onFailure = noOp,
label = "",
data = null
}) => {
console.log(url, method, onSuccess, onFailure, label, data);
return {
url,
method,
onSuccess,
onFailure,
data,
label
};
};
Even though you are calling setUser, it is not being dispatched by Redux, which is what ultimately executes a reducer and updates the store. Action creators like setUser are not automatically wired up to be dispatched; that is done in the connect HOC. You will need a Redux middleware such as redux-thunk to dispatch async / multiple actions. Your code can then be something like the example below (using redux-thunk):
export const verifyLogin = () => (dispatch) => {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: `${
localStorage.getItem("token")
? localStorage.getItem("token")
: "not_valid_token"
}`
},
onSuccess: (result) => verifiedLogin(dispatch, result),
onFailure: (result) => diapatch(failedLogin(result))
});
};
const verifiedLogin = (dispatch, data) => {
const user = {
...data.user
};
dispatch(setUser(user));
dispatch({
type: IS_LOGGED_IN,
payload: true
});
};
You're going to need to use something like redux-thunk in order to do async actions. See the documentation on how this is done.
Related
I have a react app where I use the useContext and useReducer hooks for the login and storage. While the login part works, what I want achieve is to redirect user to a specific page post successful login. I am using react-router#6 and tried to use useNavigate() to navigate user to particular route though it doesn't seem to work.
const AuthService = async (dispatch) => {
const MSAL_CONFIG = {} // populate MSAL config for Microsoft Graph API for AD auth
const msalInstance = new msal.PublicClientApplication(MSAL_CONFIG);
try {
const loginResponse = await msalInstance.loginPopup(scopes);
var username = loginResponse.account.username;
var userid = username.slice(0, username.indexOf("#"));
const loginData = {
auth_token: loginResponse.idToken,
user: {
name: loginResponse.account.name,
id: userid,
email: username,
},
};
const sessionData = {
user_id: userid,
id_token: loginResponse.idToken,
access_token: loginResponse.accessToken,
}
sessionStorage.setItem("currentUser", JSON.stringify(loginData));
dispatch({ type: "LOGIN_SUCCESS", payload: loginData });
return { loginData: loginData, error: null };
// dispatch({ type: 'LOGIN_SUCCESS', payload: loginData });
//sessionStorage.setItem('currentUser', JSON.stringify(data));
} catch (err) {
console.log("+++ Login error : ", err);
dispatch({ type: "LOGIN_ERROR", error: err });
return { loginData: null, error: err };
}
};
In my header.jsx, I have below code to handle the login button. It makes a call to the above AuthService. The code post AuthService() call, i.e. the if block, doesn't take effect, so user never gets redirected to the dashboard page.
const handleLogin = async () => {
await AuthService(dispatch)
console.log("userDetails.token : " + userDetails.token)
if (Boolean(userDetails.token)) {
navigate("/dashboard");
}
};
If I'm correct in understanding that this AuthService function eventually resolves and that the dispatched LOGIN_SUCCESS action updates the userDetails variable that is selected from the auth context state, then I think you have all that you need and are close to a working solution. The issue is that the userDetails value from the render cycle the handleLogin is called in is closed over in callback scope, it will never be a different value. If the userDetails.token value is falsey when handleLogin is called, it will remain falsey in the entire callback scope.
The AuthService function appears to return the same loginData object that is passed in the dispatched LOGIN_SUCCESS action to the store. handleLogin should await this value and conditionally navigate.
const AuthService = async (dispatch) => {
...
try {
const { account, idToken } = await msalInstance.loginPopup(scopes);
const { name, username } = account;
const userid = username.slice(0, username.indexOf("#"));
const loginData = {
auth_token: idToken,
user: {
name,
id: userid,
email: username,
},
};
...
sessionStorage.setItem("currentUser", JSON.stringify(loginData));
dispatch({ type: "LOGIN_SUCCESS", payload: loginData });
return { loginData, error: null }; // <-- return value
} catch (error) {
dispatch({ type: "LOGIN_ERROR", error });
return { loginData: null, error }; // <-- return value
}
};
const handleLogin = async () => {
const { loginData } = await AuthService(dispatch);
if (loginData && loginData.auth_token) { // or loginData?.auth_token
navigate("/dashboard", { replace: true });
}
};
I want to try and use react to fetch data efficiently using useEffect appropriately.
Currently, data fetching is happening constantly, instead of just once as is needed, and changing when there is a input to the date period (calling different data).
The component is like this,
export default function Analytics() {
const {
sentimentData,
expressionsData,
overall,
handleChange,
startDate,
endDate,
sentimentStatistical,
} = useAnalytics();
return (
UseAnalytics is another component specifically for fetching data, basically just a series of fetches.
i.e.,
export default function useAnalytics() {
....
const { data: sentimentData } = useSWR(
`dashboard/sentiment/get-sentiment-timefilter?startTime=${startDate}&endTime=${endDate}`,
fetchSentiment
);
....
return {
sentimentData,
expressionsData,
overall,
handleChange,
setDateRange,
sentimentStatistical,
startDate,
endDate,
};
}
Thanks in advance,
The apirequest is like this,
export async function apiRequest(path, method = "GET", data) {
const accessToken = firebase.auth().currentUser
? await firebase.auth().currentUser.getIdToken()
: undefined;
//this is a workaround due to the backend responses not being built for this util.
if (path == "dashboard/get-settings") {
return fetch(`/api/${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: data ? JSON.stringify(data) : undefined,
})
.then((response) => response.json())
.then((response) => {
if (response.error === "error") {
throw new CustomError(response.code, response.messages);
} else {
return response;
}
});
}
return fetch(`/api/${path}`, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: data ? JSON.stringify(data) : undefined,
})
.then((response) => response.json())
.then((response) => {
if (response.status === "error") {
// Automatically signout user if accessToken is no longer valid
if (response.code === "auth/invalid-user-token") {
firebase.auth().signOut();
}
throw new CustomError(response.code, response.message);
} else {
return response.data;
}
});
}
I think using useEffect here is the right approach. i.e.,
useEffect(()=>{
// this callback function gets called when there is some change in the
// state variable (present in the dependency array)
},[state variable])
I'm confused about how to update the constants properly, something like this seems like one approach, but not sure about how I can use useEffect to update these variables properly, or if I should be doing this inside of useAnalytics?
i.e.,
const [analytics, setAnalytics] = useState({
sentimentData: {},
expressionsData: {},
overall: {},
handleChange: () => {},
startDate: '',
endDate: '',
sentimentStatistical:{},
});
useEffect(()=>{
// this callback function gets called when there is some change in the
// state variable (present in the dependency array)
},[state variable])
const {
sentimentData,
expressionsData,
overall,
handleChange,
startDate,
endDate,
sentimentStatistical,
} = useAnalytics();
Realised SWR is a hook, need to use SWR documentation :P
You have to store the requested information in states inside your custom hook. Then you could consume this hook wherever you want. This should work.
Define custom hook
const useAnalitycs = () => {
const [analytics, setAnalytics] = useState({
sentimentData: {},
expressionsData: {},
overall: {},
startDate: '',
endDate: '',
sentimentStatistical:{},
});
const handleChange = () => {
/* */
};
useEffect(() => {
const fetchData = async () => {
// const response = await apiCall();
// setAnalytics(...)
};
fetchData();
}, []); // called once
return {
...analytics,
handleChange
};
};
Consume useAnalytics hook
const ComponentConsumerA = () => {
/*
const { state/methods you need } = useAnalytics()
...
*/
};
const ComponentConsumerB = () => {
/*
const { state/methods you need } = useAnalytics()
...
*/
};
I have an authentication action on an react native app which must during the authentication go to perform another action but it is never executed (dispatch(getMobiles())). I do not understand why. Do you have an idea ?
If my authentication went well, I immediately want to retrieve data on my new users, so I want to execute getMobiles () which is another action.
thanks in advance :)
auth actions
export const authentication = (
username: String,
password: String,
label: String,
synchro: Boolean,
url: String,
) => {
return dispatch => {
dispatch({type: LOGIN.PENDING, payload: ''});
const type = UDA_URL_LIST.map(uda => {
if (uda.url === url) {
return uda.name;
}
})
.join()
.replace(/[, ]+/g, ' ')
.trim();
fetchUser(url, username.trim(), password.trim())
.then(response => {
if (!response.err) {
const newUser = {
...response,
label,
type,
synchro,
};
dispatch({type: LOGIN.SUCCESS, payload: newUser});
// not dispatched !
return dispatch(getMobiles(url, response.key, newUser.userId));
}
})
.catch(err => dispatch({type: LOGIN.ERROR, payload: err}));
};
};
getMobiles
export const getMobiles = (
url: String | null = null,
token: String,
userId: String,
) => {
return dispatch => {
dispatch({type: MOBILES.PENDING, payload: ''});
fetchMobiles(url, token)
.then(mobilesList => {
dispatch({
type: MOBILES.SUCCESS,
payload: mobilesList.data,
meta: {userId},
});
})
.catch(err => alert(err));
};
};
};
Your code in getMobiles require second call with parametr dispatch,
try to use getMobiles(url, response.key, newUser.userId)(dispatch)
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.
I have an onSubmit function with an axios post that allows to register a user.
I would like to know if it is possible if when the user create his account, at the time of the submission a open modal with a timeout ?
I already have the component of the modal created with redux but I do not know how to integrate it in this Axios
Axios Post
const onSubmit = async function onSubmit(values) {
axios({
method: 'POST',
url: 'http://localhost:4242/registerUser',
data: values,
headers: { 'Content-Type': 'application/json' },
})
.then((res) => {
localStorage.setItem("token", res.headers["x-access-token"])
})
.catch(function (erreur) {
console.log(erreur);
})
}
Modal Reducer
export const registerModal = id => ({
type: "REGISTER_MODAL",
id
});
export const showModal = id => ({
type: "SHOW_MODAL",
id
});
export const hideModal = id => ({
type: "HIDE_MODAL",
id
});
const initialState = {
// modals: []
modals: {}
};
const modals = (state = initialState, action) => {
switch (action.type) {
case "REGISTER_MODAL":
const newModal = {
id: action.id,
visible: false
};
return {
...state,
modals: { ...state.modals, [action.id]: newModal }
};
case "SHOW_MODAL":
return {
...state,
modals: {
...state.modals,
[action.id]: { ...state.modals[action.id], visible: true }
}
};
case "HIDE_MODAL":
return {
...state,
modals: {
...state.modals,
[action.id]: { ...state.modals[action.id], visible: false }
}
};
default:
return state;
}
};
export default combineReducers({
modals
});
Ok if i get it right you want to open the modal after the post was received..
So I would try to put the action of the open modal inside the then method o request promise:
const onSubmit = async function onSubmit(values) {
axios({
method: 'POST',
url: 'http://localhost:4242/registerUser',
data: values,
headers: { 'Content-Type': 'application/json' },
})
.then((res) => {
localStorage.setItem("token", res.headers["x-access-token"])
// Here you are sure that your post was successfull I think...
// The issue here will be to get the res and the dispatcher function, this will vary for the pattern that you are following
modalReducer.showModal( res.id );
})
.catch(function (erreur) {
console.log(erreur);
})
}
A thing that i cant solve is how are you using your reducer inside the Component that is handling your post.You are passing the id? or the id already exist in the component?.