I'm fetching Dogs from my API through a JavaScript timeout. It works fine, except it fails to clear the timeout sometimes:
import { useState, useEffect, useCallback } from 'react';
const DogsPage = () => {
const [dogs, setDogs] = useRef([]);
const timeoutId = useRef();
const fetchDogs = useCallback(
async () => {
const response = await fetch('/dogs');
const { dogs } = await response.json();
setDogs(dogs);
timeoutId.current = setTimeout(fetchDogs, 1000);
},
[]
);
useEffect(
() => {
fetchDogs();
return () => clearTimeout(timeoutId.current);
},
[fetchDogs]
);
return <b>Whatever</b>;
};
It looks like the problem is that sometimes I unmount first, while the code is still awaiting for the Dogs to be fetched. Is this a common issue and if so, how would I prevent this problem?
One idea would be to use additional useRef() to keep track of whether the component has been unmounted in between fetch:
const DogsPage = () => {
const isMounted = useRef(true);
const fetchDogs = useCallback(
async () => {
// My fetching code
if (isMounted.current) {
timeoutId.current = setTimeout(fetchDogs, 1000);
}
},
[]
);
useEffect(
() => {
return () => isMounted.current = false;
},
[]
);
// The rest of the code
};
But perhaps there is a cleaner way?
You can assign a sentinel value for timeoutId.current after clearing it, then check for that value before starting a new timer:
import { useState, useEffect, useCallback } from 'react';
const DogsPage = () => {
const [dogs, setDogs] = useRef([]);
const timeoutId = useRef();
const fetchDogs = useCallback(
async () => {
const response = await fetch('/dogs');
const { dogs } = await response.json();
setDogs(dogs);
if (timeoutId.current !== -1)
timeoutId.current = setTimeout(fetchDogs, 1000);
},
[]
);
useEffect(
() => {
fetchDogs();
return () => void (clearTimeout(timeoutId.current), timeoutId.current = -1);
},
[fetchDogs]
);
return <b>Whatever</b>;
};
Related
I am trying to execute a function to update a setState but it as well needs other state to load first.
const [chatsIds, setChatsIds] = useState([]);
const [chats, setChats] = useState([]);
useEffect(() => {
getChatsIds();
}, []);
useEffect(() => {
getChats();
}, [chats]);
the "getChats" needs the value from "chatsIds" but when the screen is loaded the value isn't , only when i reload the app again it gets the value.
Here are the functions :
const getChatsIds = async () => {
const ids = await getDoc(userRef, "chats");
setChatsIds(ids);
}
const getChats = async () => {
const chatsArr = [];
chatsIds.forEach(async (id) => {
const chat = await getDoc(doc(db, "Chats", id));
chatsArr.push(chat);
console.log(chatsArr);
});
setChats(chatsArr);
}
I've tried with the useEffect and useLayoutEffect hooks, with promises and async functions, but i haven't found what i'm doing wrong :(
The problem is in your useEffect hook dependency. It should depends on chatsIds not chats.
useEffect(() => {
getChats();
}, [chatsIds]);
Which mean fetching chatsIds should depend on first mount and fetching chats should depend on if chatsIds is chnaged.
You simply change the useEffect hook to like below.
useEffect(() => {
getChatsIds();
}, [chatsIds]);
I Think getChat() is depend on chatIds...
so you use useEffect with chatIds on dependency
const [chatsIds, setChatsIds] = useState([]);
const [chats, setChats] = useState([]);
useEffect(() => {
getChatsIds();
}, []);
useEffect(() => {
getChats(chatsIds);
}, [chatsIds]);
const getChatsIds = async () => {
const ids = await getDoc(userRef, "chats");
setChatsIds(ids);
}
const getChats = async (chatsIds) => {
const chatsArr = [];
chatsIds.forEach(async (id) => {
const chat = await getDoc(doc(db, "Chats", id));
chatsArr.push(chat);
console.log(chatsArr);
});
setChats(chatsArr);
}
The code below is a simplified example of my problem. There is a lot more going on in the actual codebase, so let's just assume that my useHook function must be asynchronous and we cannot just fetch the data inside the useEffect hook.
It currently renders {}, but I want it to render "Data to be displayed"
const fetch = async () => {
/* This code can't be changed */
return "Data to be displayed"
}
const useFetch = async () => { // This function must be asynchronous
let data = await fetch();
return data;
};
const App = () => {
const data = useFetch();
const [state, setState] = useState(data);
useEffect(() => {
setState(data);
}, [data]);
return <h1>{JSON.stringify(state)}</h1>
}
export default App;
change
const data = useFetch(); to const data = await useFetch();
Move called inside the useEffect like this:
const App = () => {
const [state, setState] = useState({});
useEffect(() => {
const fetchData = async () => {
const data = await useFetch();
setState(data);
}
fetchData()
}, []); // [] load first time
return <h1>{JSON.stringify(state)}</h1>
}
Sample react code what I am currently using...
I want to abort the request as soon as I make another request using debouncing.
Right now it cancels the very request it is making even for the first time.
import "./App.css";
import React, { useCallback, useState } from "react";
function App() {
const [mesg, setMesg] = useState(0);
const [pin, setPin] = useState("");
const abortCon = new AbortController();
const signal = abortCon.signal;
const debounce = (fn, timer) => {
let time;
return function () {
let arg = arguments;
let context = this;
if (time) clearTimeout(time);
time = setTimeout(() => {
fn.apply(context, arg);
time = null;
}, timer);
};
};
const onChangeHandler = (val) => {
const url = "https://jsonplaceholder.typicode.com/todos/1";
console.log(val);
if (abortCon) abortCon.abort();
fetch(url, { signal })
.then((result) => {
return result.json();
})
.then((res) => {
// const result = await res.json();
console.log(res.title);
setPin(val);
setMesg((prev) => prev + 1);
})
.catch((e) => console.log(e));
};
// const newHandler = debounce(onChangeHandler, 400);
const newHandler = useCallback(debounce(onChangeHandler, 200), []);
return (
<div className="App">
<p>{mesg}</p>
<input
type="text"
placeholder="PIN code"
value={pin}
onChange={(e) => {
setPin(e.target.value);
newHandler(e.target.value);
}}
/>
</div>
);
}
export default App;
I want to abort the request as soon as I make another request using debouncing.
Right now it cancels the very request it is making even for the first time.
The problem is that you are not making a new Abort Controller for each request.
So instead we save a ref to the AbortController using useRef(). Then create a new AbotrController and assign it to the ref at the start of each request.
Please see the updated code below:
import "./App.css";
import React, { useCallback, useRef, useState } from "react";
function App() {
const [mesg, setMesg] = useState(0);
const [pin, setPin] = useState("");
const abortConRef = useRef();
const debounce = (fn, timer) => {
let time;
return function () {
let arg = arguments;
let context = this;
if (time) clearTimeout(time);
time = setTimeout(() => {
fn.apply(context, arg);
time = null;
}, timer);
};
};
const onChangeHandler = (val) => {
const url = "https://jsonplaceholder.typicode.com/todos/1";
console.log(val);
if (abortConRef.current) abortConRef.current.abort();
abortConRef.current = new AbortController();
fetch(url, { signal: abortConRef.current.signal })
.then((result) => {
return result.json();
})
.then((res) => {
// const result = await res.json();
console.log(res.title);
// setPin(val);
setMesg((prev) => prev + 1);
})
.catch((e) => console.log(e));
};
// const newHandler = debounce(onChangeHandler, 400);
const newHandler = useCallback(debounce(onChangeHandler, 200), []);
return (
<div className="App">
<p>{mesg}</p>
<input
type="text"
placeholder="PIN code"
value={pin}
onChange={(e) => {
setPin(e.target.value);
newHandler(e.target.value);
}}
/>
</div>
);
}
export default App;
https://codesandbox.io/s/serene-wright-f85hy?file=/src/App.js
Edit: I also commented out the setPin in the response callback. It was making the input buggy.
In many components, I need to fetch some data and I'm ending up with a lot of similar code. It looks like this:
const [data, setData] = useState();
const [fetchingState, setFetchingState] = useState(FetchingState.Idle);
useEffect(
() => {
loadDataFromServer(props.dataId);
},
[props.dataId]
);
async function loadDataFromServer(id) {
let url = new URL(`${process.env.REACT_APP_API}/data/${id}`);
let timeout = setTimeout(() => setFetchingState(FetchingState.Loading), 1000)
try {
const result = await axios.get(url);
setData(result.data);
setFetchingState(FetchingState.Idle);
}
catch (error) {
setData();
setFetchingState(FetchingState.Error);
}
clearTimeout(timeout);
}
How can I put it into a library and reuse it?
Thank you guys for the suggestion, I came up with the following hook. Would be happy to some critics.
function useFetch(id, setData) {
const [fetchingState, setFetchingState] = useState(FetchingState.Idle);
useEffect(() => { loadDataFromServer(id); }, [id]);
async function loadDataFromServer(id) {
let url = new URL(`${process.env.REACT_APP_API}/data/${id}`);
let timeout = setTimeout(() => setFetchingState(FetchingState.Loading), 1000)
try {
const result = await axios.get(url);
setData(result.data);
setFetchingState(FetchingState.Idle);
}
catch (error) {
setData();
setFetchingState(FetchingState.Error);
}
clearTimeout(timeout);
}
return fetchingState;
}
And this is how I use it:
function Thread(props) {
const [question, setQuestion] = useState();
const fetchingState = useFetch(props.questionId, setQuestion);
if (fetchingState === FetchingState.Error) return <p>Error while getting the post.</p>;
if (fetchingState === FetchingState.Loading) return <Spinner />;
return <div>{JSON.stringify(question)}</div>;
}
You can wrap your APIs calls in /services folder and use it anywhere
/services
- Auth.js
- Products.js
- etc...
Example
Auth.js
import Axios from 'axios';
export const LoginFn = (formData) => Axios.post("/auth/login", formData);
export const SignupFn = (formData) => Axios.post("/auth/signup", formData);
export const GetProfileFn = () => Axios.get("/auth/profile")
in your component
import React, { useState } from 'react'
import { LoginFn } from '#Services/Auth'
export LoginPage = () => {
const [isLoading, setIsLoading] = useState(false);
const LoginHandler = (data) => {
setIsLoading(true)
LoginFn(data).then(({ data }) => {
// do whatever you need
setIsLoading(false)
})
}
return (
<form onSubmit={LoginHandler}>
.......
)
}
const [timer,setTimer] = useState()
const [number, setNumber] = useState()
const [list, setlist] = useState([])
const numberChange = (number)=>{
setNumber(number)
if (!(list.find(item=>item===number))){
setlist([...list,number])}
}
const randomNumber=()=> 1+Math.floor(Math.random()*90)
const randNumberChange=()=>{
let randNumber = randomNumber()
if (list.find(item=>item===randNumber))
randNumberChange()
else
numberChange(randNumber)
}
const startTimer = () => {
setTimer(setInterval(()=>{
randNumberChange()
}, 5000))
}
const stopTimer=()=>{
clearInterval(timer)
}
The list is always rendering only one item and not appending it.
When randNumberChange is called separately then the list gets appended but not with setInterval.
When startTimer funcion is executed is stopped with stopTimer and then started again it appends second item then stop and it repeats
Change setlist([...list,number])} to setlist((prevState) => [...prevState, number]). React set state is async in nature. So to get the correct list value from the state you would need to get the value from previous state. Doc
Suggestion: that instead of setting timer in state, you can start the interval in useEffect.
Also in numberChange function, you should get the list from previous state and then append the new number in that. This will make sure that the list value is updated before adding new number.
import React, { Component, useState } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
const Test = () => {
const [number, setNumber] = useState(null);
const [list, setlist] = useState([]);
const numberChange = number => {
setNumber(number);
if (!list.find(item => item === number)) {
setlist((prevState) => [...prevState, number]);// instead of directly using list value, get it from previous state
}
};
const randomNumber = () => 1 + Math.floor(Math.random() * 90);
const randNumberChange = () => {
console.log("here");
let randNumber = randomNumber();
if (list.find(item => item === randNumber)) randNumberChange();
else numberChange(randNumber);
};
const startTimer = () => {
return setInterval(() => { randNumberChange(); }, 5000);
}
const stopTimer = (timer) => {
clearInterval(timer)
}
React.useEffect(() => {
const timer = startTimer();
return ()=> stopTimer(timer);
}, []);
console.log(list);
return <div>{number}</div>;
};
You'll need to use useEffect hook:
useEffect(() => {
// You don't need timer state, we'll clear this later
const interval = setInterval(() => {
randNumberChange()
},5000)
return () => { // clear up
clearInterval(interval)
}
},[])
use useEffect with setTimeout it is work as setInterval
useEffect(() => {
setTimeout(() => setList([...list, newValue]), 2000)
}, [list])