How to utilize useEffect hooks to fetch data from Firebase RealTime Database - reactjs

I have an issue where when my user is a new user both my create profile and create characters to FireBase Realtime database are not loading before my user profile page renders. I understand that useEffects run after the render. But after user profile and characters are created in the database I don't have the issue. I can log off and refresh my app, sign in and everything loads in time. Here is m code. I've tried writing my functions inside the useEffect several different ways and I get the same results every time. I saw one post where someone using a .then() but that doesn't appear to work in my situation. I rather not use any additional add-ins like AXIOs or other packages. I feel like there has to be a way to do this with the native built in tools of React and Firebase. Any advice is much appreciated. Edit: Here is my layout.
App.js
<AuthProvider>
<DBProvider>
<Switch>
<PrivateRoute path="/profile" component={ProfileBar} />
<PrivateRoute path="/update-profile" component={UpdateProfile} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
</DBProvider>
</AuthProvider>
</Router>
AuthContext.js
import React, { useContext, useState, useEffect } from 'react'
import { auth} from '../firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function signup(email, password, displayName) {
let promise = new Promise ((resolve, reject) => {
auth.createUserWithEmailAndPassword(email, password)
.then((ref) => {
ref.user.updateProfile({
displayName: displayName
});
resolve(ref);
})
.catch((error) => reject(error));
})
return promise
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
}
DBContext.js
import { db } from '../firebase'
import { useAuth } from './AuthContext'
import React, { useState, useEffect, useContext } from 'react'
const DBContext = React.createContext() // React Database FireStore .DB
export function useDB() {
useContext(DBContext);
}
export function DBProvider({ children }) {
const [profileData, setProfileData] = useState()
const [loading, setLoading] = useState(true)
const { currentUser } = useAuth()
function checkCurrentUser(){
if(currentUser){
checkProfile()
}
if(!currentUser){
setLoading(false)
console.log("No current user logged in.")
}
}
function checkProfile(){
db.ref(`users/` + currentUser.uid + `/profile`)
.on('value', (snapshot) => {
const data = snapshot.val()
if(data == null){
console.log(data, "New user... Generating profile!")
createUserProfile()
}
if(data){
getProfile()
}
});
}
function createUserProfile(){
let profile = {}
profile.gameMaster = false
profile.editor = false
profile.email = currentUser.email
profile.displayName = currentUser.displayName
db.ref('users/' + currentUser.uid).set({
profile
}).then(() =>{
getProfile()
})
}
function getProfile(){
db.ref(`users/` + currentUser.uid + `/profile`)
.on('value', (snapshot) => {
const profile = snapshot.val()
setLoading(false)
setProfileData(profile)
console.log("Profile set to State from Database.")
})
}
useEffect(() => {
checkCurrentUser()
},[])
}
Profile.js
<Switch>
<CharacterProvider>
<Route path={`${match.path}/characters`} component={CharacterSheets} />
<Route path={`${match.path}/journal`} component={Journal} />
<Route path={`${match.path}/game_charts`} component={GameCharts} />
<Route path={`${match.path}/game_rules`} component={GameRules} />
</CharacterProvider>
</Switch>
CharacterContext.js
useEffect(() => {
const ref = db.ref(`users/` + currentUser.uid + `/characters`)
ref.on('value', snapshot => {
const data = snapshot.val()
if(data){
console.log("Setting Characters to State from Database.")
setCharacters(JSON.parse(data))
setLoading(false)
}
if(data == null){
console.log("Setting Characters to State from template.")
setCharacters(characterTemplate)
setLoading(false)
}
})
return () => ref.off();
}, [])
useEffect(() => {
if(characters){
db.ref(`users/` + currentUser.uid).child("/characters").set(JSON.stringify(characters))
}
console.log("Data saved to firebase.")
}, [characters])
CharacterCards.js
import { useCharacter } from '../../../contexts/CharacterContext'
import CharacterCard from './CharacterCard'
import CharacterCardEdit from '../../ProfileContainer/CharacterEdit/CharacterCardEdit'
import SuccessRoller from '../CharacterComponents/SuccessRoller/SuccessRoller'
export default function CharacterCards() {
const { handleCharacterAdd, characters, selectedCharacter, selectedCharacterSuccessRoller } = useCharacter()
return (
<div>
<div className="add_button-container">
<button onClick={handleCharacterAdd} className="add_button-main" >Add Character</button>
</div>
<div className="parent-container">
<div>
{characters?.map(character => {
return (
<CharacterCard key={character.id} {...character} />
)
})
}
</div>
<div>
{selectedCharacter && <CharacterCardEdit character={selectedCharacter} />}
{selectedCharacterSuccessRoller && <SuccessRoller character={selectedCharacterSuccessRoller} />}
</div>
</div>
</div>
)
}

Because your code is sharded out into many functions for readability, there are a lot of listeners that are created but don't get cleaned up. In particular great care needs to be taken with .on listeners as they may be re-fired (you could use .once() to help with this). An example of this bug is in checkProfile() which listens to the user's profile, then calls getProfile() which also listens to the profile. Each time the profile is added, another call to getProfile() is made, adding yet another listener. Plus, each of the listeners in checkProfile() and getProfile() aren't ever cleaned up.
I've made a number of assumptions about your code structure and untangled it so you can read and understand it top-to-bottom. This is especially important when working with React hooks as their order matters.
// firebase.js
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/database";
firebase.initializeApp({ /* ... */ });
const auth = firebase.auth();
const db = firebase.database();
export {
firebase,
auth,
db
}
// AuthContext.js
import { auth } from "./firebase";
import React, { useContext, useEffect, useState } from "react";
const AuthContext = React.createContext();
export default AuthContext;
export function useAuth() { // <- this is an assumption
return useContext(AuthContext);
}
async function signup(email, password, avatarName) {
const userCredential = await auth.createUserWithEmailAndPassword(email, password);
await userCredential.user.updateProfile({
displayName: avatarName
});
return userCredential;
}
export function AuthProvider(props) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
}), []);
return (
<AuthContext.Provider
value={{
currentUser,
loading,
signup
}}
>
{props.children}
</AuthContext.Provider>
);
}
// DBContext.js
import { db } from "./firebase";
import { useAuth } from "./AuthContext";
import React, { useEffect, useState } from "react";
const DBContext = React.createContext();
export default DBContext;
export function DBProvider(props) {
const [profileData, setProfileData] = useState();
const [loading, setLoading] = useState(true);
const { currentUser, loading: loadingUser } = useAuth();
useEffect(() => {
if (loadingUser) {
return; // still initializing, do nothing.
}
if (currentUser === null) {
// no user signed in!
setProfileData(null);
return;
}
// user is logged in
const profileRef = db.ref(`users/` + currentUser.uid + `/profile`);
const listener = profileRef.on('value', snapshot => {
if (!snapshot.exists()) {
// didn't find a profile for this user
snapshot.ref
.set({ // <- this will refire this listener (if successful) with the below data
gameMaster: false,
editor: false,
email: currentUser.email,
displayName: currentUser.displayName
})
.catch((error) => console.error("Failed to initialize default profile", error));
return;
}
setProfileData(snapshot.val());
setLoading(false);
});
return () => profileRef.off('value', listener); // <- cleans up listener
}, [currentUser, loadingUser]);
return (
<DBContext.Provider
value={{
profileData,
loading
}}
>
{props.children}
</DBContext.Provider>
);
}
// CharacterContext.js
import { db } from "./firebase";
import { useAuth } from "./AuthContext";
import React, { useEffect, useState } from "react";
const CharacterContext = React.createContext();
export default CharacterContext;
export function CharacterProvider(props) {
const { currentUser, loading: loadingUser } = useAuth();
const [characters, setCharacters] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
if (loadingUser) {
return; // still initializing, do nothing.
}
if (!currentUser) {
// no user signed in!
setCharacters(null);
return;
}
const charactersRef = db.ref(`users/${currentUser.uid}/characters`);
const listener = charactersRef.on('value', snapshot => {
if (!snapshot.exists()) {
// no character data found, create from template
snapshot.ref
.set(DEFAULT_CHARACTERS); // <- this will refire this listener (if successful)
.catch((error) => console.error("Failed to initialize default characters", error));
return;
}
setCharacters(JSON.parse(snapshot.val()));
setLoading(false);
});
return () => charactersRef.off('value', listener);
}, [currentUser, loadingUser]);
return (
<CharacterContext.Provider
value={{
characters,
loading
}}
>
{props.children}
</CharacterContext.Provider>
);
}

Related

The context api resets and re-evaluates from start when I go to another profile route/ next page

AuthContext.js
import { createContext, useEffect, useState } from "react";
import { axiosInstance } from "../../axiosConfig";
import { useCustomToast } from "../../customHooks/useToast";
const initialState = {
user: null,
isLoggedIn: false,
login: () => null,
logOut: () => null,
};
export const AuthContext = createContext(initialState);
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const { showToast } = useCustomToast();
console.log("i am rinning agaon here");
const checkLogin = async () => {
try {
const res = await axiosInstance.get("/auth/refresh");
setIsLoggedIn(true);
console.log("the user is", res?.data);
setUser(res?.data?.user);
} catch (e) {
console.log(e);
setIsLoggedIn(false);
}
};
const logOutHandler = async () => {
try {
const res = await axiosInstance.get("/auth/logout");
showToast(res?.data?.message);
} catch (e) {
showToast("Something went wrong.Please try again");
}
};
useEffect(() => {
checkLogin();
}, []);
const login = (userData) => {
setUser(userData);
setIsLoggedIn(true);
};
const logOut = () => {
setUser(null);
logOutHandler();
setIsLoggedIn(false);
};
return (
<AuthContext.Provider
value={{
user,
isLoggedIn,
login,
logOut,
}}
>
{children}
</AuthContext.Provider>
);
};
ProtectedRoute.js
import React, { useEffect, useContext } from "react";
import { useRouter } from "next/router";
import { AuthContext } from "../context/authContext";
const ProtectedRoute = ({ children }) => {
const { isLoggedIn } = useContext(AuthContext);
const router = useRouter();
useEffect(() => {
if (!isLoggedIn) {
router.push("/login");
}
}, [isLoggedIn]);
return <>{isLoggedIn && children}</>;
};
export default ProtectedRoute;
I am using NextJS and context api for managing user state. Here at first I will check for tokens and if it is valid I will set loggedIn state to true. But suppose I want to go to profile page which is wrapped by protected route, what is happening is AuthContext is resetting and evaluating itself from beginning, the isLoggedIn state is false when I go to /profile route. If I console log isLoggedIn state inside protectedRoute.js, it is false at start and before it becomes true, that router.push("/login) already runs before isLoggedIn becomes true. It feels like all AuthContext is executing again and again on each route change. Is there any code problem? How can I fix it? The one solution I have found is wrapping that wrapping that if(!loggedIn) statement with setTimeOut() of 1 secs so that until that time loggedIn becomes true from context API

Blank page after refresh react firebase authentication

I have the above firebase db. I want to extract the displayName value and use it in a greeting message after the user is successfully login (e.g. Hello George!). I manage to achieve this but when I refresh the page everything disappears and in console I get this error "index.esm2017.js:1032 Uncaught TypeError: Cannot read properties of undefined (reading 'indexOf')".
Is this a problem of how I extract the displayName from firebase document?
Can someone explain to me what is the problem, please?
Here is my code:
AuthContext.js
import { createContext, useContext, useEffect, useState } from "react";
import {
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
} from "firebase/auth";
import { auth } from "../utils/firebase/firebase.utils";
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({});
const signIn = (email, password) =>
signInWithEmailAndPassword(auth, email, password);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
console.log(currentUser);
setUser(currentUser);
});
return () => unsubscribe();
}, []);
const logOut = () => signOut(auth);
return (
<UserContext.Provider value={{ user, signIn, logOut }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
WelcomePage.jsx
import React, { useState, useEffect } from "react";
import { UserAuth } from "../contexts/AuthContext";
import { db } from "../utils/firebase/firebase.utils";
import { doc, Firestore, getDoc } from "firebase/firestore";
const WelcomePage = () => {
const [userDetails, setUserDetails] = useState({});
const { user } = UserAuth();
useEffect(() => {
const docRef = doc(db, "users", user.uid);
const fetchData = async () => {
try {
const docSnap = await getDoc(docRef);
setUserDetails(docSnap.data());
console.log(docSnap.data());
} catch (e) {
console.log(e);
}
};
fetchData();
}, [user]);
return (
<div>
<h1>Hello, {userDetails.displayName}!</h1>
</div>
);
};
export default WelcomePage;
You might only want to fetch documents in the WelcomePage component if there's a truthy uid value to use.
const { user } = UserAuth();
useEffect(() => {
const fetchData = async () => {
const docRef = doc(db, "users", user.uid);
try {
const docSnap = await getDoc(docRef);
setUserDetails(docSnap.data());
console.log(docSnap.data());
} catch (e) {
console.log(e);
}
};
if (user?.id) {
fetchData();
}
}, [user]);

Component didn't recieve data from state using React/Toolkit and createAsyncThunk

I'm trying to pass data to the component, that i received from the API. I am using 'createAsyncThunk' to save it in the state, than when trying to get my data, get error "undefined". I understand that it happens, cause it's need some time to get data from API, but how i can force component "waiting"? What is wrong with my code?
Here is my code:
Step 1: Gettind data from API, filtered it and push it in state.
import { generateRandom } from "../helpers/randomInt";
const API_URL = "https://akabab.github.io/superhero-api/api/all.json";
export const fetchHeroes = createAsyncThunk(
"data_slice/fetchHeroes",
async function (_, { rejectWithValue }) {
try {
const res = await fetch(API_URL);
if (!res.ok) {
throw new Error("Could not fetch cart data!");
}
const data = await res.json();
const marvel_heroes = data.filter(
(item) => item.biography.publisher == "Marvel Comics"
);
const dark_horse_heroes = data.filter(
(item) => item.biography.publisher == "Dark Horse Comics"
);
const dc_heroes = data.filter(
(item) => item.biography.publisher == "DC Comics"
);
const filtered_data = [
...marvel_heroes,
...dark_horse_heroes,
...dc_heroes,
];
const heroesData = [];
for (let index = 0; index < 49; index++) {
const item = filtered_data[generateRandom(0, 439)];
heroesData.push(item);
}
const main_data = [filtered_data, heroesData];
return main_data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const heroesSlice = createSlice({
name: "data_slice",
initialState: { heroes_data: [], isLoading: null, error: null },
extraReducers: {
[fetchHeroes.pending]: (state) => {
state.isLoading = true;
},
[fetchHeroes.fulfilled]: (state, action) => {
state.heroes_data = action.payload;
state.isLoading = false;
},
[fetchHeroes.rejected]: (state) => {
state.isLoading = false;
state.error = "Something go wrong!";
alert("aaa");
},
},
});
export default heroesSlice;
Step 2: Firing (using dispatch) fetch function "fetchHeroes" in 'App.js' with 'UseEffect' to get data when app starting
import { Fragment, useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchHeroes } from "./store/heroes-slice";
import { Routes, Route } from "react-router-dom";
import Main from "./pages/Main";
import Heroes from "./pages/Heroes";
import Hero_page from "./pages/Hero_page";
import LoginModal from "./components/LoginModal";
import RegisterModal from "./components/RegisterModal";
function App() {
const [scrollY, setScrollY] = useState(0);
const isLoginModal = useSelector((state) => state.modal.isLoginModal);
const isRegisterModal = useSelector((state) => state.modal.isRegisterModal);
const dispatch = useDispatch();
function logit() {
setScrollY(window.scrollY);
console.log(new Date().getTime());
}
useEffect(() => {
function watchScroll() {
window.addEventListener("scroll", logit);
}
watchScroll();
return () => {
window.removeEventListener("scroll", logit);
};
});
useEffect(() => {
dispatch(fetchHeroes());
}, [dispatch]);
return (
<Fragment>
{isRegisterModal && <RegisterModal></RegisterModal>}
{isLoginModal && <LoginModal></LoginModal>}
<Routes>
<Route path="/" element={<Main />} />
<Route path="/heroes" exact element={<Heroes scroll={scrollY} />} />
<Route path="/heroes/:heroId" element={<Hero_page />}></Route>
</Routes>
</Fragment>
);
}
export default App;
Step 3: I am trying to recieve data from state(heroes_fetched_data) using 'usSelector', but when trying parce it through 'map', get error 'undefined'
import classes from "./Heroes.module.css";
import Header from "../components/Header";
import Footer from "../components/Footer.js";
import Hero_card from "../components/Hero_card";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
export default function Heroes(props) {
const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
const loadingStatus = useSelector((state) => state.heroes.isLoading);
console.log(heroes_fetched_data);
const heroes_cards = heroes_fetched_data[1].map((item, i) => (
<Link to={`/heroes/${item.id}`} key={item.id + Math.random()}>
<Hero_card
key={i}
img={item.images.lg}
name={item.name}
publisher={item.biography.publisher}
/>
</Link>
));
return (
<div className={classes.main}>
<Header scroll={props.scroll} />
{!loadingStatus && (
<section className={classes.heroes}>
<ul className={classes.ully} id="heroes">
{heroes_cards}
</ul>
</section>
)}
{loadingStatus && <p>Loading...</p>}
<Footer />
</div>
);
}
Because the fetch is asynchronous, you cannot assume that heroes_fetched_data inside your Heroes component will have the data when the component first renders. You need to check whether this data is present before attempting to use it. If it's not yet present, the component should render an alternate "loading" state. When the fetch completes, your component should re-render automatically, at which point heroes_fetched_data will have the data you want and you can proceed.
Roughly, you want something like this pseudocode:
export default function Heroes(props) {
const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
const loadingStatus = useSelector((state) => state.heroes.isLoading);
if (!heroes_fetched_data) {
return <p>{loadingStatus}</p>;
}
const heroes_cards = heroes_fetched_data[1].map((item, i) => (
// ...
);
// proceed as normal
}

Weird React Native Behavior

I have been building this mobile app with React Native/Expo and Firestore for a while now. The app schedules study sessions, and when a study session is active, a Pomodoro timer screen is to be shown, and when a session is inactive, the main homepage should be shown. However, I have been having trouble implementing this after a refactor to my database structure. Currently, for each schedule, a document is created in a subcollection corresponding to the user's UID. So, the path for a schedule would be "Users/(auth.currentUser.uid)/S-(auth.currentUser.uid)/(document id). To implement this feature, I have tried to run a function every second that checks through all of the documents and finds out whether a schedule is active, and if it is, it shows the Pomodoro timer screen. However, there is some weird behavior occurring. I am reading the database once using a Context Api, and the data shows perfectly in the screen where you view all your schedules, however in the function it is showing as an empty object. I have a feeling that it might be due to the bounds of the Context, however I am not sure. Does anyone know why?
CurrentDataProvider.js
import React, { createContext, useEffect, useState } from "react";
import {
doc,
getDocs,
onSnapshot,
collection,
query,
} from "firebase/firestore";
import { db, auth } from "../config/firebase";
export const CurrentDataContext = createContext({});
const CurrentDataProvider = ({ children }) => {
const [data, setData] = useState({});
useEffect(async () => {
if (auth.currentUser) {
const ref = query(
collection(
db,
"Users",
auth.currentUser.uid,
`S-${auth.currentUser.uid}`
)
);
const unsub = onSnapshot(ref, (querySnap) => {
let dat = {};
querySnap.forEach((doc) => {
dat[doc.id] = doc.data();
});
setData(dat);
});
return () => unsub;
}
}, []);
return (
<CurrentDataContext.Provider value={{ data, setData }}>
{children}
</CurrentDataContext.Provider>
);
};
export { CurrentDataProvider };
function being used to read schedules
const readSchedules = () => {
const currentTime = new Date();
Object.keys(data).forEach((key) => {
const clientSeconds =
currentTime.getHours() * 3600 + currentTime.getMinutes() * 60;
const startTimestamp = new Timestamp(data[key]["start"]["seconds"]);
const endTimestamp = new Timestamp(data[key]["end"].seconds);
const utcStartSeconds = startTimestamp.seconds;
const utcEndseconds = endTimestamp.seconds;
console.log(utcStartSeconds, clientSeconds, utcEndseconds);
const greaterTime = clientSeconds > utcStartSeconds;
const lessTime = clientSeconds < utcEndseconds;
const trueDay = data[key][dayOfWeekAsString(currentTime.getDay())];
if (trueDay) {
if (greaterTime && lessTime) {
setPomodoro(true);
setCurrentSchedule(key.toString());
console.log(`Schedule ${currentSchedule} selected!`);
return;
}
}
});
setPomodoro(false);
};
RootStack.js
import SplashScreen from "../screens/SplashScreen";
import AuthStack from "./AuthStack";
import React, { useState, useContext, useEffect } from "react";
import { View, ActivityIndicator } from "react-native";
import { auth } from "../config/firebase";
import { onAuthStateChanged } from "firebase/auth";
import { UserContext } from "./../components/UserProvider";
import { NavigationContainer } from "#react-navigation/native";
import FinalStack from "./MainStack";
import { CurrentDataProvider } from "../components/CurrentDataProvider";
const RootStack = () => {
const { user, setUser } = useContext(UserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const authListener = auth.onAuthStateChanged(async (user) => {
try {
await (user ? setUser(user) : setUser(null));
setTimeout(() => {
setIsLoading(false);
}, 3000);
} catch (err) {
console.log(err);
}
});
return authListener;
}, []);
if (isLoading) {
return <SplashScreen />;
}
return (
<NavigationContainer>
{user ? (
<CurrentDataProvider>
<FinalStack />
</CurrentDataProvider>
) : (
<AuthStack />
)}
</NavigationContainer>
);
};
export default RootStack;
Thanks for all your help!

Canceling an Axios REST call in React Hooks useEffects cleanup failing

I'm obviously not cleaning up correctly and cancelling the axios GET request the way I should be. On my local, I get a warning that says
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
On stackblitz, my code works, but for some reason I can't click the button to show the error. It just always shows the returned data.
https://codesandbox.io/s/8x5lzjmwl8
Please review my code and find my flaw.
useAxiosFetch.js
import {useState, useEffect} from 'react'
import axios from 'axios'
const useAxiosFetch = url => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
let source = axios.CancelToken.source()
useEffect(() => {
try {
setLoading(true)
const promise = axios
.get(url, {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log(`request cancelled:${thrown.message}`)
} else {
console.log('another error happened')
}
})
.then(a => {
setData(a)
setLoading(false)
})
} catch (e) {
setData(null)
setError(e)
}
if (source) {
console.log('source defined')
} else {
console.log('source NOT defined')
}
return function () {
console.log('cleanup of useAxiosFetch called')
if (source) {
console.log('source in cleanup exists')
} else {
source.log('source in cleanup DOES NOT exist')
}
source.cancel('Cancelling in cleanup')
}
}, [])
return {data, loading, error}
}
export default useAxiosFetch
index.js
import React from 'react';
import useAxiosFetch from './useAxiosFetch1';
const index = () => {
const url = "http://www.fakeresponse.com/api/?sleep=5&data={%22Hello%22:%22World%22}";
const {data,loading} = useAxiosFetch(url);
if (loading) {
return (
<div>Loading...<br/>
<button onClick={() => {
window.location = "/awayfrom here";
}} >switch away</button>
</div>
);
} else {
return <div>{JSON.stringify(data)}xx</div>
}
};
export default index;
Here is the final code with everything working in case someone else comes back.
import {useState, useEffect} from "react";
import axios, {AxiosResponse} from "axios";
const useAxiosFetch = (url: string, timeout?: number) => {
const [data, setData] = useState<AxiosResponse | null>(null);
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let unmounted = false;
let source = axios.CancelToken.source();
axios.get(url, {
cancelToken: source.token,
timeout: timeout
})
.then(a => {
if (!unmounted) {
// #ts-ignore
setData(a.data);
setLoading(false);
}
}).catch(function (e) {
if (!unmounted) {
setError(true);
setErrorMessage(e.message);
setLoading(false);
if (axios.isCancel(e)) {
console.log(`request cancelled:${e.message}`);
} else {
console.log("another error happened:" + e.message);
}
}
});
return function () {
unmounted = true;
source.cancel("Cancelling in cleanup");
};
}, [url, timeout]);
return {data, loading, error, errorMessage};
};
export default useAxiosFetch;
Based on Axios documentation cancelToken is deprecated and starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
//...
React.useEffect(() => {
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
return () => {
controller.abort();
};
}, []);
//...
The issue in your case is that on a fast network the requests results in a response quickly and it doesn't allow you to click the button. On a throttled network which you can achieve via ChromeDevTools, you can visualise this behaviour correctly
Secondly, when you try to navigate away using window.location.href = 'away link' react doesn't have a change to trigger/execute the component cleanup and hence the cleanup function of useEffect won't be triggered.
Making use of Router works
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'
import useAxiosFetch from './useAxiosFetch'
function App(props) {
const url = 'https://www.siliconvalley-codecamp.com/rest/session/arrayonly'
const {data, loading} = useAxiosFetch(url)
// setTimeout(() => {
// window.location.href = 'https://www.google.com/';
// }, 1000)
if (loading) {
return (
<div>
Loading...
<br />
<button
onClick={() => {
props.history.push('/home')
}}
>
switch away
</button>
</div>
)
} else {
return <div>{JSON.stringify(data)}</div>
}
}
ReactDOM.render(
<Router>
<Switch>
<Route path="/home" render={() => <div>Hello</div>} />
<Route path="/" component={App} />
</Switch>
</Router>,
document.getElementById('root'),
)
You can check the demo working correctly on a slow network
Fully cancellable routines example, where you don't need any CancelToken at all (Play with it here):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios"; // cancellable axios wrapper
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
console.log("mount");
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield cpAxios(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
setText(`Failed: ${err}`);
}
return () => {
console.log("unmount");
};
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Abort</button>
</div>
);
}
This is how I do it, I think it is much simpler than the other answers here:
import React, { Component } from "react";
import axios from "axios";
export class Example extends Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
axios.get("/data").then((res) => {
if (this._isMounted && res.status === 200) {
// Do what you need to do
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return <div></div>;
}
}
export default Example;

Resources