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);
}
Related
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>;
};
const [searchText, setSearchText] = useState('')
const searchHandler = async (searchTextInput) => {
console.log('search is:::', searchTextInput)
setSearchText(() => searchTextInput)
}
const searchOnSubmitHandler = async () => {
await loadShopsCount()
await loadShops()
}
const searchClearHandler = async () => {
setSearchText(() => "")
console.log('onClear-----------search is:::', searchText)
await loadShopsCount()
}
On calling searchClearHandler after searching for something, searchText returns the previous state value instead of empty string. Hence, my component is not able to re-render.
How can I set searchText to empty string and re-render the component on calling searchClearHandler?
Use useEffect.
useEffect(() => {
Here your function which you want to call.
}, [searchText]);
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>
}
I'm struggling to figure out how to perform this:
const [stateOne, setStateOne] = useState();
const [stateTwo, setStateTwo] = useState();
useEffect(() => {
/* fetch data */
setStateOne(); /* not before data is fetched */
setStateTwo(); /* not before data is fetched and setStateOne is complete */
},[])
Is this conceptually right and it is possible to run such tasks asynchronously within useEffect?
Multiple effects:
const [asyncA,setAsyncA] = useState();
const [asyncB,setAsyncB] = useState();
useEffect(() => {
(async() => {
setAsyncA(await apiCall());
})();
// on mount fetch your data - no dependencies
},[]);
useEffect(() => {
if(!asyncA) return;
(async() => {
setAsyncB(await apiCall(asyncA));
})();
// when asyncA is ready, then get asyncB
},[asyncA]);
useEffect(() => {
if(!asyncA || !asyncB) return;
// both are ready, do something
},[asyncA,asyncB])
OR, just an async function in one effect:
useEffect(() => {
(async() => {
const first = await apiCallA();
const second = await apiCallB(first);
})();
},[]);
You can't run async actions in a react hook, so you need to extract your functionality outside the hook and then call it inside the hook, then create a second effect to run after stateOne is updated to update state 2.
const fetchAction = async () => {
await fetchData(...)/* fetch data */
setStateOne(); /* not before data is fetched */
}
useEffect(() => {
},[])
useEffect(() => {
setStateTwo();
},[StateOne])
This would give you an idea of how to use useEffect
const {useState, useEffect} = React;
const App = () => {
const [stateOne, setStateOne] = useState(null)
const [stateTwo, setStateTwo] = useState(null)
useEffect(() => {
stateOne && call(setStateTwo);
},[stateOne])
const call = setState => {
let called = new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, 2000)
})
called.then(res => setState(res))
}
return (
<div>
<button onClick={() => call(setStateOne)}>make call</button>
<pre>stateOne: {JSON.stringify(stateOne)}</pre>
<pre>stateTwo: {JSON.stringify(stateTwo)}</pre>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
use async/await with the direct arrow function
useEffect(() => {
(async() => {
/* fetch data */
// await is holding process till you data is fetched
const data = await fetchData()
// then set data on state one
await setStateOne();
// then set data on state second
setStateTwo();
})();
},[])
I tried to create a function for fetching data from the server, and it works.
But I am not sure if that the right way?
I created a function component to fetching data, using useState, useEffect and Async/Await :
import React, { useState, useEffect } from "react";
const Fetch = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
let res = await fetch(
"https://api.coindesk.com/v1/bpi/currentprice.json" //example and simple data
);
let response = await res.json();
setData(response.disclaimer); // parse json
console.log(response);
};
fetchData();
}, []);
return <div>{data}</div>;
};
export default Fetch; // don't run code snippet, not working, this component must be imported in main
Where I am not sure is a place where to call the fetchData function. I do that inside useEffect? Right place? And, this call will happen only one? Because i use []?
Generally, how would you do something like this?
Overall, you are heading in the right direction. For fetching data, you'd wanna use useEffect and pass [] as a second argument to make sure it fires only on initial mount.
I believe you could benefit from decoupling fetchJson function and making it more generic, as such:
const fetchJson = async (url) => {
const response = await fetch(url);
return response.json();
};
const Fetch = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetchJson("https://api.coindesk.com/v1/bpi/currentprice.json")
.then(({ disclaimer }) => setData(disclaimer));
}, []);
return <div>{data}</div>;
};
Another option is to use a self invoking function:
const Fetch = () => {
const [data, setData] = useState(null);
useEffect(() => {
(async () => {
let res = await fetch(
"https://api.coindesk.com/v1/bpi/currentprice.json" //example and simple data
);
let response = await res.json();
setData(response);
})();
}, []);
return <div>{data}</div>;
};
The suggestion to separate out the fetch logic to a separate function is a good idea and can be done as follows:
const Fetch = () => {
const [data, setData] = useState(null);
useEffect(() => {
(async () => {
let response= await fetchData("https://api.coindesk.com/v1/bpi/currentprice.json");
setData(response);
})();
}, []);
return <div>{data}</div>;
};
const fetchData = async (url) => {
const response = await fetch(url);
const json = await response.json();
return json;
};
And yet another option is to create a wrapper function around useEffect that triggers the async function for you similar to this:
export function useAsyncEffect(effect: () => Promise<any>) {
useEffect(() => {
effect().catch(e => console.warn("useAsyncEffect error", e));
});
}