I have a register modal which has 3 step.
Fill up the info, getActivate Code and Success Message.
I Want when user filled to inputs and clicked the submit button if there is no error move to next Step
but if there is error then React Toastify will show the message.
My problem is why try catch block in MyComponent dosen't work ?
P.S: I'm using Formik and Yup
httpService
const handleExpectedError = (response: any) => {
if (response?.status >= 400 && response?.status < 500) {
const errors = response?.data?.errors;
const errPropertyName: string[] = Object.keys(errors);
toast.error(errors?.[errPropertyName?.[0]]?.[0]);
}
};
export const handleRegister = async (user: User): Promise<void> => {
try {
await axios.post(`${config.apiEndPoint}/auth/register`, user, header);
} catch ({ response }) {
handleExpectedError(response);
}
};
MyComponent
const [step, setStep] = useState(1);
const formik = useFormik({
initialValues: {
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
password: "",
},
onSubmit: (value) => {
if (terms) {
handleNextStep(value);
}
},
validationSchema: registerSchema,
});
// Event Handler
// Value below is a referance to Formik object
const handleNextStep = async (value: any) => {
if (step === 1) {
try {
await handleRegister(value);
setStep(step + 1);
await handeGetActivateCode({ email: value.email });
} catch (error) {
setStep(1);
}
}
if (step !== 1) return setStep(step - 1);
};
In httpService file, you have used try-catch. In that catch you are trying to get the error in the curly braces, instead of doing like that if you do the following thing. then the catch block will work fine
export const handleRegister = async (user: User): Promise<void> => {
try {
await axios.post(`${config.apiEndPoint}/auth/register`, user, header);
} catch (response) {
handleExpectedError(response);
}
};
Related
I have a resolver file for my User with some mutations on it to update, delete, markInactive and banUser
async updateUser(
#Args() { id, input },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin `);
}
const user = await this.userService.update(id, input);
return {
id: user.id,
user
};
}
async deleteUser(
#Args() { id, input },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.delete(id, input);
return {
id: user.id,
user
};
}
async deleteUser(
#Args() { id, input },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.delete(id, input);
return {
id: user.id,
user
};
}
async markInactive(
#Args() { id },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.markInactive(id);
return {
id: user.id,
user
};
}
async banUser(
#Args() { id },
) {
const user = await this.userService.getById(id);
if (!user) {
return new NotFoundError('User not found');
}
const isAdminUser = this.userService.isUserAdmin(id);
if (!isAdminUser) {
return new PermissionError(`You can't update this user cause it's an admin`);
}
const user = await this.userService.banUser(id);
return {
id: user.id,
user
};
}
I always repeat the error handler (check user exist and check the user is admin) in all my resolver and now I need to add two update mutation, but I want to find a way to factorize this error checking in a common function
Do you have a solution to achieve this ?
great usecase here for refactoring. I would suggest you two approaches. Note that some parts are only guesses as I don't know your code base.
Using a repetitve approach
Since these methods are doing the same thing, you can move the logic somewhere else :
#Injectable()
export class UserService {
async getById(userId: string): Promise<User> {
// just an example
const user = { id: '1', isAdmin: false };
return Promise.resolve(user);
}
async ensuresUserExists(userId: string): Promise<void> {
const user = await this.getById(userId);
if (!user) {
throw new NotFoundError('User not found');
}
}
async ensuresUserIsNotAdmin(userId: string): Promise<void> {
const user = await this.getById(userId);
if (!user) {
throw new PermissionError("You can't update this user cause it's an admin");
}
}
}
You can simply use this in your controller methods:
async updateUser(
#Args() { id, input },
) {
await this.userService.ensuresUserExists(id);
await this.userService.ensuresUserIsNotAdmin(id);
const user = await this.userService.update(id, input);
return {
id: user.id,
user
};
}
I think most ORMs will throw an error if user does not exist anyway, I assume you can go deeper and make sures the user exists and is not admin in the same method but again I don't know what is your architecture.
Using guards
NestJs allows you to create custom guards where you can perform any operations prior to a method execution. It's IMO a cleaner approach.
guard.ts
export const ADMIN_OP = 'admin';
#Injectable()
export class UserEditGuard implements CanActivate {
constructor(private reflector: Reflector, private userService: UserService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const operationType = this.reflector.get<string>(
'operationType',
context.getHandler(),
);
if (operationType !== ADMIN_OP) return true;
const { userId } = request.body; // assuming you are sending params in POST request body
if (!userId) {
throw new UnauthorizedException();
}
await this.userService.ensuresUserExists(userId);
await this.userService.ensuresUserIsNotAdmin(userId);
return true;
}
}
Now you just need to "plug" the guard to your method.
export const UserPermissionCheck = () => SetMetadata('operationType', ADMIN_OP); // This will add the type to metadata
And then add it to your Controller (this will work with a service too)
#Controller()
#UseGuards(UserEditGuard)
export class UserController {
constructor(private readonly userService: UserService) {}
#Post()
#UserPermissionCheck()
async updateUser(#Body() { id, input }) {
const user = await this.userService.update(id, input);
return {
id: user.id,
user,
};
}
#Post()
#UserPermissionCheck()
async deleteUser(#Body() { id, input }) {
const user = await this.userService.delete(id, input);
return {
id: user.id,
user,
};
}
}
My Code looks like this:
interface MutationProps{
username: any,
Mutation: any
}
const UseCustomMutation: React.FC<MutationProps> = (MutationProps: MutationProps) => {
const [myFunc, {data, error}] = useMutation(MutationProps.Mutation);
useEffect(() => {
myFunc({variables:{username: MutationProps.username}})
console.log(JSON.stringify(data))
console.log(JSON.stringify(error, null , 2))
}, [])
return data
}
export const DisplayUser = () => {
const GET_USER = gql`
mutation GetUser($username: String!){
getUser(username: $username) {
pfp
username
password
age
CurrentLive
ismod
description
fullname
}
}
`
const {username} : {username: any} = useParams()
const MyData = UseCustomMutation(username, GET_USER)
console.log(JSON.stringify(MyData))
But I get this error back: ×
Argument of undefined passed to parser was not a valid GraphQL DocumentNode. You may need to use >'graphql-tag' or another method to convert your operation into a document
How about your code looks like this:
interface MutationProps {
username: string;
Mutation: any;
}
const UseCustomMutation: React.FC<MutationProps> = ({ username, Mutation }) => {
const [functionForDoingAction, { data, loading, error }] = useMutation(
Mutation,
{
variables: {
username,
},
}
);
useEffect(() => {
// fn trigger for change data
functionForDoingAction({
variables: {
username: "string_value",
},
});
console.log(JSON.stringify(data));
console.log(JSON.stringify(error, null, 2));
}, []);
if (loading) return "loading...";
if (error) return `Submission error! ${error.message}`;
return data;
};
export const DisplayUser = () => {
const GET_USER = gql`
mutation GetUser($username: String!) {
getUser(username: $username) {
pfp
username
password
age
CurrentLive
ismod
description
fullname
}
}
`;
const { username }: { username: string } = useParams();
const MyData = UseCustomMutation(username, GET_USER);
console.log(JSON.stringify(MyData));
};
you can pass an argument directly to the useMutation hook which they provide as an Options parameter. Or is the direct trigger function from the hook you get.
I'm running a function that uploads an image to Firebase and sets the data to its respective user:
import { Component, OnInit } from '#angular/core';
import { AngularFirestoreDocument } from '#angular/fire/firestore';
import { FbUser } from 'src/app/common/fb-user';
import { AuthService } from 'src/app/services/auth.service';
import { NgxImageCompressService } from 'ngx-image-compress';
#Component({
selector: 'app-profile-info',
templateUrl: './profile-info.component.html',
styleUrls: ['./profile-info.component.scss']
})
export class ProfileInfoComponent implements OnInit {
constructor(
public _auth: AuthService
) { }
ngOnInit(): void {
}
currentImageUrl: string = "";
async uploadPicture(e: any) {
const file = e.target.files[0]
const filePath = this._auth.userData.uid
const task = await this.uploadImage(filePath, file)
if (task) {
//Promise.resolve().then(() => window.location.reload())
this.currentImageUrl = await this._auth._afstg.ref(task).getDownloadURL().toPromise();
const userRef: AngularFirestoreDocument<any> = this._auth._afs.doc(`users/${filePath}`);
const userData: FbUser = {
uid: filePath,
email: this._auth.userData.email,
displayName: this._auth.userData.displayName,
photoURL: this.currentImageUrl,
emailVerified: this._auth.userData.emailVerified
}
return userRef.set(userData, {
merge: true
})
alert("Image uploaded succesfully")
window.location.reload()
} else {
alert("Error when uploading image, try again")
}
}
async uploadImage(uid: string, file: any): Promise<string> {
const fileRef = this._auth._afstg.ref(uid).child("profile-picture");
// Upload file in reference
if (!!file) {
const result = await fileRef.put(file);
return result.ref.fullPath;
}
return ""
}
My problem is, as you can see I have an alert and a reload() after the return function, i tried to run userRef.set without the return function, but it just doesn't work, only way it works is if it is inside the return, and now I can't refresh the page after the data is modified, any ideas why or how I could reload after the return? Already tried try-finally and Prosimes.resolve, neither worked.
I fixed it by using a try-catch and await:
currentImageUrl: string = "";
async uploadPicture(e: any) {
const file = e.target.files[0]
const filePath = this._auth.userData.uid
const task = await this.uploadImage(filePath, file)
if (task) {
//Promise.resolve().then(() => window.location.reload())
this.currentImageUrl = await this._auth._afstg.ref(task).getDownloadURL().toPromise();
const userRef: AngularFirestoreDocument<any> = this._auth._afs.doc(`users/${filePath}`);
const userData: FbUser = {
uid: filePath,
email: this._auth.userData.email,
displayName: this._auth.userData.displayName,
photoURL: this.currentImageUrl,
emailVerified: this._auth.userData.emailVerified
}
try {
await userRef.set(userData, {
merge: true
})
} catch (error) {
console.error("Error modifying user document", error);
}
alert("Image uploaded succesfully")
window.location.reload()
} else {
alert("Error when uploading image, try again")
}
}
async uploadImage(uid: string, file: any): Promise<string> {
const fileRef = this._auth._afstg.ref(uid).child("profile-picture");
// Upload file in reference
if (!!file) {
const result = await fileRef.put(file);
return result.ref.fullPath;
}
return ""
}
The following code (with some parts of it cut out for the sake of brevity) is working:
function AddressInputList({
isOpen,
inputValue,
highlightedIndex,
getItemProps,
getMenuProps
}: AutocompleteInputListProps) {
const [items, setItems] = useState<MarkerPoint[]>([])
const api = 'XXXX'
const fetchURL = `https://api.opencagedata.com/geocode/v1/json?key=${api}&q=${inputValue}&limit=5&pretty=1`
useEffect(() => {
async function fetchData() {
if (inputValue !== null && inputValue.length > 1) {
try {
const request = await axios.get(fetchURL)
const items = request.data.results.map((res: any) => {
return {
lat: res.geometry.lat,
lng: res.geometry.lng,
address: res.formatted
}
})
setItems(items)
} catch (error) {
console.error(error)
}
}
}
fetchData()
}, [inputValue])
return (/*Code cut out*/)
}
What I now would like to do is to refactor the code to make it more lean. So I will create a utility.ts-file in which I have the fetchData-function and I subsequently would like to import the fetchData-function into the initial AddressInputList-function:
utility.ts:
export async function fetchData(inputValue: string, fetchURL: string) {
if (inputValue !== null && inputValue.length > 1) {
try {
const request = await axios.get(fetchURL)
const items = request.data.results.map((res: any) => {
return {
lat: res.geometry.lat,
lng: res.geometry.lng,
address: res.formatted
}
})
setItems(items)
} catch (error) {
console.error(error)
}
}
}
Now my problem here is that I don't know how to make the useState-hook setItems available in utility.ts. I read somewhere that this could be done with props but I'm not sure how this would look like. A short example would be highly appreciated!
Just create a custom hook that would fetch data for you.
I wouldn't recommend to tie this hook to inputValue so much. Also that .map formatting does not feel universal too.
export function useFetchData(inputValue: string, fetchURL: string) {
const [items,setItems] = useState([]);
useEffect(() => {
async function fetchData() {
if (inputValue !== null && inputValue.length > 1) {
try {
const request = await axios.get(fetchURL)
const items = request.data.results.map((res: any) => {
return {
lat: res.geometry.lat,
lng: res.geometry.lng,
address: res.formatted
}
})
setItems(items)
} catch (error) {
console.error(error)
}
}
}
}, [inputValue]);
return items;
}
After that you can use this custom hook like so
const items = useFetchData(inputValue, "/api/<endpoint>);
I guess you could just pass setItems as a callback function, as a parameter to your fetchData function.
fetchData(inputValue: string, fetchURL: string, setItems) {
...
}
I have a reducer that is intended for handling notification banners.
const notifReducer = (state = { notifMessage: null, notifType: null, timeoutID: null },
action
) => {
switch (action.type) {
case 'SET_NOTIFICATION':
if (state.timeoutID) {
clearTimeout(state.timeoutID)
}
return {
notifMessage: action.notifMessage,
notifType: action.notifType,
timeoutID: null
}
case 'REMOVE_NOTIFICATION':
return {
notifMessage: null,
notifType: null,
timeoutID: null
}
case 'REFRESH_TIMEOUT':
return {
...state,
timeoutID: action.timeoutID
}
default:
return state
}
}
export const setNotification = (notifMessage, notifType) => {
return async dispatch => {
dispatch({
type: 'SET_NOTIFICATION',
notifMessage,
notifType
})
let timeoutID = await setTimeout(() => {
dispatch({
type: 'REMOVE_NOTIFICATION'
})
}, 5000)
dispatch({
type: 'REFRESH_TIMEOUT',
timeoutID
})
}
}
export default notifReducer
It works fully fine in the rest of my app, except in this one event handler that uses a try-catch. If I intentionally trigger the catch statement (by logging in with a bad username/password), I get "Unhandle Reject (Error): Actions must be plain objects. Use custom middleware for async action", but I am already using redux-thunk middleware!
const dispatch = useDispatch()
const handleLogin = async (event) => {
event.preventDefault()
try {
const user = await loginService.login({
username, password
})
//
} catch (exception) {
dispatch(setNotification(
'wrong username or password',
'error')
)
}
}
edit:
here is my store.js contents
const reducers = combineReducers({
blogs: blogReducer,
user: userReducer,
notification: notifReducer,
})
const store = createStore(
reducers,
composeWithDevTools(
applyMiddleware(thunk)
)
)
I hope your question is answered in a post already. Please check the below link
Error handling redux-promise-middleware