Hi im a programming student and im trying to build a login/register page. and ive written a custom hook to get data of the user/ post data to login or register.
and my problem is that react only accepts custom hooks inside react components or other custom hooks and i want to post the login/register on button click.
whats the best why to use custom hooks onClick?
Custom useFetch hook
import React, { useState, useEffect } from "react";
import axios from "axios";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
const useFetch = () => {
const [data, setData] = useState([]);
const history = useHistory();
let initialBizCardArray = [];
useEffect(() => {
(async () => {
try {
let currLocation = history.location.pathname;
switch (currLocation) {
case (currLocation = "/my-cards"):
case (currLocation = "/cards"):
{
let { data } = await axios.get(`/cards${currLocation}`);
initialBizCardArray = data;
setData(initialBizCardArray);
}
break;
case (currLocation = "/login"):
case (currLocation = "/register"):
{
let { data } = await axios.post(`/users${currLocation}`);
initialBizCardArray = data;
setData(initialBizCardArray);
}
break;
}
} catch (error) {
toast.error(error, {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
return { error };
}
})();
}, []);
return { data, setData, initialBizCardArray };
};
export default useFetch;
How i tried using the useFetch
const handleSubmitLogIn = async (ev) => {
ev.preventDefault();
ValidateErr(
{
email: loginInput.email,
password: loginInput.password,
},
loginSchema
);
try {
let { data } = await useFetch(userLocation, {
email: loginInput.email,
password: loginInput.password,
});
console.log("Succuss");
localStorage.setItem("token", data.token);
autoLoginFunction(data.token);
setTimeout(() => {
let userInfo = jwt_decode(data.token);
userInfo && userInfo.biz
? history.push("/my-cards")
: history.push("/");
}, 100);
} catch (err) {
console.error("error", err.response.data);
toast.error(`😠Email or password are invalid.`, err, {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
progress: undefined,
});
history.push("/login");
}
You could replace your useEffect hook within useFetch by a method, that way you can return it and call it in your event handler :
import React, { useState } from "react";
import axios from "axios";
import { useHistory } from "react-router-dom";
import { toast } from "react-toastify";
const useFetch = () => {
const [data, setData] = useState([]);
const history = useHistory();
const fetchData = async () => {
try {
let currLocation = history.location.pathname;
switch (currLocation) {
case (currLocation = "/my-cards"):
case (currLocation = "/cards"):
{
let { data } = await axios.get(`/cards${currLocation}`);
setData(data);
}
break;
case (currLocation = "/login"):
case (currLocation = "/register"):
{
let { data } = await axios.post(`/users${currLocation}`);
setData(data);
}
break;
}
} catch (error) {
toast.error(error, {
position: "top-right",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
}
return { data, fetchData };
};
export default useFetch;
Then you would use it like this within your component :
import React, {useEffect} from "react"
const Component = () => {
const { data,fetchData} = useFetch()
useEffect(() => {
fetchData()
},[])
return (
<div>{data.property}</div>
)
}
Some points
Avoid using location string to decide which api call to make as it makes the code fragile, i.e. it has many reasons to break, like route name change and every time a new route is added/changed there is a possiblity that someone can break other parts of the code block
try just create a api function and export them
export const loginRegisterApi = async (email, password) => {
//network call with axios instance or anything you
// error checks
return response.data
}
use libraries like React query which make server related state management a lot more easier
cont LoginComponent = ({ ... }) => {
const { mutate, ... } = useMutation({
mutationFn: loginRegisterApi,
onSuccess: () => {
// Invalidate and refetch
// queryClient.invalidateQueries({ queryKey: ['userProfile'] })
},
})
return (
<button
onClick={() => {
// validations and checks
mutate({
{
email: loginInput.email,
password: loginInput.password,
},
})
}}
>
Login
</button>
...
)
}
If you expect the login and register requirments will change and code becomes complex, its could be better to seperate them as changing one could break the other, and login is used more often then register
Consider these as points for discussion, hope they help in someway
Related
I have a problem when I want to log in to the login by entering the email and password. What happens is that when I enter with the correct email and correct password, the animation appears but it stays cycled, and if I refresh the page and try again, now it lets me enter into the application
Here's my login form code:
import axios from "axios";
import { useRef, useState } from "react";
import { storeToken } from "../utils/authServices";
import { useNavigate } from "react-router-dom";
import { useLoading } from "../context/hooks/useLoading";
import { LoginForm } from "../components";
export const Login = () => {
const API_URL = "https://api.app"; //I hide the API for security reasons
const { run } = useLoading();
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const navigate = useNavigate();
const correoRef = useRef("");
const passwordRef = useRef("");
const handleSubmit = async (e) => {
e.preventDefault();
const { value: correo } = correoRef.current;
const { value: password } = passwordRef.current;
await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
})
.then((response) => {
storeToken(response.data.token);
run();
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
})
.catch((err) => {
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
});
};
return (
<LoginForm
correoRef={correoRef}
passwordRef={passwordRef}
handleSubmit={handleSubmit}
error={error}
errorMessage={errorMessage}
/>
);
};
import { createContext, useReducer, useContext } from "react";
const initialState = {
loading: false,
alerts: [],
};
const reducers = (state, action) => {
switch (action.type) {
case "LOADING_RUN":
return {
...state,
loading: true,
};
case "LOADING_STOP":
return {
...state,
loading: false,
};
default:
return { ...state };
}
};
const AppContext = createContext();
const AppContextProvider = (props) => {
const [state, dispatch] = useReducer(reducers, initialState);
return <AppContext.Provider value={{ state, dispatch }} {...props} />;
};
const useAppContext = () => useContext(AppContext);
export { AppContextProvider, useAppContext };
import { useMemo } from "react";
import { useAppContext } from "../AppContext";
export const useLoading = () => {
const { dispatch } = useAppContext();
const loading = useMemo(
() => ({
run: () => dispatch({ type: "LOADING_RUN" }),
stop: () => dispatch({ type: "LOADING_STOP" }),
}),
[dispatch]
);
return loading;
};
import jwt_decode from "jwt-decode";
export const storeToken = (token) => {
localStorage.setItem("token", token);
};
export const getToken = (decode = false) => {
const token = localStorage.getItem("token");
if (decode) {
const decoded = jwt_decode(token);
return decoded;
}
return token;
};
export const logout = () => {
localStorage.removeItem("token");
};
How can I log in without refreshing the page?
There's two problems here. One is you're using await with a .then .catch block. Pick one or the other. You're also never calling the stop() dispatch when your async call is complete which appears to be responsible for removing the loader.
Instead of:
const { run } = useLoading();
Use:
const { run, stop } = useLoading();
Then change this:
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
To this:
setTimeout(() => {
navigate("/nueva-solicitud");
stop();
}, 1000);
Although I would just recommend writing the entire promise like this:
try {
run();
const response = await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
});
storeToken(response.data.token);
navigate("/nueva-solicitud");
stop();
} catch (err) {
stop();
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
}
I have created the following custom hook, and I'm having trouble mocking the hook in a way that the returned data would be updated when the callback is called.
export const useLazyFetch = ({ method, url, data, config, withAuth = true }: UseFetchArgs): LazyFetchResponse => {
const [res, setRes] = useState({ data: null, error: null, loading: false});
const callFetch = useCallback(() => {
setRes({ data: null, error: null, loading: true});
const jwtToken = loadItemFromLocalStorage('accessToken');
const authConfig = {
headers: {
Authorization: `Bearer ${jwtToken}`
}
};
const combinedConfig = Object.assign(withAuth ? authConfig : {}, config);
axios[method](url, data, combinedConfig)
.then(res => setRes({ data: res.data, loading: false, error: null}))
.catch(error => setRes({ data: null, loading: false, error}))
}, [method, url, data, config, withAuth])
return { res, callFetch };
};
The test is pretty simple, when a user clicks a button to perform the callback I want to ensure that the appropriate elements appear, right now I'm mocking axios which works but I was wondering if there is a way to mock the useLazyFetch method in a way that res is updated when the callback is called. This is the current test
it('does some stuff', async () => {
(axios.post as jest.Mock).mockReturnValue({ status: 200, data: { foo: 'bar' } });
const { getByRole, getByText, user } = renderComponent();
user.click(getByRole('button', { name: 'button text' }));
await waitFor(() => expect(getByText('success message')).toBeInTheDocument());
});
Here's an example of how I'm using useLazyFetch
const Component = ({ props }: Props) => {
const { res, callFetch } = useLazyFetch({
method: 'post',
url: `${BASE_URL}/some/endpoint`,
data: requestBody
});
const { data: postResponse, loading: postLoading, error: postError } = res;
return (
<Element
header={header}
subHeader={subHeader}
>
<Button
disabled={postLoading}
onClick={callFetch}
>
Submit Post Request
</Button>
</Element>
);
}
axios is already tested so there's no point in writing tests for that. We should be testing useLazyFetch itself. However, I might suggest abstracting away the axios choice and writing a more generic useAsync hook.
// hooks.js
import { useState, useEffect } from "react"
function useAsync(func, deps = []) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [data, setData] = useState(null)
useEffect(_ => {
let mounted = true
async function run() {
try { if (mounted) setData(await func(...deps)) }
catch (e) { if (mounted) setError(e) }
finally { if (mounted) setLoading(false) }
}
run()
return _ => { mounted = false }
}, deps)
return { loading, error, data }
}
export { useAsync }
But we can't stop there. Other improvements will help too, like a better API abstraction -
// api.js
import axios from "axios"
import { createContext, useContext, useMemo } from "react"
import { useLocalStorage } from "./hooks.js"
function client(jwt) {
// https://axios-http.com/docs/instance
return axios.create(Object.assign(
{},
jwt && { headers: { Authorization: `Bearer ${jwt}` } }
))
}
function APIRoot({ children }) {
const jwt = useLocalStorage("accessToken")
const context = useMemo(_ => client(jwt), [jwt])
return <ClientContext.Provider value={context}>
{children}
</ClientContext.Provider>
}
function useClient() {
return useContext(ClientContext)
}
const ClientContext = createContext(null)
export { APIRoot, useClient }
When a component is a child of APIRoot, it has access to the axios client instance -
<APIRoot>
<User id={4} /> {/* access to api client inside APIRoot */}
</APIRoot>
// User.js
import { useClient } from "./api.js"
import { useAsync } from "./hooks.js"
function User({ userId }) {
const client = useClient() // <- access the client
const {data, error, loading} = useAsync(id => { // <- generic hook
return client.get(`/users/${id}`).then(r => r.data) // <- async
}, [userId]) // <- dependencies
if (error) return <p>{error.message}</p>
if (loading) return <p>Loading...</p>
return <div data-user-id={userId}>
{data.username}
{data.avatar}
</div>
}
export default User
That's helpful, but the component is still concerned with API logic of constructing User URLs and things like accessing the .data property of the axios response. Let's push all of that into the API module -
// api.js
import axios from "axios"
import { createContext, useContext, useMemo } from "react"
import { useLocalStorage } from "./hooks.js"
function client(jwt) {
return axios.create(Object.assign(
{ transformResponse: res => res.data }, // <- auto return res.data
jwt && { headers: { Authorization: `Bearer ${jwt}` } }
))
}
function api(client) {
return {
getUser: (id) => // <- user-friendly functions
client.get(`/users/${id}`), // <- url logic encapsulated
createUser: (data) =>
client.post(`/users`, data),
loginUser: (email, password) =>
client.post(`/login`, {email,password}),
// ...
}
}
function APIRoot({ children }) {
const jwt = useLocalStorage("accessToken")
const context = useMemo(_ => api(client(jwt)), [jwt]) // <- api()
return <APIContext.Provider value={context}>
{children}
</APIContext.Provider>
}
const APIContext = createContext({})
const useAPI = _ => useContext(APIContext)
export { APIRoot, useAPI }
The pattern above is not sophisticated. It could be easily modularized for more complex API designs. Some segments of the API may require authorization, others are public. The API module gives you a well-defined area for all of this. The components are now freed from this complexity -
// User.js
import { useAPI } from "./api.js"
import { useAsync } from "./hooks.js"
function User({ userId }) {
const { getUser } = useAPI()
const {data, error, loading} = useAsync(getUser, [userId]) // <- ez
if (error) return <p>{error.message}</p>
if (loading) return <p>Loading...</p>
return <div data-user-id={userId}>
{data.username}
{data.avatar}
</div>
}
export default User
As for testing, now mocking any component or function is easy because everything has been isolated. You could also create a <TestingAPIRoot> in the API module that creates a specialized context for use in testing.
See also -
react-query
useSWR
useLocalStorage
I'm relatively new to testing and I can happily test UI components that use renders, but I'm struggling to figure out the correct way to test logic components in my current preferred architecture method.
I'm using the BLoC architecture method with RxJS as I much prefer using this to other state management libraries.
I've tried to simply import the functions or access them like I would do in the components and mock them but I can't seem to actually properly simulate the functions.
Here is the an example of the BLoC:
import { IHomeScreenService } from 'actions/interface'
import delivery from 'delivery'
import { ProtoBloc } from '../ProtoBloc'
import { IHomeScreenState } from './stateModel'
const defaultHomeScreenState: IHomeScreenState = {
categoriesData: [],
mealsData: null,
isLoaded: false,
mealsLoading: false,
currentCategory: 'Beef'
}
class HomeScreenBloc extends ProtoBloc<IHomeScreenState> implements IHomeScreenService {
constructor() {
super(defaultHomeScreenState)
}
initialLoad = async () => {
await this.getCategories()
// Set a default category for initial load
await this.getRecipesByCategory(this.state.currentCategory)
this.setLoader()
}
getCategories = async () => {
const { value, error } = await delivery.MealAPI.getCategories()
const newState = { ...this.state }
if (error) {
console.warn('Request Failed')
return
}
if (value) {
newState.categoriesData = value.categories
this.pushState(newState)
}
}
getRecipesByCategory = async (category: string) => {
const { value, error } = await delivery.MealAPI.getRecipesByCategory(category)
const newState = { ...this.state }
if (error) {
console.warn('Request Failed')
return
}
if (value) {
newState.mealsData = value.meals
newState.currentCategory = category
this.pushState(newState)
}
}
private setLoader = () => {
this.pushState({
...this.state,
isLoaded: true,
})
}
}
export default new HomeScreenBloc()
And here is my test file for this BLoC:
import React from 'react'
import HomeScreenBloc from './index'
let defaultState
HomeScreenBloc.getCategories = jest.fn()
beforeEach(() => {
defaultState = {
categoriesData: [],
mealsData: null,
isLoaded: false,
mealsLoading: false,
currentCategory: 'Beef'
}
})
test('should get default state', () => {
expect(HomeScreenBloc.defaultState).toEqual(defaultState)
})
test('it should set the categories state', () => {
HomeScreenBloc.getCategories()
const newState = HomeScreenBloc.getSubject()
expect(HomeScreenBloc.getCategories).toBeCalled()
console.log(newState)
})
Currently all tests are passing, but what I wan't to be able to check is too see if the state is changing as expected. I also don't wan't to test the API calls in this function as I'll be testing those in the delivery layer.
Any help is very much appreciated, I've been banging my head against this for a while and can't find any resources online that relate to this architecture method.
Im trying to implement debouncing with my own pure js function for calling the action in action file with axios POST request.
The below code in the input box component
import React, { useState, useCallback } from 'react'
import { searchDrug } from '../../actions/drug-interaction'
function CustomInputSearch(props) {
const { handleSelectInput } = props
const apiCall = (value) => {
searchDrug(value)
}
const debounce = (apiFunc, delay) => {
let inDebounce
return function () {
const context = this
const args = arguments
clearTimeout(inDebounce)
inDebounce = setTimeout(() => apiFunc.apply(context, args), delay)
}
}
const optimizedVersion = debounce(apiCall, 500)
const handleChange = (e) => {
optimizedVersion(e.target.value)
}
return (
<div>
<input
className='form-control'
placeholder='Search Drug...'
onKeyUp={handleChange}
/>
</div>
)
}
export default CustomInputSearch
Ignore the unnececssary imports.
The below code is the action file.
export const searchDrug = (drug) => {
const params = {
"start": drug,
"limit": 100
}
let axiosConfig = {
headers: {
// 'Access-Control-Allow-Origin': '*'
}
}
return (dispatch) => {
dispatch({ type: 'DRUG_LIST_NOTIFY', payload: { drugListLoading: true } })
axios.post(`${API_URL}/drug/autocomplete`, params, axiosConfig)
.then((response) => {
dispatch({
type: 'DRUG_LIST',
payload: { response: response.data, drugListLoading: false }
})
})
.catch((error) => {
dispatch({ type: 'DRUG_LIST_NOTIFY', payload: { drugListLoading: false } })
if (error.response && error.response.status === 401) {
window.open('/?src=auth-error', '_self')
}
});
};
}
But im not seeing any request going in network tab in browser.Im also composedWithDevtools in redux store.Thanks in advance
It is because your searchDrug action must be came from dispatch instead of calling it directly.
import React, { useState, useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { searchDrug } from '../../actions/drug-interaction'
function CustomInputSearch(props) {
const { handleSelectInput } = props
const dispatch = useDispatch()
const apiCall = (value) => {
dispatch(searchDrug(value))
}
...
I have been battling this issue for some time now and I can't seem to figure out how to solve it.
On my homescreen I fetch the user coordinates and an array of cinemas. The Cinemas array contains coordinates for each cinema, which I use with the user coordinates to calculate the distance from the user to the cinema.
Fetching and storing the cinemas in the cinemaContext is fine and working, but once I run the function that calculates the distance, the cinema object in context is empty.
The distance calculation adds a distance value to each cinema in the array and returns the new array.
The weird thing is the cinema context object is empty when I try. Then if I edit something in the cinemaContext or inside the getUserCoordinates function and navigate to the cinema overview screen, then the cinemas are there with the distance value.
It must be something with the loading sequence or async functionality, because the code is "working", but does not seem to populate the context at the right time or overwrite it with empty value.
I should add that I use the cinemas array on another screen, where I get access to it like this:
const { state } = useContext(Context)
Home.js
import React, { useState, useEffect, useContext } from "react";
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, ActivityIndicator } from "react-native";
import * as Location from 'expo-location';
import { Context } from "../context/CinemaContext";
const Home = ({ navigation }) => {
const [errorMsg, setErrorMsg] = useState(null);
const { state, updateCinemas, getCinemas } = useContext(Context)
// Fetch user coordinates and call updateCinemas with the coordinates and cinemas
const getUserCoordinates = async (cinemas) => {
try {
const granted = await Location.requestPermissionsAsync();
if (granted) {
let location = await Location.getCurrentPositionAsync({});
await updateCinemas(cinemas, location.coords.latitude, location.coords.longitude)
} else {
throw new Error("Location permission not granted");
}
} catch (e) {
setErrorMsg(e)
}
}
useEffect(() => {
if (state.cinemas.length === 0) {
getCinemas();
}
getUserCoordinates(state.cinemas);
}, []);
if (!state.cinemas) {
return <ActivityIndicator size="large" style={{ marginTop: 200 }} />
}
return ( Some views ..)
CinemaContext.js
import dataContext from "./DataContext";
import _ from "lodash";
import { computeDistance } from "../helpers/utils";
const cinemaReducer = (state, action) => {
switch (action.type) {
case "add_error":
return { ...state, errorMessage: action.payload };
case "get_cinemas":
return { ...state, cinemas: action.payload };
case "update_cinemas":
return { ...state, cinemas: action.payload };
default:
return state
}
};
const getCinemas = dispatch => async () => {
try {
const response = await fetch(
"url-to-cinemas-array",
{ mode: "no-cors" })
const cinemas = await response.json();
dispatch({
type: "get_cinemas",
payload: cinemas
});
} catch (err) {
dispatch({
type: "add_error",
payload: "Something went wrong with the cinemas"
})
}
}
const updateCinemas = (dispatch) => {
return async (cinemas, referenceLat, referenceLong) => {
const cinemasWithDistance = cinemas.map(cinema => {
return {
...cinema,
distance: computeDistance([cinema.geo.latitude, cinema.geo.longitude], [referenceLat, referenceLong]) // Calculate the distance
};
});
const orderedCinemas = _.orderBy(cinemasWithDistance, 'distance');
dispatch({ type: "update_cinemas", payload: orderedCinemas });
}
}
export const { Context, Provider } = dataContext(
cinemaReducer,
{ updateCinemas, getCinemas },
{ cinemas: [], errorMessage: '' }
);
DataContext.js
import React, { useReducer } from 'react';
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
App.js
import React from "react";
import RootStackNavigator from "./src/navigation/RootStackNavigator";
import { Provider } from "./src/context/CinemaContext";
export default function App() {
return (
<Provider>
<RootStackNavigator />
</Provider>
);
};
Any help is greatly appreciated!
Thanks in advance!
I think state.cinemas is still empty when you call getUserCoordinates(state.cinemas); so "update_cinemas" action will run with an empty array, overwriting the cinemas array updated previously by "get_cinemas". You can verify this by adding console.log('state.cinemas.length: ', state.cinemas.length); before the getUserCoordinates call.
One solution to this I think would be to call getUserCoordinates in a separate useEffect that depends on state.cinemas array (so that it runs again every time state.cinemas changes):
useEffect(() => {
if (state.cinemas.length === 0) {
getCinemas();
}
}, []);
useEffect(() => {
if (state.cinemas.length === 0) return; // cinemas not retrieved yet
if (typeof state.cinemas[0].distance !== undefined) return; // distance already computed
getUserCoordinates(state.cinemas);
}, [state.cinemas]);