I wrote a Register component in react, it is a simple form that on submit will post to an API. The call to the API will return an object with certain data, this data will be then added to the redux store.
I wrote some tests for this. I'm using Mock Service Worker (MSW) to mock the API call. This is my first time for writing these kind of tests so I'm not sure if I'm doing anything wrong, but my understanding was that MSW would intercept the call to the API and return whatever I specify in the MSW config, after that it should follow the regular flow.
Here's my reducer:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = new User().register(
action.payload.email,
action.payload.firstName,
action.payload.lastName,
action.payload.password
)
console.log("User registered data back:");
console.log(newUser);
return {
...state,
'user': newUser
}
default:
return state;
}
}
this is my User class where the actual call is performed:
import axios from "axios";
import { REGISTER_API_ENDPOINT } from "../../api";
export default class User {
/**
* Creates a new user in the system
*
* #param {string} email - user's email address
* #param {string} firstName - user's first name
* #param {string} lastName - user's last name
* #param {string} password - user's email address
*/
register(email, firstName, lastName, password) {
// console.log("registering...")
axios.post(REGISTER_API_ENDPOINT, {
email,
firstName,
lastName,
password
})
.then(function (response) {
return {
'email': response.data.email,
'token': response.data.token,
'active': response.data.active,
'loggedIn': response.data.loggedIn,
}
})
.catch(function (error) {
console.log('error');
console.log(error);
});
}
}
this is my action creator:
export function createNewUser(userData) {
return {
type: REGISTER_NEW_USER,
payload: userData
}
}
this is the onSubmit method in my Register component:
const onSubmit = data => {
// console.log(data);
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
dispatch(createNewUser(data))
}
and this is my actual test:
describe('Register page functionality', () => {
const server = setupServer(
rest.post(REGISTER_API_ENDPOINT, (req, res, ctx) => {
console.log("HERE in mock server call")
// Respond with a mocked user object
return res(
ctx.status(200),
ctx.json({
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}))
})
)
// Enable API mocking before tests
beforeEach(() => server.listen());
// Reset any runtime request handlers we may add during the tests.
afterEach(() => server.resetHandlers())
// Disable API mocking after the tests are done.
afterAll(() => server.close())
it('should perform an api call for successful registration', async () => {
// generate random data to be used in the form
const email = faker.internet.email();
const firstName = faker.name.firstName();
const lastName = faker.name.lastName();
const password = faker.internet.password();
// Render the form
const { store } = renderWithRedux(<Register />);
// Add values to the required input fields
const emailInput = screen.getByTestId('email-input')
userEvent.type(emailInput, email);
const firstNameInput = screen.getByTestId('first-name-input');
userEvent.type(firstNameInput, firstName);
const lastNameInput = screen.getByTestId('last-name-input');
userEvent.type(lastNameInput, lastName);
const passwordInput = screen.getByTestId('password-input');
userEvent.type(passwordInput, password);
const confirmPasswordInput = screen.getByTestId('confirm-password-input');
userEvent.type(confirmPasswordInput, password);
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
// verify the store was populated
console.log(await store.getState())
});
});
So I was expecting my call to be intercepted whenever the REGISTER_API_ENDPOINT url is detected, and the value of the mocked call to be added to my redux state instead of the value of the actual API call in register method but that doesn't seem to be happening. If that's not the way to test a value in the store, how else can I achieve that?
So at the end of my test, when printing the store I was expecting to see:
{ auth: { user:
{
'email': faker.internet.email(),
'token': faker.datatype.uuid(),
'active': true,
'loggedIn': true,
}
}
but instead I'm seeing:
{ auth: { user: null } }
Is this the right approach for this test?
Thanks
EDIT
Doing some refactoring based on the comments. Now my onSubmit method looks like:
const onSubmit = async data => {
if (data.password !== data.confirmPassword) {
console.log("Invalid password")
setError('password', {
type: "password",
message: "Passwords don't match"
})
return;
}
// if we got up to this point we don't need to submit the password confirmation
// todo but we might wanna pass it all the way through to the backend TBD
delete data.confirmPassword
let user = new User()
await user.register(data).
then(
data => {
// console.log("Response:")
// console.log(data)
// create cookies
cookie.set("user", data.email);
cookie.set("token", data.token);
dispatch(createNewUser(data))
}
).catch(err => console.log(err))
Notice that now I'm dispatching the response from User.register in here instead of doing it in User.register. Also notice that this function is now async and await for the register function call to be finalized, at that moment it'll populate the store.
The register method now looks like the following:
async register(data) {
let res = await axios.post(REGISTER_API_ENDPOINT, {
'email': data.email,
'firstName': data.firstName,
'lastName': data.lastName,
'password': data.password
})
.then(function (response) {
return response
})
.catch(function (error) {
console.log('error');
console.log(error);
});
return await res.data;
}
now it's only in charge of performing the API call and returning the response.
The reducer was also simplified not to have any side effect changes, so it looks like:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_NEW_USER:
const newUser = action.payload
return {
...state,
'user': newUser
}
default:
return state;
}
}
my test is mostly the same, the only difference is the part where I'm inspecting the store value:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
});
await waitFor(() => {
// verify the store was populated
console.log("Store:")
console.log(store.getState())
})
Now, this sometimes work and sometimes does not. Meaning, sometimes I get correct store printed as follows:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{
auth: {
user: {
email: 'Selena.Tremblay#hotmail.com',
token: '1a0fadc7-7c13-433b-b86d-368b4e2311eb',
active: true,
loggedIn: true
}
}
}
at test/pages/Register.test.js:220:21
but sometimes I'm getting null:
console.log
Store:
at test/pages/Register.test.js:219:21
console.log
{ auth: { user: null } }
at test/pages/Register.test.js:220:21
I guess I'm missing some async code somewhere but I cannot put a pin on where is it.
There are some Redux rules that are being broken here:
Don't do side effects in reducers:
reducers should be pure functions: for the same input, return always
the same output. This is not the place to do API calls.
State should be immutable: you should never change a state value by reference, always provide a new state with a new object containing the changes.
So, the classical redux approach would be to have three actions in Redux: REGISTER_USER, REGISTER_USER_SUCCEEDED, REGISTER_USER_FAILED .
reducer:
const authReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case actionTypes.REGISTER_USER:
return {
...state,
status: 'loading'
}
case actionTypes.REGISTER_USER_SUCCEEDED:
return {
...state,
status: 'idle',
user: action.user
}
case actionTypes.REGISTER_USER_FAILED:
return {
...state,
status: 'error'
}
default:
return state;
}
}
Then, async work should be done in your event handlers:
onSubmit:
const onSubmit = async data => {
// ...
dispatch(registerNewUser());
const user = new User()
try {
await user.register(data);
dispatch(registerNewUserSucceeded(user));
} catch(e) {
console.error(e);
dispatch(registerNewUserFailed());
}
}
**Don't forget to return the promise from axios inside your register function, so you can await on the promise. Currently, you are only calling axios, but not updating or returning anything...
What's great about this, is that testing your store doesn't require you to do any network calls! You could ditch MSW (although it's a great lib, just not needed here).
In your tests, just check your store state before and after every transition:
const mockUser = {...} // provide a mock user for your test
const store = createStore(authReducer);
store.dispatch(registerNewUserSucceeded(mockUser);
expect(store.getState()).toEqual({user: mockUser, status: 'idle'});
Edit
In response to the asker's edit, there is now a bug because of the confusing combination of await with .then.
Specifically, in onSubmit, you are doing both await and .then on the same promise. In this case, there is a race condition. The .then call happens first, and after that the await happens.
So instead of await user.register(data).then(...):
const onSubmit = async data => {
// ...
try {
await user.register(data);
} catch(e) {
console.log(e);
}
dispatch(createNewUser(data));
}
Here I'm only using await. the try/catch clause is instead of calling .catch on the promise.
using await lets you write as if you are writing synchronic code, so just write whatever you would put inside .then on the next line after an await expression.
Also in your register function:
async register(data) {
try {
let res = await axios.post(...);
return res;
} catch(e) {
console.log("error: ", e);
}
}
The state won't be updated instantly, as the server call is a promise. You should await something on the page the indicates the process is complete like this:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
await wait(() => getByText('Some text that appears after success '));
// verify the store was populated
console.log(await store.getState())
});
Or you can wait for the update:
// Click on the Submit button
await act(async () => {
userEvent.click(screen.getByTestId('register-submit-button'));
await act(() => sleep(500));
// verify the store was populated
console.log(await store.getState())
});
Related
I'm trying to use Firebase custom claims to protect content for my users, but the first time a user signs up and is redirected to /protectedpage, they cannot view the page because their claim is not set. If they log out and log back in, everything works properly.
Signup Flow
User signs up with email and password
A user document is created in a users collection in Firestore
The user is redirected to /protectedpage
Creation of the user document triggers a cloud function which assigns the custom claim role=A or role=B depending on the information in the user document.
In Javascript (React), it looks like this
Client side
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Add a new document in collection "users"
setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Send email verification
sendEmailVerification(userCredential.user)
.then(() => {
// Redirect to home page
router.push('/protectedpage');
})
.catch((error) => {
console.log("Error sending email verification", error.message);
});
})
.catch((error) => {
setFormError(error.message);
})
Server side
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// This function runs when a document is created in
// the users collection
exports.createUser = functions.firestore
.document('users/{userId}')
.onCreate(async (snap, context) => {
// Get an object representing the document
const doc = snap.data()
const userId = context.params.userId;
// Declare customClaims
let customClaims = {};
// Assign user role
if (doc.account_type == 'A') {
customClaims["role"] = "A"
} else if (doc.account_type == 'B') {
customClaims["role"] = "B"
} else {
functions.logger.info('A role could not be assigned to user:', doc)
response.send('Error: A role could not be assigned')
}
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(userId, customClaims);
} catch (error) {
functions.logger.info(error);
}
return "OK"
})
By the time the user gets to /protectedpage, his JWT does not have the custom claim.
Authorization
My authorization code is using a React context manager, and looks like this
import { createContext, useContext, useEffect, useState } from 'react'
import { onAuthStateChanged, signOut as authSignOut } from 'firebase/auth'
import { auth } from './firebase'
export default function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const clear = () => {
setAuthUser(null)
setIsLoading(false)
}
const authStateChanged = async (user) => {
setIsLoading(true)
if (!user) {
clear()
return
}
// Use getIdTokenResult() to fetch the custom claims
user.getIdTokenResult()
.then((idTokenResult) => {
console.log("idTokenResult", idTokenResult)
setAuthUser({
uid: user.uid,
email: user.email,
role: idTokenResult.claims.role,
})
setIsLoading(false)
})
.catch((error) => {
console.log(error)
})
}
const signOut = () => authSignOut(auth).then(clear)
// Listen for Firebase Auth state change
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, authStateChanged)
return () => unsubscribe()
}, [])
return {
authUser,
isLoading,
signOut,
}
}
const AuthUserContext = createContext({
authUser: null,
isLoading: true,
signOut: async () => {},
})
export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth()
return (
<AuthUserContext.Provider value={auth}>{children}</AuthUserContext.Provider>
)
}
export const useAuth = () => useContext(AuthUserContext)
If I change user.getIdTokenResult() to user.getIdTokenResult(true), the user no longer has to sign out and sign back in to access the custom claim BUT
They need to manually refresh the page to acquire the custom claim
I think this is bad, as it's going to forcibly refresh the token on every page load ??
The Firebase docs seem to address this problem with some trickery involving "metadataRef" but I don't understand it exactly, as I think it's related to the Realtime database whereas I'm using Firestore.
Finally got this to work. Two things were tripping me up.
router.push('/protectedpage') doesn't do a hard refresh. I changed this to window.location.replace('/protectedpage')
Instead of assigning the custom claim on creation of the user record, I wrote a cloud function to do it. After my user is created, I call this function. After I get the response, then I redirect the user to /protectedpage
My cloud function looks like this
const functions = require('firebase-functions')
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
initializeApp();
// IMPORTANT:
// Note the distinction between onCall and onRequest
// With onCall, authentication / user information is automatically added to the request.
// https://stackoverflow.com/questions/51066434/firebase-cloud-functions-difference-between-onrequest-and-oncall
// https://firebase.google.com/docs/functions/callable
// Function to set a user's role as either "A" or "B"
exports.setRole = functions.https.onCall((data, context) => {
// Check that the user is authenticated.
if (!context.auth) {
// Throw an HttpsError so that the client gets the error details.
// List of error codes: https://firebase.google.com/docs/reference/node/firebase.functions#functionserrorcode
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
// Confirm that the function contains a role
if (!data.hasOwnProperty("role")) {
throw new functions.https.HttpsError(
'failed-precondition',
"The function data must contain a 'role'"
);
}
// Confirm that role is either A or B
if (data.role !== "A" && data.role !== "B") {
throw new functions.https.HttpsError(
'failed-precondition',
"'role' must be set to either 'A' or 'B'"
);
}
// Confirm that the user doesn't already have a role
if (context.auth.token.role) {
throw new functions.https.HttpsError(
'failed-precondition',
"The user's role has already been set"
);
}
// Assign the role
// IMPORTANT:
// We need to return the promise! The promise returns the response. This way, on the client,
// we can wait for the promise to get resolved before moving onto the next step.
return getAuth().setCustomUserClaims(context.auth.uid, { role: data.role })
.then(() => {
return "OK"
})
.catch((error) => {
throw new functions.https.HttpsError(
'internal',
'Error setting custom user claim'
);
})
})
and I call it from the client like this
// Handle form submission
const onSubmit = (formValues) => {
// Create a new user with email and password
createUserWithEmailAndPassword(auth, formValues.email, formValues.password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// Send email verification
sendEmailVerification(user);
// Add a new document in collection "users"
const promise1 = setDoc(doc(db, "users", user.uid), {
account_type: formValues.account_type,
full_name: formValues.full_name,
});
// Set the user role (custom claim)
// Then force refresh the user token (JWT)
const setRole = httpsCallable(functions, 'setRole');
const promise2 = setRole({ role: formValues.account_type })
.then(() => user.getIdTokenResult(true));
// When the user document has been created and the role has been set,
// redirect the user
// IMPORTANT: router.push() doesn't work for this!
Promise.all([promise1, promise2]).then((values) => {
window.location.replace('/protectedpage');
})
})
.catch((error) => {
setFormError(error.message);
})
}
I have an App.js file that contains a form that when on submitted, causes triggers a state change to render a new page. I'm trying to create a mock Jest test that does these steps:
Take mock data
Sends a POST request like addInfo is doing
Checks if "DONE WITH FORM" is rendered onto the screen.
I also had an idea that we could just fill out a form that takes in the valid_address and valid_number and click a button that triggers the addInfo function to run with the information passed in however I'm unsure of that method and it leads me to a CORS error.
From what I've seen on the web, I think mocking this addInfo using Jest and then testing what is rendered is the best way to go however I'm completely stuck on building this test.
Here's what I have for my App.js
const addInfo = async (formInfo) => {
try {
let data = {
valid_number: formInfo.validNumber,
valid_address: formInfo.validAddress
}
let addUserUrl = process.env.REACT_APP_URL +'/verify'
let addUserData = await fetch(
addUserUrl,
{
method: "POST",
headers: {
"Content-type": "application/json",
"x-api-key": process.env.REACT_APP_KEY
},
body: JSON.stringify(data)
}
)
if (addUserData.status !== 200) {
throw 'Error adding User'
}
let addUserDataJson = addUserData.json()
let ret = {
added: true,
}
return ret
} catch (error) {
console.log('Error')
let ret = {
added: false,
}
return ret
}
}
const onFinish = async (values: any) => {
console.log('Transaction verified');
let addStatus = await addInfo({
validNumber: "123434",
validAddress: "D74DS8JDSF",
})
if (promoStatus.added) {
setState({
...state,
showPage: false
})
} else {
setState({
...state,
showPage: true
})
}
};
return (
{!state.showPage &&
<>
<div>
<p>
DONE WITH FORM
</p>
<div>
</>
}
)
Here's what I've tried in App.test.js:
it('DONE WITH FORM APPEARS', async() =>{
// Render App
const { getByPlaceholderText, queryByText, getByText } = render(<App />);
// Entering Valid Number
const validNumberInputBox = getByText('Enter Valid Number);
fireEvent.change(validNumberInputBox, { target: { value: "123434" } });
expect(validNumberInputBox).toHaveValue("123434");
// Entering Valid Address
const validAddressInputBox = getByText('Enter Valid Address');
fireEvent.change(validAddressInputBox, { target: { value: "D74DS8JDSF" } });
expect(validAddressInputBox).toHaveValue("D74DS8JDSF");
// Button Click
userEvent.click(screen.getByRole('button', {name: /Submit/i}));
//Check if the DONE WITH FORM is shown
expect(await waitFor(() => getByText('DONE WITH FORM'))).toBeInTheDocument();
});
I've tried almost everything I could find through other stack overflow posts and web articles. so I'd really appreciate any help on how to implement this unit test.
The first step would be to mock the async function performing the POST request (addInfo). You never want to try real HTTP requests in unit tests (this won't work since Jest runs in a Node environment where fetch or XMLHttpRequest APIs are not implemented). Beside this, component/unit tests should be independent from any other system like a backend exposing APIs.
To do so, your async function should be in a separate file (so a JS module), then you could mock this module using Jest :
// api.js
export const addInfo = () => {...}
// App.test.js
import * as Api from 'api.js';
// here you can define what your mock will return for this test suite
const addInfoSpy = jest.spyOn(Api, 'addInfo').mockResolvedValue({ ret: true });
describe('...', () => {
test('...', async () => {
// perform user interactions that should trigger an API call
expect(addInfoSpy).toHaveBeenCalledWith('expected addInfos parameter');
// now you can test that your component displays "DONE WITH FORM" or whatever
// UI it should be displaying after a successful form submission
});
});
https://jestjs.io/docs/mock-function-api#mockfnmockresolvedvaluevalue
https://jestjs.io/docs/jest-object#jestspyonobject-methodname
https://jestjs.io/docs/expect#tohavebeencalledwitharg1-arg2-
I have axios post method to check username and password when they available I want to set a state as true but always get the state as false.
My code is:
const [validated, setValidated] = useState(false);
const login = () => {
Axios.post('http://124.43.17.60:4000/loginnew', {
username: email,
password: password
}).then((response) => {
// let data = response.data
console.log("my web response all", response)
console.log("my web response", response.data[0])
try {
if (response.data[0].role) {
let role = response.data[0].role
setValidated(true)
console.log(validated)
if (role == "admin") {
history.push('/admin')
} else {
history.push('/member')
}
}
} catch (err) {
alert.error("wrong credentials");
}
});
}
Can anyone tell me what um doing wrong and please help me to solve this problem
In React, modifying the state is an asynchronous action. Meaning that this piece of code won't give your expected results:
setValidated(true)
console.log(validated) // Won't be updated to true
If you want to achieve something specifically on true, you have 2 options
1. useEffect
useEffect(() => { if (validated) { ...do something... } }, [validated])
2. setState callback
setValidated((oldValidated) => {
const newValidated = !oldValidated;
console.log(newValidated) // true if oldValidated was false
// do something with newValidated === true
return newValidated; // Make sure to return the new value, otherwise state won't update at all.
})
You cannot view the "updated" state inside you login function as setValidated is an asynchronous process, which mean the code will not wait for the data to get back and still use the old value. Read this post for more explanation
If you want to view the "updated" value of validated you need to create a useEffect like so
useEffect(() => {
console.log(validated)
})
useEffect(() => {
Axios.post('http://124.43.17.60:4000/loginnew', {
username: email,
password: password
}).then((response) => {
// let data = response.data
console.log("my web response all", response)
console.log("my web response", response.data[0])
try {
if (response.data[0].role) {
let role = response.data[0].role
setValidated(true)
console.log(validated)
if (role == "admin") {
history.push('/admin')
} else {
history.push('/member')
}
}
} catch (err) {
alert.error("wrong credentials");
}
});
}, []);
I'm new to Next Js and functional comoponents. I'm trying to retrieve data from /api/retrieve2
//this is retrieve page
export default function Retrieve() {
const onSubmit = async data => {
const { user } = await axios.post("/api/retrieve2", data);
console.log(user) // user here is undefined
};
return (...);
}
//this is retrieve2, inside the API folder
export default async (req, res) => {
try {
const { data } = await axios.post(myBackendUrl, req.body);
console.log(data) //this is printing the right data - { email: 'casas#gmail.com', code: '123123' }
res.json(data);
} catch (e) {
res.json({ err: e.message || e });
}
};
What am I missing, is this something about Next? About functional components?
You should read about ES6 destructuring
You try to destructure user but the axios respons witch is a object doesnt contain the key user
For data it works because there is a data property in the response
Here are all properties that you can destructure:
{ data, status, statusText, headers, config, request }
You need to get the full URL to make http request to using getInitialProps, here Home is the name of your component
const Home = ({ENDPOINT}) => {
const onSubmit = async data => {
const { data } = await axios.post(`${ENDPOINT}/api/retrieve2`, data);
// consider changing `user` here to `data` since Axios stores response in data object
console.log(data) // should be defined
};
return (...);
}
Home.getInitialProps = ctx => {
const ENDPOINT = getEndpoint(ctx.req);
return { ENDPOINT };
};
// You should store this somewhere you can reuse it
export function getEndpoint(req) {
return !!req
? `${req.headers['x-forwarded-proto']}://${req.headers['x-forwarded-host']}`
: window.location.origin;
}
I'm using the basic Formik template to work on a Login Form.
onSubmit={(
values,
{ setSubmitting, setErrors /* setValues and other goodies */ }
) => {
props.logMeIn(values);
// LoginToSystem(values).then(
// user => {
// setSubmitting(false);
// // do whatevs...
// // props.updateUser(user)
// },
// errors => {
// setSubmitting(false);
// // Maybe transform your API's errors into the same shape as Formik's
// //setErrors(transformMyApiErrors(errors));
// console.log(errors);
// }
// );
}}
This problem is within the onSubmit section; The demo code is commented out but it uses a LoginToSystem function that seems to be a promise. I can not figure out 'what' this function is supposed to me. My function that handles this would be props.logMeIn() - Which also does not work as intended
If the login is successful, it will currently work as expected, and everything is fine. However, if the login fails (404, 401, whatever) the form will remain there, and the setSubmitting log stays there so Submit is grayed out but nothing is done.
If I try to replace LoginToSystem with my function, I get an error on the .then that I can't perform .then on undefined.
I'm wondering if perhaps this is because my function is not set up like a Promise?
loginClickHandler = (user) => {
let userObj = {
email: user.email,
password: user.password
}
axios.post('api/v1/auth/sign_in', userObj)
.then((res) => {
console.log(res.headers);
let loggedInUser = {
'access_token': res.headers['access-token'],
'client': res.headers['client'],
'uid':res.headers['uid'],
'signedIn': true
};
this.setState({
user: loggedInUser
})
this.props.retrieve(user.email);
})
.catch((err) => {
console.log(err);
return err
})
};
My function does properly catch (Thanks to axios) on the .then/.catch, but perhaps I am supposed to modify those to provide a callback so that onSubmit can properly fire?
With some guidance I was able to resolve this one simpler. Axios is natively returning a 'promise' so I just needed to ensure the outcome of the function was axios' method in the end.
loginClickHandler = (user) => {
let userObj = {
email: user.email,
password: user.password
}
const request = axios.post('api/v1/auth/sign_in', userObj);
request.then((res) => {
console.log(res.headers);
let loggedInUser = {
'access_token': res.headers['access-token'],
'client': res.headers['client'],
'uid': res.headers['uid'],
'signedIn': true
};
this.setState({user: loggedInUser, auth: true, anchorEl: null})
}).catch((err) => {
console.log(err);
// setErrors({ test: 'This was an error' })
})
return request;
};
In onSubmit there's a second argument for setting your errors. I added flow to be able to see the types better in this answer for you.
<Formik
initialValues={...}
... // Other Props
onSubmit={this.handleSubmit} // This is where you handle your login logic
render={this.renderForm} // Render your form here.
You have a callback to help you set errors in the second argument
handleSubmit = (
user: FormValues,
{ setErrors }: FormikActions<FormValues>
) => {
return axios.post('api/v1/auth/sign_in', userObj)
...
.catch(e) => {
setErrors({ username: 'Invalid Username' }) // Object you want to put here.
}
}
In your render form function you now have errors that you can use based on what you called in your setErrors
renderForm = ({
... // These are your other FormikProps you're using
errors // You want to use this
}: FormikProps<FormValues>) => (
... // Your rendering here
// errors.username
)
For flow types on Formik
https://github.com/flowtype/flow-typed/blob/master/definitions/npm/formik_v0.9.x/flow_v0.53.x-/formik_v0.9.x.js