How can I deal with the state updating asynchronously? - reactjs

I have a login page where if a user enters the wrong credentials or submits blank fields, an error will be displayed on the page. Currently, when a user fails to signin with the right credentials, the error will only be displayed on the second click. I'm aware that my current problem is due to the state updating asynchronously, but I'm not sure how to resolve the problem in my code:
onst Login: React.FC<Props> = () => {
const user = useAppSelector(selectUser);
const auth = useAppSelector(selectAuth);
const dispatch = useAppDispatch();
...
const [signInError, setSignInError] = useState<boolean>(false);
const handleSignInError = () => {
if (auth.error?.status === 401 && auth.error.message === Constants.Errors.WRONG_CREDENTIALS) {
setSignInError(true);
}
}
const renderSigninError = () => {
if (signInError) {
return (
<Box paddingTop={2.5}>
<Alert variant="outlined" severity="error">
{Constants.Auth.FAILED_SIGN_IN}
</Alert>
</Box>
);
} else {
return (
<div/>
);
}
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData: any = new FormData(event.currentTarget);
const loginData: LoginData = {
email: formData.get("email"),
password: formData.get("password"),
}
try {
const res: any = await dispatch(login(loginData));
const resType: string = res.type;
if (resType === "auth/login/fulfilled") {
const userPayload: UserLogin = res.payload;
const loginUser: UserInterface = {
...
}
setSignInError(false);
dispatch(setUser(loginUser))
navigate("/");
}
else {
console.log("Failed to login");
handleSignInError();
}
}
catch (error: any) {
console.log(error.message);
}
}
return (
<Box
...code omitted...
{renderSigninError()}
...
)
}
What can I do to make sure that when the app loads and the user fails to login on the first click, the state for signInError should be true and the error displays?

You have at least 2 options.
add a conditional component.
Add a useEffect listenning for signInError and handle there as you want. It will trigger everytime signInError state changes
import React from 'react';
const [signInError, setError] = useState(false)
import React from 'react';
const [signInError, setError] = useState(false)
useEffect(() => {
console.log('new signInError value >', signInError)
// do whatever you want
}, [signInError])
export function App(props) {
return (
<div className='App'>
{
signInError ? (<p>something happened</p>) : null
}
</div>
);
}
There might be better approaches. Hope this can help you

I found a work around by changing handleSignInError() to update the state directly through setSignInError(true) as in:
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData: any = new FormData(event.currentTarget);
const loginData: LoginData = {
email: formData.get("email"),
password: formData.get("password"),
}
try {
const res: any = await dispatch(login(loginData));
const resType: string = res.type;
if (resType === "auth/login/fulfilled") {
const userPayload: UserLogin = res.payload;
const loginUser: UserInterface = {
...
}
setSignInError(false);
dispatch(setUser(loginUser))
navigate("/");
}
else {
console.log("Failed to login");
setSignInError(true); //changed here
}
}
catch (error: any) {
console.log(error.message);
}
}
Could someone help me understand why using another function initially didnt work?

Related

I need to refresh the page to login | React and Axios

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);
}

How to immediately update a state in React

I'm struggling with this problem and I've already tried many solutions but none of them fit me.
I have a context that I use to share information that I get from an API. I will summarize the files for you:
file: useGetInfo.tsx
type InfoContextData = { ... }
type Props = { ... }
type InfoResponseProps = { ... }
export const InfoContext = createContext<InfoContextData>({} as InfoContextData)
export const InformationProvider = ({ children }: Props) => {
const isBrowser = typeof window !== `undefined`
const [infoStorage, setInfoStorage] = useState(
isBrowser && localStorage.getItem('info')
? String(localStorage.getItem('info'))
: undefined
)
const [result, setResult] = useState<InfoResponseProps | null>(null)
const getInfo = useCallback(async (value: string) => {
const url = `<URL_FROM_API${value}>`
await axios.get(url)
.then((response) => {
setResult(response.data)
})
.catch((_) => {
setResult(null)
})
})
useEffect(() => {
if (!infoStorage) {
return
}
getInfo(infoStorage)
}, [infoStorage, getInfo])
return (
<InfoContext.Provider
value={{
result,
setResult,
infoStorage,
setInfoStorage,
getInfo,
}}
>
{children}
</InfoContext.Provider>
)
}
Then in the component I call the context:
file: SomeComponent.tsx
const Component = () => {
const { setInfoStorage, getInfo, result } = useContext(InfoContext)
const [input, setInput] = useState('')
const handleInfoSubmit = useCallback(() => {
getInfo(input)
if (!result || !result?.ok) {
localStorage.removeItem('info')
setInfoStorage(undefined)
}
setInfoStorage(input)
localStorage.setItem('info', 'input')
setInput('')
}, [input, result, getInfo, setInfoStorage, setInput])
return (
...
<Form onSubmit={handleInfoSubmit}>
<input>
...
</Form>
)
}
Basically, the user inserts a code in the form and when he submits the form, it runs the handleInfoSubmit function. Then, the code runs the function getInfo() and after requesting the API it returns the information to the state result.
The problem is in the SomeComponent.tsx file: when I run the function getInfo(input) I need the information in the state result but at the time axios finishes the request to the API and the code goes to the if (!result || !result?.ok) line, the result state is not still fulfilled.
I know that React/Gatsby can't update immediately the state like what I need, but is there a way to overcome this problem? Thanks in advance.
I think the value of the result would always be stale inside the handleInfoSubmit function per your code.
Rewrite the getInfo and handleInfoSubmit like this
// Return data from getInfo so that we can use the value directly in handleInfoSubmit
const getInfo = useCallback(async (value: string) => {
const url = `<URL_FROM_API${value}>`
try {
const { data } = await axios.get(url);
setResult(data)
return data;
} catch {
setResult(null)
}
return null;
})
const handleInfoSubmit = useCallback(async () => {
// await getInfo and get the axios response data.
const result = await getInfo(input)
if (!result || !result?.ok) {
localStorage.removeItem('info')
setInfoStorage(undefined)
}
setInfoStorage(input)
localStorage.setItem('info', 'input')
setInput('')
}, [input, getInfo, setInfoStorage, setInput])

React Native renders previous page before going to the intended screen

New to react native so I'm not sure if this is just a glitch. My ultimate intention is to check if a user has been fully onboarded or not. If a user logs in, they're a returning user, so they shouldn't go through the onboarding screens again.
This is the flow:
New user? Landing -> Registration -> Onboarding -> Home
Existing user? Landing -> Login -> Home
In order to know if the user is a returning user, I check to see if the onboarded completed variable is in the firestore db and if it's true, the Onboarded function is set to true. This all works fine, except that before the login page switches to the home page upon login, the screen displays the onboarding page for a quick second. How do I stop this? As this isn't ideal in production.
Here is a snippet of the login
export function LoginScreen({ navigation }) {
const { onboarded, setOnboarded, login } = useContext(AuthContext);
const [isChecked, setChecked] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
const userCred = await login(email, password);
const docRef = doc(db, "userInfo", userCred.user.uid);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
if (docSnap.get("onboardingCompleted") === true) {
setOnboarded(true);
} else {
console.log("nothing here for you");
}
} else {
console.log("nothing exists!");
}
};
Here is a snippet of my Routes page:
const Routes = () => {
const { user, setUser, onboarded, setOnboarded } = useContext(AuthContext);
const [initializing, setInitializing] = useState(true);
const StateChanged = (user) => {
setUser(user);
if (initializing) setInitializing(false);
};
useEffect(() => {
const subscriber = onAuthStateChanged(auth, StateChanged);
return subscriber; // unsubscribe on unmount
}, []);
if (initializing) return null;
const DisplayStacks = () => {
if (user && onboarded) {
// returning user or end of onboarding; working for returning user
return <AppStack />;
} else if (user) {
//after registering; clicking create account; working fine
return <RegistrationStack />;
} else {
return <AuthStack />; // before registering; working fine
}
};
return <NavigationContainer>{DisplayStacks()}</NavigationContainer>;
};
export default Routes;
Auth Context:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [onboarded, setOnboarded] = useState(false);
return (
<AuthContext.Provider
value={{
user,
setUser,
onboarded,
setOnboarded,
login: async (email, password) => {
try {
const res = await signInWithEmailAndPassword(auth, email, password);
return res;
} catch (e) {
console.log(e);
alert("Wrong info mane!!");
}
},
register: async (email, password) => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (e) {
console.log(e);
alert(e);
}
},
logout: async () => {
try {
await signOut(auth);
setOnboarded(false);
} catch (e) {
console.log(e);
}
},
}}
>
{children}
</AuthContext.Provider>
);
};
User state is created in the Auth context, but it's set in the routes page
Centralize the authentication logic and the logic to check if an authenticated user has been onboarded into the AuthProvider component. In the stateChange callback for the onAuthStateChanged event handler do an additional isOnboarded check, and then only after both the user and isOnboarded states/checks have completed is the initializing state set false. This is obviously all not tested but I believe should get you close to the UX you desire.
AuthProvider
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isOnboarded, setIsOnboarded] = useState(false);
const [initializing, setInitializing] = useState(true);
useEffect(() => {
const checkOnboardStatus = async (userId) => {
const docRef = doc(db, "userInfo", userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.get("onboardingCompleted");
} else {
console.log("nothing exists!");
}
};
const stateChanged = async (user) => {
setUser(user);
try {
if (user?.uid) {
// have a user, check isOnboarded status
const isOnboarded = await checkOnboardStatus();
if (!isOnboarded) {
console.log("nothing here for you");
}
setIsOnboarded(isOnboarded);
} else {
// no user, clear isOnboarded status
setIsOnboarded(false);
}
} catch(error) {
// handle any fetching/processing error if necessary
} finally {
setInitializing(false);
}
};
const subscriber = onAuthStateChanged(auth, stateChanged);
return subscriber; // unsubscribe on unmount
}, []);
const login = (email, password) => {
try {
// Set initializing true here each time a user is authenticating
// to make the UI wait for the onboarded check to complete
setInitializing(true);
return signInWithEmailAndPassword(auth, email, password);
} catch (e) {
console.log(e);
alert("Wrong info mane!!");
}
};
const register = (email, password) => {
try {
return createUserWithEmailAndPassword(auth, email, password);
} catch (e) {
console.log(e);
alert(e);
}
};
const logout = () => {
try {
return signOut(auth);
} catch (e) {
console.log(e);
}
}
return (
<AuthContext.Provider
value={{
user,
isOnboarded,
initializing,
login,
register,
logout,
}}
>
{children}
</AuthContext.Provider>
);
};
Routes
const Routes = () => {
const { user, isOnboarded, initializing } = useContext(AuthContext);
if (initializing) return null;
return (
<NavigationContainer>
{user
? isOnboarded ? <AppStack /> : <RegistrationStack />
: <AuthStack />
}
</NavigationContainer>
);
};
LoginScreen
export function LoginScreen({ navigation }) {
const { login } = useContext(AuthContext);
const [isChecked, setChecked] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = () => {
login(email, password);
};
...

React Firebase is not returning error message even after giving wrong input

I am using React Firebase hook to log in to my website. when trying to log in with the wrong email or password in the login form, an error message will be returned from the React firebase hook. But even after giving the wrong input, an error message is not returning
const Login = () => {
const [signInWithEmailAndPassword, error] =
useSignInWithEmailAndPassword(auth);
const location = useLocation();
const navigate = useNavigate();
const from = location?.state?.from?.pathname || '/';
if (error) {
return (
<div>
<p>Error: {error.message}</p>
</div>
);
}
const handleLogIn = (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
signInWithEmailAndPassword(email, password)
e.target.reset();
navigate(from, { replace: true })
}
You are using signInWithEmailAndPassword hook incorrectly.
signInWithEmailAndPassword returns an array & 3th index is of error message.
You can follow this: https://github.com/CSFrequency/react-firebase-hooks/blob/master/auth/README.md#usesigninwithemailandpassword
const [
signInWithEmailAndPassword,
user,
loading,
error,
] = useSignInWithEmailAndPassword(auth);
Since, useSignInWithEmailAndPassword returns an Array, We need to extract/destructure the value from respective index.
Apart from that, You must also use loading to display whether firebase is still authorizing the request or not (Loading State).
The signInWithEmailAndPassword appears to be an async function and your code isn't waiting for the returned Promise to resolve. I'm guessing you are seeing the navigate("/"); called and the app is navigating to the home page.
const handleLogIn = (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
signInWithEmailAndPassword(email, password); // <-- no waiting for promise
e.target.reset();
navigate(from, { replace: true }); // <-- navigate away
};
useSignInWithEmailAndPassword
export default (auth: Auth): EmailAndPasswordActionHook => {
const [error, setError] = useState<AuthError>();
const [loggedInUser, setLoggedInUser] = useState<UserCredential>();
const [loading, setLoading] = useState<boolean>(false);
const signInWithEmailAndPassword = async (
email: string,
password: string
) => {
setLoading(true);
setError(undefined);
try {
const user = await firebaseSignInWithEmailAndPassword(
auth,
email,
password
);
setLoggedInUser(user);
} catch (err) {
setError(err as AuthError);
} finally {
setLoading(false);
}
};
const resArray: EmailAndPasswordActionHook = [
signInWithEmailAndPassword,
loggedInUser,
loading,
error,
];
return useMemo<EmailAndPasswordActionHook>(() => resArray, resArray);
};
The handleLogin handler should probably wait for the Promise to settle so any errors can be returned by the hook. It turns out though that signInWithEmailAndPassword also doesn't return any resolve/rejected values, so there's no way to know the authentication was successful from within the handleLogIn callback function, the component will need to use the hook's returned loading and loggedInUser states to determine if it is safe to navigate.
Example:
const Login = () => {
const [
signInWithEmailAndPassword,
loggedInUser,
loading,
error,
] = useSignInWithEmailAndPassword(auth);
const location = useLocation();
const navigate = useNavigate();
const from = location?.state?.from?.pathname || '/';
useEffect(() => {
if (!loading && loggedInUser) {
navigate(from, { replace: true });
}, [loggedInUser, loading, navigate, from]);
if (error) {
return (
<div>
<p>Error: {error.message}</p>
</div>
);
}
const handleLogIn = (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
signInWithEmailAndPassword(email, password)
e.target.reset();
}
...

To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function

I have this code
import ReactDOM from "react-dom";
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>
<Link to="/">Netflix</Link>
<Route path="/" component={Miliko} />
</div>
</Router>
);
}
const Miliko = ({ match }) => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const Res = await fetch("https://foo0022.firebaseio.com/New.json");
const ResObj = await Res.json();
const ResArr = await Object.values(ResObj).flat();
setData(ResArr);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
console.log(data);
}, [match]);
return <div>{`${isLoading}${isError}`}</div>;
};
function App() {
return (
<div className="App">
<ParamsExample />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I created three links that open the Miliko component. but when I quickly click on the links I get this error:
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I think the problem is caused by dismount before async call finished.
const useAsync = () => {
const [data, setData] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
if (!mountedRef.current) return null
setData(res)
return res
})
}, [])
useEffect(() => {
return () => {
mountedRef.current = false
}
}, [])
}
mountedRef is used here to indicate if the component is still mounted. And if so, continue the async call to update component state, otherwise, skip them.
This should be the main reason to not end up with a memory leak (access cleanedup memory) issue.
Demo
https://codepen.io/windmaomao/pen/jOLaOxO , fetch with useAsync
https://codepen.io/windmaomao/pen/GRvOgoa , manual fetch with useAsync
Update
The above answer leads to the following component that we use inside our team.
/**
* A hook to fetch async data.
* #class useAsync
* #borrows useAsyncObject
* #param {object} _ props
* #param {async} _.asyncFunc Promise like async function
* #param {bool} _.immediate=false Invoke the function immediately
* #param {object} _.funcParams Function initial parameters
* #param {object} _.initialData Initial data
* #returns {useAsyncObject} Async object
* #example
* const { execute, loading, data, error } = useAync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* funcParams: { data: '1' },
* initialData: 'Hello'
* })
*/
const useAsync = (props = initialProps) => {
const {
asyncFunc, immediate, funcParams, initialData
} = {
...initialProps,
...props
}
const [loading, setLoading] = useState(immediate)
const [data, setData] = useState(initialData)
const [error, setError] = useState(null)
const mountedRef = useRef(true)
const execute = useCallback(params => {
setLoading(true)
return asyncFunc({ ...funcParams, ...params })
.then(res => {
if (!mountedRef.current) return null
setData(res)
setError(null)
setLoading(false)
return res
})
.catch(err => {
if (!mountedRef.current) return null
setError(err)
setLoading(false)
throw err
})
}, [asyncFunc, funcParams])
useEffect(() => {
if (immediate) {
execute(funcParams)
}
return () => {
mountedRef.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return {
execute,
loading,
data,
error
}
}
Update 2022
This approach has been adopted in the book https://www.amazon.com/Designing-React-Hooks-Right-Way/dp/1803235950 where this topic has been mentioned in useRef and custom hooks chapters, and more examples are provided there.
useEffect will try to keep communications with your data-fetching procedure even while the component has unmounted. Since this is an anti-pattern and exposes your application to memory leakage, cancelling the subscription to useEffect optimizes your app.
In the simple implementation example below, you'd use a flag (isSubscribed) to determine when to cancel your subscription. At the end of the effect, you'd make a call to clean up.
export const useUserData = () => {
const initialState = {
user: {},
error: null
}
const [state, setState] = useState(initialState);
useEffect(() => {
// clean up controller
let isSubscribed = true;
// Try to communicate with sever API
fetch(SERVER_URI)
.then(response => response.json())
.then(data => isSubscribed ? setState(prevState => ({
...prevState, user: data
})) : null)
.catch(error => {
if (isSubscribed) {
setState(prevState => ({
...prevState,
error
}));
}
})
// cancel subscription to useEffect
return () => (isSubscribed = false)
}, []);
return state
}
You can read up more from this blog juliangaramendy
Without #windmaomao answer, I could spend other hours trying to figure out how to cancel the subscription.
In short, I used two hooks respectively useCallback to memoize function and useEffect to fetch data.
const fetchSpecificItem = useCallback(async ({ itemId }) => {
try {
... fetch data
/*
Before you setState ensure the component is mounted
otherwise, return null and don't allow to unmounted component.
*/
if (!mountedRef.current) return null;
/*
if the component is mounted feel free to setState
*/
} catch (error) {
... handle errors
}
}, [mountedRef]) // add variable as dependency
I used useEffect to fetch data.
I could not call the function inside effect simply because hooks can not be called inside a function.
useEffect(() => {
fetchSpecificItem(input);
return () => {
mountedRef.current = false; // clean up function
};
}, [input, fetchSpecificItem]); // add function as dependency
Thanks, everyone your contribution helped me to learn more about the usage of hooks.
fetchData is an async function which will return a promise. But you have invoked it without resolving it. If you need to do any cleanup at component unmount, return a function inside the effect that has your cleanup code. Try this :
const Miliko = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState('http://hn.algolia.com/api/v1/search?query=redux');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
(async function() {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
})();
return function() {
/**
* Add cleanup code here
*/
};
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
I would suggest reading the official docs where it is clearly explained along with some more configurable parameters.
Folowing #Niyongabo solution, the way I ended up that fixed it was:
const mountedRef = useRef(true);
const fetchSpecificItem = useCallback(async () => {
try {
const ref = await db
.collection('redeems')
.where('rewardItem.id', '==', reward.id)
.get();
const data = ref.docs.map(doc => ({ id: doc.id, ...doc.data() }));
if (!mountedRef.current) return null;
setRedeems(data);
setIsFetching(false);
} catch (error) {
console.log(error);
}
}, [mountedRef]);
useEffect(() => {
fetchSpecificItem();
return () => {
mountedRef.current = false;
};
}, [fetchSpecificItem]);
Create a mutable ref object and set it to true, and during clean-up toggle its value, to ensure that the component has been unmouted.
const mountedRef = useRef(true)
useEffect(() => {
// CALL YOUR API OR ASYNC FUNCTION HERE
return () => { mountedRef.current = false }
}, [])
const [getAllJobs, setgetAlljobs] = useState();
useEffect(() => {
let mounted = true;
axios.get('apiUrl')
.then(function (response) {
const jobData = response.data;
if (mounted) {
setgetAlljobs(jobData)
}
})
.catch(function (error) {
console.log(error.message)
})
return () => mounted = false;
}, [])
set a variable mounted to true->
then if it is true, mount the function->
in the bottom you return it to unmount it
My case was pretty different from what this questions wants. Still I got the same error.
My case was because I had a 'list', which was rendered by using .map from array. And I needed to use .shift. (to remove first item in array)
If array had just one item, it was ok, but since it had 2 of them -> the first one got 'deleted/shifted' and because I used key={index} (while index was from .map), it assumed, that the second item, which later was first, was the same component as the shifted item..
React kept info from the first item (they were all nodes) and so, if that second node used useEffect(), React threw error, that the component is already dismounted, because the former node with index 0 and key 0 had the same key 0 as the second component.
The second component correctly used useEffect, but React assumed, that it is called by that former node, which was no longer on the scene -> resulting in error.
I fixed this by adding different key prop value (not index), but some unique string.
you can wrap any action as a callback inside checkUnmount
const useUnmounted = () => {
const mountedRef = useRef(true);
useEffect(
() => () => {
mountedRef.current = false;
},
[],
);
const checkUnmount = useCallback(
(cb = () => {}) => {
try {
if (!mountedRef.current) throw new Error('Component is unmounted');
cb();
} catch (error) {
console.log({ error });
}
},
[mountedRef.current],
);
return [checkUnmount, mountedRef.current];
};
import React, { useCallback, useEffect, useRef, useState } from "react";
import { userLoginSuccessAction } from "../../../redux/user-redux/actionCreator";
import { IUser } from "../../../models/user";
import { Navigate } from "react-router";
import XTextField from "../../../x-lib/x-components/x-form-controls/XTextField";
import { useDispatch } from "react-redux";
interface Props {
onViewChange?: (n: number) => void;
userInit?: (user: IUser) => void;
}
interface State {
email: string;
password: string;
hasError?: boolean;
errorMessage?: string;
}
const initialValue = {
email: "eve.holt#reqres.in",
password: "cityslicka",
errorMessage: "",
};
const LoginView: React.FC<Props> = (props) => {
const { onViewChange } = props;
const [state, setState] = useState(initialValue);
const mountedRef = useRef(true);
const dispatch = useDispatch();
const handleEmailChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
email: val,
}));
},
[state.email]
);
const handlePasswordChange = useCallback(
(val: string) => {
setState((state) => ({
...state,
password: val,
}));
},
[state.password]
);
const onUserClick = useCallback( async () => {
// HTTP Call
const data = {email: state.email , password: state.password}
try{
await dispatch(userLoginSuccessAction(data));
<Navigate to = '/' />
setState( (state)=>({
...state,
email: "",
password: ""
}))
}
catch(err){
setState( (state)=>({
...state,
errorMessage: err as string
}))
}
},[mountedRef] )
useEffect(()=>{
onUserClick();
return ()=> {
mountedRef.current = false;
};
},[onUserClick]);
const Error = (): JSX.Element => {
return (
<div
className="alert alert-danger"
role="alert"
style={{ width: "516px", margin: "20px auto 0 auto" }}
>
{state.errorMessage}
</div>
);
};
return (
<div>
<div>
email: "eve.holt#reqres.in"
<span style={{ paddingRight: "20px" }}></span> password: "cityslicka"{" "}
</div>
{state.errorMessage && <Error />}
<form className="form-inline">
<div className="form-group">
<XTextField
label="email"
placeholder="E-Posta"
value={state.email}
onChange={handleEmailChange}
/>
</div>
<div className="form-group my-sm-3">
<XTextField
type="password"
label="password"
placeholder="Şifre"
value={state.password}
onChange={handlePasswordChange}
/>
</div>
<button type="button" className="btn btn-primary" onClick = {onUserClick} >
Giriş Et
</button>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(3);
}}
>
Şifremi Unuttum!
</a>
</form>
<p>
Hələdə üye deyilsiniz? <br />
pulsuz registir olmak üçün
<b>
<u>
<a
style={{ fontSize: "18px" }}
href="#"
onClick={(e) => {
e.preventDefault();
onViewChange && onViewChange(2);
}}
>
kilik edin.
</a>
</u>
</b>
</p>
</div>
);
};
export default LoginView;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
For this problem I used a tricky way
first I deploy a state like this
const [routing,setRouting] = useState(false)
then when my works finished I changed it to true
and change my useEffect like this
useEffect(()=>{
if(routing)
navigation.navigate('AnotherPage')
),[routing]}

Resources