I'm sending a PDF upload as part of a payload to an in-house tool that is separate from my Next.js application through an API route. I'm having trouble getting the FileList to upload correctly. It's part of a form built with react-hook-form, I'm appending the pdf to formData, and it's being correctly converted to a binary that submits to the API route. However, the API hangs and doesn't do anything. This is what my component looks like:
OnboardingDetails.tsx
import { ChangeEvent } from 'react';
import { useRouter } from 'next/router';
import { Button, Input, Spinner } from '#vero/ui';
import { useOnboardingStep } from 'client-data/hooks/use-onboarding-step';
import { inferMutationInpu } from 'client-data/utils/trpc';
import { env } from 'env/client.mjs';
import { OnboardingPageProps } from 'pages/onboarding';
import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
type InputType = inferMutationInput<'users.onboarding-details'>;
export const OnboardingDetails = ({
user,
}: {
user: OnboardingPageProps['user'];
}) => {
const router = useRouter();
useOnboardingStep();
const {
handleSubmit,
register,
watch,
control,
formState: { errors },
} = useForm<InputType>({
defaultValues: {
resume: null,
},
});
const onSubmit = async (values: InputType) => {
if (!values.resume) {
toast.error(`Resume is required.`);
return;
}
const formData = new FormData();
formData.append('resume', values.resume);
await fetch(`/api/upload-user`, {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
return (
<>
<h3 className="m-0 p-0 text-3xl">What are your contact details?</h3>
<form
className="flex w-full flex-col gap-6"
onSubmit={handleSubmit(onSubmit)}
>
<Controller
name="resume"
control={control}
defaultValue={null}
render={({ field }) => (
<Input
showLabel
label="Resume"
type="file"
onChange={(e) => {
field.onChange(e.target.files);
}}
/>
)}
/>
<Button
type="submit"
className="btn-primary"
disabled={isLoading}
icon={isLoading && <Spinner />}
>
Next
</Button>
</form>
</>
);
};
Like I said, when I submit the form I'm able to see the pdf that I upload converted to a binary, but I don't receive a response; this is what my API route looks like:
upload-user.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { authOptions } from './auth/[...nextauth]';
import { getServerSession } from '#vero/lib/auth';
import { prisma } from '#vero/prisma';
import { uploadUser } from '#vero/rater';
import { env } from 'env/server.mjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const body = req.body;
const session = await getServerSession({ req, res }, authOptions);
try {
if (req.method !== 'POST') {
res.status(405).json({ error: 'Method Not Allowed' });
return;
}
if (!session) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
id: true,
name: true,
email: true,
recruit: {
select: {
github_username: true,
},
},
},
});
if (!user) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
await uploadUser({
apiKey: env.RATER_API_KEY,
baseUrl: env.RATER_API,
userEmail: user.email as string,
userName: user.name as string,
usernameList: user?.recruit?.github_username as string,
userId: user.id,
runAll: true,
resume: body.resume[0],
});
res.status(200).json({ success: true });
} catch (error) {
res.status(500).json({ success: false, error });
}
};
export default handler;
And then, my uploadUser function:
import axios from 'axios';
import FormData from 'form-data';
import fs from 'fs';
import { z } from 'zod';
const UploadUserInput = z.object({
apiKey: z.string(),
baseUrl: z.string().url(),
usernameList: z.string(),
userEmail: z.string(),
userName: z.string(),
userId: z.string(),
resume: z.any().optional(),
runAll: z.boolean(),
});
const GetUsernameFromIdInput = z.object({
accountId: z.string(),
});
export const getUsernameFromId = async (args: { accountId: string }) => {
const { accountId } = GetUsernameFromIdInput.parse(args);
const res = await fetch(`https://api.github.com/user/${accountId}`);
const userData = await res.json();
return userData.login;
};
export const uploadUser = async (args: {
apiKey: string;
baseUrl: string;
usernameList: string;
userEmail: string;
userName: string;
runAll: boolean;
userId: string;
resume?: File;
}) => {
const {
apiKey,
baseUrl,
usernameList,
userEmail,
userName,
runAll,
userId,
resume,
} = UploadUserInput.parse(args);
const data = new FormData();
data.append('username_list', usernameList);
data.append('user_email', userEmail);
data.append('user_name', userName);
data.append('school_user_id', userId);
data.append('resume', resume);
const config = {
method: 'post',
url: `${baseUrl}/api/upload-user/?runAllScrapers=${runAll}`,
headers: {
Authorization: apiKey,
},
data: data,
};
return await axios(config);
};
To clarify, I need to be able to submit the PDF upload from the client to the server, then send the data to our external tool. I'm receiving the binary from the client, and am even able to log it in the req.body, but I can't do anything with it beyond that. It just makes the API route hang up.
Related
I have a user model which has a phone_number field. In the User schema, phone_number is defined as type string. Yet, when saving the user's details, the phone_number string gets any leading zeros removed and it gets converted to a number.
Can't find the reason why.
I am using next-auth with a mongodb adapter to initially create the user on the DB, so at first there is no phone_number field for the user. Then, I have the following component for editing the user's details:
import React, { useState } from "react";
import { User } from "../../types";
import CustomInput from "../shared/CustomInput";
function EditProfile({ user }: { user: User }) {
const [updatedUser, setUpdatedUser] = useState<User>(user);
async function saveChanges() {
if (!updatedUser) return;
// This logs it to be a string
console.log(
"typeof updatedUser.phone_number",
typeof updatedUser.phone_number
);
try {
let response = await fetch("http://localhost:3000/api/users", {
method: "PATCH",
body: JSON.stringify(updatedUser),
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
},
});
response = await response.json();
} catch (error) {
console.log("An error occurred while deleting ", error);
}
}
if (!updatedUser) return <></>;
return (
<section className="section-width py-20">
<h1 className="text-7xl font-fancy">Hello {updatedUser.name}</h1>
<div className="grid grid-cols-2 mt-20">
<div className="flex flex-col space-y-5">
<h2 className="font-extrabold text-lg text-gray-400">Personal Info</h2>
<CustomInput
type="text"
label="Full Name"
placeholder="Full Name"
value={updatedUser.name}
onChange={(name) => setUpdatedUser({ ...updatedUser, name })}
/>
<CustomInput
type="text"
label="Phone Number"
placeholder="Phone Number"
value={updatedUser.phone_number || ""}
onChange={(phone_number) => {
var pattern = /^[0-9]+$|^$/;
const isNumber = pattern.test(phone_number);
if (isNumber) setUpdatedUser({ ...updatedUser, phone_number });
}}
/>
<button className="button" onClick={saveChanges}>
Save details
</button>
</div>
</div>
</section>
);
}
export default EditProfile;
Then I have these as my api/users API routes:
import type { NextApiRequest, NextApiResponse } from "next";
import dbConnect from "../../utils/dbconnect";
import User from "../../models/user";
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") {
const data = req.body;
const { email } = data;
await dbConnect();
const user = await User.findOne({ email });
res.status(201).json({ user });
}
if (req.method === "PATCH") {
const data = req.body;
const { _id, name, phone_number } = data;
// This comes of as type of string
console.log("typeof phone_number", typeof phone_number);
// This prints it normally with the zero
console.log("phone_number", phone_number);
await dbConnect();
const user = await User.findByIdAndUpdate(_id, {
name,
phone_number,
});
// Here phone_number is no longer a string and has no leading zero.
console.log("user", user);
res.status(201).json({ user });
}
}
export default handler;
This is the User model:
import { Schema, model, models } from "mongoose";
export const userSchema = new Schema(
{
name: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true,
},
phone_number: {
type: String,
required: false,
},
avatar: {
type: String,
required: false,
},
},
{
timestamps: true,
}
);
const User = models.User || model("User", userSchema);
export default User;
I migrated and added Next.js to my React app. I getting the following error when I try to login. When I checked seems that I have to use promise.all. I tried different solutions without success. I want to know how it works. Your help and advice are highly appreciated.
error message;
Unhandled Runtime Error
Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
src/action/auth.js;
import axios from 'axios';
import { setAlert } from './alert';
// import { API_URL } from '../config/index';
import {
LOGIN_SUCCESS,
LOGIN_FAIL,
SIGNUP_SUCCESS,
SIGNUP_FAIL,
ACTIVATION_SUCCESS,
ACTIVATION_FAIL,
USER_LOADED_SUCCESS,
USER_LOADED_FAIL,
AUTHENTICATED_SUCCESS,
AUTHENTICATED_FAIL,
PASSWORD_RESET_SUCCESS,
PASSWORD_RESET_FAIL,
PASSWORD_RESET_CONFIRM_SUCCESS,
PASSWORD_RESET_CONFIRM_FAIL,
LOGOUT
} from './types';
export const checkAuthenticated = () => async dispatch => {
if (typeof window !== 'undefined' ? window.localStorage.getItem('access') : false) {
const config = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
const body = JSON.stringify({ token: typeof window !== 'undefined' ? window.localStorage.getItem('access') : false });
try {
const res = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/jwt/verify/`, body, config)
if (res.data.code !== 'token_not_valid') {
dispatch({
type: AUTHENTICATED_SUCCESS
});
} else {
dispatch({
type: AUTHENTICATED_FAIL
});
}
} catch (err) {
dispatch({
type: AUTHENTICATED_FAIL
});
}
} else {
dispatch({
type: AUTHENTICATED_FAIL
});
}
};
export const load_user = () => async dispatch => {
if (typeof window !== 'undefined' ? window.localStorage.getItem('access') : false) {
const config = {
headers: {
'Content-Type': 'application/json',
'Authorization': `JWT ${typeof window !== 'undefined' ? window.localStorage.getItem('access') : false}`,
'Accept': 'application/json'
}
};
try {
const res = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/auth/users/me/`, config);
dispatch({
type: USER_LOADED_SUCCESS,
payload: res.data
});
}catch (err) {
dispatch({
type: USER_LOADED_FAIL
});
}
} else {
dispatch({
type: USER_LOADED_FAIL
});
}
};
export const login = (email, password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/jwt/create/`, body, config);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
});
dispatch(setAlert('Authenticated successfully', 'success'));
dispatch(load_user());
}catch (err) {
dispatch({
type: LOGIN_FAIL
});
dispatch(setAlert('Error Authenticating', 'error'));
}
};
export const signup = (name, email, password, re_password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ name, email, password, re_password });
try {
const res = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/users/`, body, config);
dispatch({
type: SIGNUP_SUCCESS,
payload: res.data
});
dispatch(setAlert('Check Your Email to Activate Your Account.', 'warning'));
} catch (err) {
dispatch({
type: SIGNUP_FAIL
})
}
};
export const verify = (uid, token) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ uid, token });
try {
await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/users/activation/`, body, config);
dispatch({
type: ACTIVATION_SUCCESS,
});
dispatch(setAlert('Account Activated Successfully.', 'success'));
} catch (err) {
dispatch({
type: ACTIVATION_FAIL
})
}
};
//Reset Password
export const reset_password = (email) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email });
try {
await axios.post (`${process.env.NEXT_PUBLIC_API_URL}/auth/users/reset_password/`, body, config);
dispatch({
type: PASSWORD_RESET_SUCCESS
});
dispatch(setAlert('Check Your Email to Rest Password.', 'warning'));
} catch (err) {
dispatch({
type: PASSWORD_RESET_FAIL
});
}
};
// Reset Password Confirm
export const reset_password_confirm = (uid, token, new_password, re_new_password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ uid, token, new_password, re_new_password });
try {
await axios.post (`${process.env.NEXT_PUBLIC_API_URL}/auth/users/reset_password_confirm/`, body, config);
dispatch(setAlert('Password Rest Successful.', 'success'));
dispatch({
type: PASSWORD_RESET_CONFIRM_SUCCESS
});
} catch (err) {
dispatch({
type: PASSWORD_RESET_CONFIRM_FAIL
});
}
};
//Logout
export const logout = () => dispatch => {
dispatch(setAlert('Logout successful.', 'success'));
dispatch({
type: LOGOUT
});
};
src/pages/login.js;
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { connect } from 'react-redux';
import { Button } from '#mui/material';
import { login } from '../actions/auth';
import styles from '../styles/Login.module.css';
import Head from 'next/head';
import WelcomePageFooter from '../components/WelcomePageFooter';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import i18n from '../../i18n';
function Login({ login, isAuthenticated }) {
const { t } = useTranslation();
const navigate = useRouter();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const { email, password } = formData;
const onChange = e => setFormData({ ...formData, [e.target.name]: e.target.value});
const onSubmit = e => {
e.preventDefault();
login (email, password)
};
if (isAuthenticated) {
return (
navigate.replace('/')
);
}
return (
<main>
<div className={styles.login}>
<Head>
<title>Diploman - Login</title>
<meta
name='description'
content='login page'
/>
</Head>
<h1 className={styles.login__title}>{t('login_title')}</h1>
<p className={styles.login__lead}>{t('login_lead')}</p>
<form className={styles.login__form} onSubmit={e => onSubmit(e)}>
<div className={styles.login__form__group}>
<input
className={styles.login__form__input}
type='email'
placeholder={t('Form_email')}
name='email'
value={email}
onChange={e => onChange(e)}
required
/>
</div>
<div className={styles.login__form__group}>
<input
className={styles.login__form__input}
type='password'
placeholder={t('Form_pw')}
name='password'
value={password}
onChange={e => onChange(e)}
minLength='8'
required
/>
</div>
<Button className={styles.login__button__main} type='submit'>{t('login_title')}</Button>
</form>
<p className={styles.link__to__Signup}>
{t('login_text1')} <Link href='/signup' className={styles.login__link}>{t('login_register')}</Link>
</p>
<p className={styles.link__to__resetPassword}>
{t('login_text2')} <Link href='/reset-password' className={styles.reset__password__link}>{t('login_reset')}</Link>
</p>
</div>
<WelcomePageFooter/>
</main>
)
};
export const getServerSideProps = async ({ locale }) => (
{ props: {
...(await serverSideTranslations(
locale,
['common'],
i18n,
)),
} }
);
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect (mapStateToProps, { login }) (Login);
I really appreciate your help here
I am still relatively new to node.js and I am wondering what the best way to call my API's GET method from within this next.js component. So far I have my DemoForm.js component here:
import { useState } from 'react'
import { useRouter } from 'next/router'
import { mutate } from 'swr'
const DemoForm = ({ req, formId, demoForm, forNewDemo = true }) => {
const router = useRouter()
const contentType = 'application/json'
const [errors, setErrors] = useState({})
const [message, setMessage] = useState('')
const [demoName, setDemoName] = useState(demoForm.demoName || 'demo name')
const [response, setResponse] = useState (null)
const makeRequest = async () => {
const res = await fetch('api/demos')
setResponse({
status: res.status,
body: await res.json(),
limit: res.headers.get('X-RateLimit-Limit'),
remaining: res.headers.get('X-RateLimit-Remaining'),
})
}
const [form, setForm] = useState({
demo_token: demoForm.demo_token,
demo_name: demoForm.demo_name,
})
/* The PUT method edits an existing entry in the mongodb database. */
const putData = async (form) => {
const { id } = router.query
try {
const res = await fetch(`/api/demos/${id}`, {
method: 'PUT',
headers: {
Accept: contentType,
'Content-Type': contentType,
},
body: JSON.stringify(form),
})
// Throw error with status code in case Fetch API req failed
if (!res.ok) {
throw new Error(res.status)
}
const { data } = await res.json()
mutate(`/api/demos/${id}`, data, false) // Update the local data without a revalidation
router.push('/')
} catch (error) {
setMessage('Failed to update')
}
}
/* The POST method adds a new entry in the mongodb database. */
const postData = async (form) => {
try {
const res = await fetch('/api/demos', {
method: 'POST',
headers: {
Accept: contentType,
'Content-Type': contentType,
},
body: JSON.stringify(form),
})
// Throw error with status code in case Fetch API req failed
if (!res.ok) {
throw new Error(res.status)
}
router.push('/')
} catch (error) {
setMessage('Failed to add new function')
}
}
const handleChange = (e) => {
const target = e.target
const value =
target.name === 'demo_token' ? target.checked : target.value
const name = target.name
setForm({
...form,
[name]: value,
})
}
/* Makes sure demo info is filled for demo name, owner name, species, and image url*/
const formValidate = () => {
let err = {}
// if (!form.demo_token) err.demo_token = 'Checkbox selection required'
if (!form.demo_name) err.demo_name = 'Name is required'
return err
}
const handleSubmit = (e) => {
e.preventDefault()
const errs = formValidate()
if (Object.keys(errs).length === 0) {
forNewDemo ? postData(form) : putData(form)
} else {
setErrors({ errs })
}
}
return (
<>
<form id={formId} onSubmit={handleSubmit}>
<span className="flex flex-row justify-center">
<label htmlFor="demo_name">Name</label>
<input
name="demo_name"
value={form.demo_name}
onChange={handleChange}
/>
<br />
</span>
<span className="flex flex-row justify-center">
<label htmlFor="demo_token">Pro?</label>
<input
type="checkbox"
name="demo_token"
checked={form.demo_token}
onChange={handleChange}
/>
<br />
</span>
<span className="flex flex-row justify-center">
<button type="submit" className=" bg-gray-800 btn-sm text-teal-200 hover:text-teal-300 flex-row justify-start pl-4" onClick={() => makeRequest()}>Make Request</button>
{/* {response && (
<code>
<pre>{JSON.stringify(response, null, 2)}</pre>
</code>
)} */}
</span>
<p>{message}</p>
<div>
{Object.keys(errors).map((err, index) => (
<li key={index}>{err}</li>
))}
</div>
</form>
</>
)
}
export default DemoForm
and here is the API
pages/api/demos/[id].js
import dbConnect from '../../../lib/dbConnect'
import Demo from '../../../models/Demo'
export default async function handler(req, res) {
const {
query: { id },
method,
} = req
await dbConnect()
switch (method) {
case 'GET' /* Get a model by its ID */:
try {
const demo = await Demo.findById(id)
if (!demo) {
return res.status(400).json({ success: false })
}
res.status(200).json({ success: true, data: demo })
} catch (error) {
res.status(400).json({ success: false })
}
break
case 'PUT' /* Edit a model by its ID */:
try {
const demo = await Demo.findByIdAndUpdate(id, req.body, {
new: true,
runValidators: true,
})
if (!demo) {
return res.status(400).json({ success: false })
}
res.status(200).json({ success: true, data: demo })
} catch (error) {
res.status(400).json({ success: false })
}
break
case 'DELETE' /* Delete a model by its ID */:
try {
const deletedDemo = await Demo.deleteOne({ _id: id })
if (!deletedDemo) {
return res.status(400).json({ success: false })
}
res.status(200).json({ success: true, data: {} })
} catch (error) {
res.status(400).json({ success: false })
}
break
default:
res.status(400).json({ success: false })
break
}
}
pages/api/demos/index.js
import dbConnect from '../../../lib/dbConnect'
import Demo from '../../../models/Demo'
import fs from 'fs'
const shell = require('shelljs');
export default async function handler(req, res) {
const {
method,
body,
} = req
await dbConnect()
switch (method) {
case 'GET':
try {
const demos = await Demo.find({}) /* find all the data in our database */
res.status(200).json({ success: true, data: demos })
} catch (error) {
res.status(400).json({ success: false })
}
break
case 'POST':
try {
const demo = await Demo .create(
req.body
) /* create a new model in the database */
res.status(201).json({ success: true, data: demo })
} catch (error) {
res.status(400).json({ success: false })
}
break
default:
res.status(400).json({ success: false })
break
}
}
The form is setup to work with my API's post and put methods, however, I am needing to call the GET method to essentially map out all of the demos by there ${id} so they can be rendered on screen. I am having a tough time doing this from within my Next.js component. ANY help would be greatly appreciated!
Thanks y'all...
I have the following code but it renders the cookieData undefined on the first render and query, so the query doesn't get the cookie and it fails authetication. Any way to make the query wait for the call to the cookie api to come back before running.
const { data: cookieData, error: cookieError } = useSWR(
"/api/cookie",
fetcher
);
console.log(cookieData);
var test = `Bearer ${cookieData}`;
const { loading, error, data } = useQuery(FORMS, {
context: {
headers: {
authorization: test,
},
},
});
UPDATE: I ended up using lazy query for the above, but I will try skip as well, but I have been trying to implement skip on this mutation now and it says the id is undefined, it consoles on the page but is undfined first a few times.
const addFormClicked = async (data) => {
//console.log(data);
const res = await createForm({
variables: {
name: data.name,
user: user.id,
},
skip: !user.id,
});
console.log(res);
Router.push(`/formBuild/${res.data.createForm._id}`);
};
Here's the whole code for context
import { useMutation, gql } from "#apollo/client";
import Layout from "../components/Layout";
import { useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { FORMS } from "../components/Layout";
import useSWR from "swr";
import { useState } from "react";
const ADD_FORM = gql`
mutation AddForm($name: String!, $id: ID!) {
createForm(data: { name: $name, user: { connect: $id } }) {
name
_id
}
}
`;
const fetcher = (url) => fetch(url).then((r) => r.json());
export default function AddForm() {
const { data: user } = useSWR("/api/user"); // add
const { data: cookieData, error: cookieError } = useSWR(
"/api/cookie",
fetcher
);
var test = `Bearer ${cookieData}`;
const Router = useRouter();
const [
createForm,
{
data: createFormData,
error: createFormError,
loading: createFormLoading,
},
] = useMutation(ADD_FORM, {
refetchQueries: [{ query: FORMS }],
context: {
headers: {
authorization: test,
},
},
});
const addFormClicked = async (data) => {
//console.log(data);
const res = await createForm({
variables: {
name: data.name,
user: user.id,
},
skip: !user.id,
});
console.log(res);
Router.push(`/formBuild/${res.data.createForm._id}`);
};
const { register, handleSubmit, errors, reset } = useForm();
if (createFormLoading) return <p>Loading</p>;
if (createFormError) return <p>Error: {createFormError.message}</p>;
//console.log(createFormData);
return (
<Layout>
<form onSubmit={handleSubmit(addFormClicked)}>
<h1>Form Name</h1>
<input type="text" name="name" ref={register()} />
<button type="submit">Add Form</button>
</form>
</Layout>
);
}
UPDATE: The user needed to be id, seen below
const addFormClicked = async (data) => {
//console.log(data);
const res = await createForm({
variables: {
name: data.name,
id: user.id, //NOT user:user.id BUT id:user.id
},
skip: !user.id,
});
console.log(res);
Router.push(`/formBuild/${res.data.createForm._id}`);
};
The user variable will be undefined while the query is in a loading state. Same with cookieData. There's no skip option available in useMutation since it does not automatically execute the mutation when the component renders.
A simple solution would be to render the form if only if user and cookieData exist. This way, you can know for sure the user id and token will be available when the form gets submitted.
// Add `userError` to use in combination with `user` to check if the query is loading
const { data: user, error: userError } = useSWR('/api/user', userFetcher)
const [
createForm,
{ data: createFormData, error: createFormError, loading: createFormLoading },
] = useMutation(ADD_FORM, {
refetchQueries: [{ query: FORMS }],
})
const addFormClicked = async (data) => {
const res = await createForm({
context: {
headers: {
authorization: `Bearer ${cookieData}`,
},
},
variables: {
name: data.name,
user: user.id,
},
})
Router.push(`/formBuild/${res.data.createForm._id}`)
}
if (userError || cookieError) {
return <div>Something went wrong</div>
}
if (!user || !cookieData) {
return <div>Loading...</div>
}
// Render form
I'm struggling to debug a problem and I'd appreciate any help the community might be able to offer. I'm building my first React app and have built a working Login feature, but after every successful login the user is forced to hard refresh his/her browser in order see the app in a "logged in" state. There is no error logged to the browser console, but our DevTools monitor shows the following error:
"TypeError: Cannot read property 'setState' of undefined"
What's funny is that the login authentication first succeeds, and then immediately seems to try again and fails. After clicking "login," the user must hard refresh the web page in order to make it appear that the login has worked.
I'm stumped. Can anyone see anything wrong with my code? Thank you very much in advance for taking a look!
Here's our LoginPage jsx file that contains the actual login web form:
import React from 'react';
import { Button, Form, FormGroup, Label, Input } from 'reactstrap';
export default class LoginPage extends React.Component {
constructor(props) {
super(props);
//bound functions
this.compileFormData = this.compileFormData.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
//component state
this.state = {
email: '',
password: '',
};
}
//update state as email value changes
handleEmailChange(e) {
this.setState({ email: e.target.value });
}
//update state as password value changes
handlePasswordChange(e) {
this.setState({ password: e.target.value });
}
compileFormData() {
const { loginFunction } = this.props;
const formData = this.state;
loginFunction(formData);
}
render() {
return (
<div className="row justify-content-center">
<div className="col-10 col-sm-7 col-md-5 col-lg-4">
<Form>
<FormGroup>
<Label for="exampleEmail">Email</Label>
<Input
type="email"
name="email"
id="userEmail"
placeholder="test#mccre.com"
value={this.state.email}
onChange={this.handleEmailChange}
/>
</FormGroup>
<FormGroup>
<Label for="examplePassword">Password</Label>
<Input
type="password"
name="password"
id="userPassword"
placeholder="password"
value={this.state.password}
onChange={this.handlePasswordChange}
/>
</FormGroup>
<Button onClick={this.compileFormData}>Log In</Button>
</Form>
</div>
</div>
);
}
}
Here's our login page Container that renders the login page:
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { logUserIn } from '../../actions/authentication';
import LoginPage from './LoginPage';
export class LoginPageContainer extends React.Component {
constructor(props) {
super(props);
//bound functions
this.logUserInFunction = this.logUserInFunction.bind(this);
}
logUserInFunction(userData) {
const { dispatch } = this.props;
dispatch(logUserIn(userData));
}
render() {
const { authentication } = this.props;
if (authentication.isLoggedIn) {
return (
<Redirect to="/" />
);
}
return (
<div>
<LoginPage loginFunction={this.logUserInFunction} />
</div>
);
}
}
function mapStateToProps(state) {
return {
authentication: state.authentication,
};
}
export default connect(mapStateToProps)(LoginPageContainer);
Here's our API endpoint in which we actually make the database query:
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const User = require('../../models/user.js');
const router = express.Router();
//configure mongoose promises
mongoose.Promise = global.Promise;
//POST to /register
router.post('/register', (req, res) => {
//Create a user object to save, using values from incoming JSON
const newUser = new User({
username: req.body.username,
firstName: req.body.firstName,
lastname: req.body.lastName,
email: req.body.email,
});
//Save, via passport's "register" method, the user
User.register(newUser, req.body.password, (err, user) => {
//If there's a problem, send back a JSON object with the error
if (err) {
return res.send(JSON.stringify({ error: err }));
}
// Otherwise, for now, send back a JSON object with the new user's info
return res.send(JSON.stringify(user));
});
});
//POST to /login
router.post('/login', async (req, res) => {
//look up user by their email
const query = User.findOne({ email: req.body.email });
const foundUser = await query.exec();
//If they exist, they'll have a username, so add that to our body
if (foundUser) {
req.body.username = foundUser.username;
}
passport.authenticate('local') (req, res, () => {
//If logged in, we should have use info to send back
if (req.user) {
return res.send(JSON.stringify(req.user));
}
//Otherwise return an error
return res.send(JSON.stringify({ error: 'There was an error logging in' }));
});
});
//GET to /checksession
router.get('/checksession', (req, res) => {
if (req.user) {
return res.send(JSON.stringify(req.user));
}
return res.send(JSON.stringify({}));
});
//GET to /logout
router.get('/logout', (req, res) => {
req.logout();
return res.send(JSON.stringify(req.user));
});
module.exports = router;
Here's the action file in which we define the logUserIn() function:
import { decrementProgress, incrementProgress } from './progress';
import 'whatwg-fetch';
//Action Creators
export const loginAttempt = () => ({ type: 'AUTHENTICATION_LOGIN_ATTEMPT' });
export const loginFailure = error => ({ type: 'AUTHENTICATION_LOGIN_FAILURE', error });
export const loginSuccess = json => ({ type: 'AUTHENTICATION_LOGIN_SUCCESS', json });
export const logoutFailure = error => ({ type: 'AUTHENTICATION_LOGOUT_FAILURE', error });
export const logoutSuccess = () => ({ type: 'AUTHENTICATION_LOGOUT_SUCCESS' });
export const sessionCheckFailure = () => ({ type: 'AUTHENTICATION_SESSION_CHECK_FAILURE'});
export const sessionCheckSuccess = json => ({ type: 'AUTHENTICATION_SESSION_CHECK_SUCCESS', json });
//Check User Session
export function checkSession() {
return async (dispatch) => {
//contact the API
await fetch(
//where to contact
'/api/authentication/checksession',
//what to send
{
method: 'GET',
credentials: 'same-origin',
},
)
.then((response) => {
if (response.status === 200) {
return response.json();
}
return null;
})
.then((json) => {
if (json.username) {
return dispatch(sessionCheckSuccess(json));
}
return dispatch(sessionCheckFailure());
})
.catch((error) => dispatch(sessionCheckFailure(error)));
};
}
//Log user in
export function logUserIn(userData) {
return async (dispatch) => {
//turn on spinner
dispatch(incrementProgress());
//register that a login attempt is being made
dispatch(loginAttempt());
//contact login API
await fetch(
//where to contact
'http://localhost:3000/api/authentication/login',
//what to send
{
method: 'POST',
body: JSON.stringify(userData),
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
},
).then((response) => {
if (response.status === 200) {
return response.json();
}
return null;
})
.then((json) => {
if (json) {
dispatch(loginSuccess(json));
this.setState({ redirect: true });
} else {
dispatch(loginFailure(new Error('Authentication Failed')));
}
}).catch((error) => {
dispatch(loginFailure(new Error(error)));
});
//turn off spinner
dispatch(decrementProgress());
};
}
//Log user out
export function logUserOut() {
return async (dispatch) => {
//turn on spinner
dispatch(incrementProgress());
//contact the API
await fetch(
//where to contact
'/api/authentication/logout',
//what to send
{
method: 'GET',
credentials: 'same-origin',
},
)
.then((response) => {
if (response.status === 200) {
dispatch(logoutSuccess());
} else {
dispatch(logoutFailure(`Error: ${response.status}`));
}
})
.catch((error) => {
dispatch(logoutFailure(error));
});
//turn off spinner
return dispatch(decrementProgress());;
};
}
Finally, here's the reducer file that is supposed to update the application's state depending on authentication success / failure:
const initialState = {
firstName: '',
id: '',
isLoggedIn: false,
isLoggingIn: false,
lastName: '',
username: '',
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'AUTHENTICATION_LOGIN_ATTEMPT': {
const newState = Object.assign({}, state);
newState.isLoggingIn = true;
return newState;
}
case 'AUTHENTICATION_LOGIN_FAILURE':
case 'AUTHENTICATION_SESSION_CHECK_FAILURE':
case 'AUTHENTICATION_LOGOUT_SUCCESS': {
const newState = Object.assign({}, initialState);
return newState;
}
case 'AUTHENTICATION_LOGIN_SUCCESS':
case 'AUTHENTICATION_SESSION_CHECK_SUCCESS': {
const newState = Object.assign({}, state);
newState.firstName = action.json.firstName;
newState.id = action.json._id;
newState.isLoggedIn = true;
newState.isLoggingIn = false;
newState.lastName = action.json.lastName;
newState.username = action.json.username;
return newState;
}
case 'AUTHENTICATION_LOGOUT_FAILURE': {
//todo: hanle error
return state;
}
default: {
return state;
}
}
}
Found the solution: "the this.setState({ redirect: true });" line needed to be removed from the action file.