I have this exported const in one file useLocation.tsx where I get the user's location and retrieve the user's county, state/province, and country. I also have an exported const in another file useCountryData.tsx where I fetch the COVID cases and deaths from an API. There is a variable in useLocation.tsx that is called countryNameshort. How do I use this variable in useCountryData.tsx?
useLocation.tsx
export const useLocation = () => {
var [stateName, setstateName] = useState(String);
var [countyName, setCountyName] = useState(String);
var [countryName, setCountryName] = useState(String);
var [stateNameshort, setstateNameshort] = useState(String);
var [countryNameshort, setCountryNameshort] = useState(String);
const [latitude, setlatitude] = useState(Number);
const [longitude, setlongitude] = useState(Number);
const [location, setLocation] = useState(Object);
const [errorMsg, setErrorMsg] = useState(String);
useEffect(() => {
(async () => {
if (Platform.OS === "android" && !Constants.isDevice) {
setErrorMsg(
"Oops, this will not work on Snack in an Android emulator. Try it on your device!"
);
return;
}
let { status } = await Location.requestPermissionsAsync();
if (status !== "granted") {
setErrorMsg("Permission to access location was denied");
return;
}
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
const latitude = location.coords.latitude;
setlatitude(latitude);
const longitude = location.coords.longitude;
setlongitude(longitude);
})();
}, []);
let text = "Waiting..";
if (errorMsg) {
text = errorMsg;
} else if (location) {
text = JSON.stringify(location);
}
fetch(
"https://maps.googleapis.com/maps/api/geocode/json?address=" +
latitude +
"," +
longitude +
"&key=" +
apiKey
)
.then((response) => response.json())
.then((responseJson) => {
const resState = responseJson.results[0].address_components.filter(
(x: any) =>
x.types.filter((t: Object) => t == "administrative_area_level_1")
.length > 0
)[0].long_name;
setstateName(resState);
const resCounty = responseJson.results[0].address_components.filter(
(x: any) =>
x.types.filter((t: Object) => t == "administrative_area_level_2")
.length > 0
)[0].long_name;
setCountyName(resCounty);
const resCountry = responseJson.results[0].address_components.filter(
(x: any) => x.types.filter((t: Object) => t == "country").length > 0
)[0].long_name;
setCountryName(resCountry);
const resStateShort = responseJson.results[0].address_components.filter(
(x: any) =>
x.types.filter((t: Object) => t == "administrative_area_level_1")
.length > 0
)[0].short_name;
setstateNameshort(resStateShort);
const resCountryShort = responseJson.results[0].address_components.filter(
(x: any) => x.types.filter((t: Object) => t == "country").length > 0
)[0].short_name;
setCountryNameshort(resCountryShort);
if (countryNameshort === "US") {
countryNameshort = "US" + "A";
}
})
.catch((err) => {
console.log(err);
});
return { countryName, countyName, stateName, stateNameshort, countryNameshort };
};
useCountryData.tsx
import { useLocation } from './useLocation';
export const useCountryData = () => {
const [earliest2, setEarliest2] = useState([]);
const [countryDeaths, setcountryDeaths] = useState(Number);
const [countryCases, setcountryCases] = useState(Number);
useEffect(() => {
axios
.get("https://coronavirus-19-api.herokuapp.com/countries")
.then((response) => {
setEarliest2(response.data);
const countryArray = response.data.filter(
(item) => item.country === props.countryNameshort //???
);
const resCountryDeaths = countryArray[0].deaths;
setcountryDeaths(resCountryDeaths);
const resCountryCases = countryArray[0].cases;
setcountryCases(resCountryCases);
console.log("hiiii", countryCases);
})
.catch((err) => {
console.log(err);
});
}, []);
return { countryCases, countryDeaths };
};
CountryCard.tsx
const CountryCard = (props) => {
const mappedLocation = useMappedLocation();
const countryName = mappedLocation.country;
return (
<RectButton style={[styles.container, { backgroundColor: "white" }]}>
<Text style={[styles.textLocation, { top: 15, left: 10 }]}>
{countryName} /???
</Text>
)
}
Here is a pseudo-code suggestion for how you could refactor these stages, without adopting useEffect and useState for operations that are more conventionally just async, followed with a 'hook-style' pattern which does useState and useEffect to make the async results available to your UI. There is no chance on earth that this code will run as I don't have access to your environment to really try it, but it gives you an idea of how it might be refactored. If the state needs to be consumed by multiple parts of your UI, then makes sense for the useMappedLocation hook to assign a mappedLocation variable in an ancestor component, with the result passed down to descendants through Context, Composition or Props. This will have the effect of caching the result.
I've also sketched out how a second hook might consume the first hook as I think having re-read your question that was the point you got stuck with your original approach. However, embedding the useMappedLocation hook in multiple places will cause it to be re-executed multiple times and will not benefit from caching compared to hoisting it into an ancestor component.
const apikey = "myapikey";
interface GeoEntry {
address_components:[
{types:("country"|"administrative_area_level_1")[]
short_name:string,
long_name:string
}
]
}
interface MappedLocation {
state:string,
country:string
}
async function getLocation(){
return await Location.getCurrentPositionAsync({});
}
async function getFirstGeoEntry() : Promise<GeoEntry>{
const {latitude,longitude} = await getLocation();
const response = await fetch(
"https://maps.googleapis.com/maps/api/geocode/json?address=" +
latitude +
"," +
longitude +
"&key=" +
apikey
)
const json = await response.json();
return json.results[0]
}
function getStateNameLong(geoEntry:GeoEntry){
return geoEntry.address_components.filter(
(x: any) =>
x.types.filter((t: Object) => t == "administrative_area_level_1")
.length > 0
)[0].long_name
}
function getCountryNameShort(geoEntry:GeoEntry){
return geoEntry.address_components.filter(
(x: any) => x.types.filter((t: Object) => t == "country").length > 0
)[0].short_name
}
async function getMappedLocation() : Promise<MappedLocation>{
const geoEntry = await getFirstGeoEntry();
return {
country:getCountryNameShort(geoEntry),
state:getStateNameLong(geoEntry),
}
}
const useMappedLocation = () => {
const [mappedLocation,setMappedLocation] = useState<MappedLocation>(null);
useEffect(() => {
(async () => {
setMappedLocation(await getMappedLocation())
})()
}, [])
return mappedLocation
}
Here's how a second hook ( useCountryData ) might consume the first ( useMappedLocation ). Note the useEffect handles the case that the location hasn't arrived yet, and mappedLocation is in the dependency array to ensure the useEffect runs a second time when the mappedLocation DOES finally arrive.
import { useMappedLocation } from './useMappedLocation';
export const useCountryData = () => {
const [earliest2, setEarliest2] = useState([]);
const [countryDeaths, setcountryDeaths] = useState(Number);
const [countryCases, setcountryCases] = useState(Number);
const mappedLocation = useMappedLocation()
useEffect(() => {
if(mappedLocation !== null){
axios.get("https://coronavirus-19-api.herokuapp.com/countries")
.then((response) => {
setEarliest2(response.data);
const countryArray = response.data.filter(
(item) => item.country === mappedLocation.country
);
const resCountryDeaths = countryArray[0].deaths;
setcountryDeaths(resCountryDeaths);
const resCountryCases = countryArray[0].cases;
setcountryCases(resCountryCases);
console.log("hiiii", countryCases);
})
.catch((err) => {
console.log(err);
});
}
}, [mappedLocation]);
return { countryCases, countryDeaths };
};
Related
I'm trying a project where I use my handpose to play the dino game in chrome. It's been 6 hours and I cannot seem to find a good solution to passing props to the game. Here is the code of the App.js
function App() {
const [isTouching, setIsTouching] = useState(false);
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const runHandpose = async () => {
const net = await handpose.load();
console.log('Handpose model loaded.');
// Loop and detect hands
setInterval(() => {
detect(net);
}, 100)
};
const detect = async (net) => {
if (
typeof webcamRef.current !== 'undefined' &&
webcamRef.current !== null &&
webcamRef.current.video.readyState === 4
) {
...
await handleDistance(hand);
}
}
const handleDistance = (predictions) => {
if (predictions.length > 0) {
predictions.forEach(async (prediction) => {
const landmarks = prediction.landmarks;
const thumbTipPoint = landmarks[4]
const indexFingerTipPoint = landmarks[8]
const xDiff = thumbTipPoint[0] - indexFingerTipPoint[0]
const yDiff = thumbTipPoint[1] - indexFingerTipPoint[1]
const dist = Math.sqrt(xDiff*xDiff + yDiff*yDiff)
if (dist < 35) {
setIsTouching(true);
} else {
setIsTouching(false);
}
})
}
}
useEffect(() => {
console.log(isTouching)
}, [isTouching])
useEffect(() => {
runHandpose();
}, [])
return (
<div className="App">
<div className="App-header">
<Webcam ref={webcamRef}
style={{...}}/>
<canvas ref={canvasRef}
style={{...}} />
</div>
<Game isTouching={isTouching} />
</div>
);
}
And here is some code from the game
export default function Game({ isTouching }) {
const worldRef = useRef();
const screenRef = useRef();
const groundRef1 = useRef();
const groundRef2 = useRef();
const dinoRef = useRef();
const scoreRef = useRef();
function setPixelToWorldScale() {
...
}
function handleStart() {
lastTime = null
speedScale = 1
score = 0
setupGround(groundRef1, groundRef2)
setupDino(dinoRef, isTouching)
setupCactus()
screenRef.current.classList.add("hide")
window.requestAnimationFrame(update)
}
async function update(time) {
if (lastTime == null) {
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
return
}
const delta = time - lastTime
updateGround(delta, speedScale, groundRef1, groundRef2)
updateDino(delta, speedScale, dinoRef)
updateCactus(delta, speedScale, worldRef)
updateSpeedScale(delta)
updateScore(delta, scoreRef)
// if (checkLose()) return handleLose(dinoRef, screenRef)
lastTime = time
window.requestAnimationFrame((time) => {
update(time)
})
}
function updateSpeedScale(delta) {...}
function updateScore(delta) {...}
function checkLose() {...}
function isCollision(rect1, rect2) {...}
useEffect(() => {
console.log(isTouching)
window.requestAnimationFrame(update);
}, [isTouching])
function handleLose() {...}
useEffect(() => {
setPixelToWorldScale()
window.addEventListener("resize", setPixelToWorldScale())
document.addEventListener("click", handleStart, { once: true })
},[])
return (...);
}
What I've been trying to do is how I can pass isTouching to the game everytime my thumb and my index finger meet. But I want to avoid re-render the game and I only want to update the dino. But I cannot find a way to do that. I was also trying to create a isJump state inside the game but I don't know how to pass the setisJump to the parent (which is App) so that I can do something like this:
useEffect(() => {
setIsJump(true)
}, [isTouching])
Does anyone have a better idea of how to do this? Or did I made a mistake on passing the props here? Thank you
Am requesting help setting the cartItems state to local storage so that when never I refresh, I still get the items.
import React, { createContext, useState } from 'react';
export const AppContext = createContext();
export function Context(props) {
const [cartItems, setCartItems] = useState([]);
const cart = (item) => {
const exist = cartItems.find((x) => x.id === item.id);
if (!exist) {
setCartItems([...cartItems, { ...item }]);
} else {
alert('Item Already Taken');
}
};
const onAdd = (items) => {
console.log(cartItems);
const exist = cartItems.find((x) => x.id === items.id);
if (exist) {
setCartItems(
cartItems.map((x) =>
x.id === items.id ? { ...exist, qty: exist.qty + 1 } : x,
),
);
console.log(cartItems);
} else {
console.log(items);
setCartItems([...cartItems, { ...items, qty: 1 }]);
}
};
const onRemove = (product) => {
const exist = cartItems.find((x) => x.id === product.id);
if (exist.qty === 1) return;
setCartItems(
cartItems.map((x) =>
x.id === product.id ? { ...exist, qty: exist.qty - 1 } : x,
),
);
};
const onDeleted = (product) => {
if (window.confirm('Do you want to delete this product?')) {
setCartItems(cartItems.filter((x) => x.id !== product.id));
}
};
const itemPrice =
cartItems && cartItems.reduce((a, c) => a + c.qty * c.price, 0);
const delieveryPrice = '3000';
// eslint-disable-next-line
const totalPrice =
parseInt(itemPrice) + (itemPrice && parseInt(delieveryPrice));
return (
<AppContext.Provider
value={{
cartItems,
setCartItems,
onAdd,
onRemove,
cart,
onDeleted,
itemPrice,
totalPrice,
delieveryPrice,
}}
>
{props.children}
</AppContext.Provider>
);
}
You can acheive your goal by creating a customHook to initialize state with value in localStorage and alos write state on the localStorage on every update, like this:
import * as React from 'react'
function useLocalStorageState(
key,
defaultValue = '',
{serialize = JSON.stringify, deserialize = JSON.parse} = {},
) {
const [state, setState] = React.useState(() => {
const valueInLocalStorage = window.localStorage.getItem(key)
if (valueInLocalStorage) {
try {
return deserialize(valueInLocalStorage)
} catch (error) {
window.localStorage.removeItem(key)
}
}
return typeof defaultValue === 'function' ? defaultValue() : defaultValue
})
const prevKeyRef = React.useRef(key)
React.useEffect(() => {
const prevKey = prevKeyRef.current
if (prevKey !== key) {
window.localStorage.removeItem(prevKey)
}
prevKeyRef.current = key
window.localStorage.setItem(key, serialize(state))
}, [key, state, serialize])
return [state, setState]
}
and use it in your component like this:
const [cartItems, setCartItems] = useLocalStorageState('cartItems',[]);
You use can use localStorage.setItem().but since you have a list of items, you will first stringify it while saving and parsing when you fetch it back.
import React, { createContext, useState } from 'react';
export const AppContext = createContext();
export function Context(props) {
const [cartItems, setCartItems] = useState([]);
const onAdd = (items) => {
const tempItems = [...cartItems];
const exist = cartItems.findIndex((x) => x.id === items.id);
console.log(exist);
if (exist >= 0) {
tempItems[exist].qty = tempItems[exist].qty + 1;
}
else {
// setCartItems([...cartItems, { ...items, qty: 1 }]);
tempItems.push(items)
}
setCartItems(tempItems)
localStorage.setItem("cart" , JSON.stringify(tempItems))
};
//to fetch data
const getCart = () => {
const FetchedcartItems = localStorage.getItem("cart");
if (FetchedcartItems) {
const parsedCartItems = JSON.parse(FetchedcartItems);
console.log(parsedCartItems);
setCartItems(parsedCartItems);
}
};
useEffect(() => {
getCart();
}, []);
return (
<AppContext.Provider
value={{
cartItems,
setCartItems,
onAdd,
onRemove,
cart,
onDeleted,
itemPrice,
totalPrice,
delieveryPrice,
}}
>
{props.children}
</AppContext.Provider>
);
}
What I do, usually, is to set up a file just to handle localStorage, so in your case, cart.services.js. Inside that file you can create functions that receive, save, read, modify and etc the localStorage.
Something like this:
const getCart = () => {
return JSON.parse(localStorage.getItem("cart"));
};
const setCart = (cart) => {
localStorage.setItem("cart", JSON.stringify(cart));
};
const removeUser = () => {
localStorage.removeItem("cart");
};
Obviously you might need some more fancier logic to add items based on previous state, etc, but the basic logic is that and it's super straightforward.
More info: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
I'm new in react hooks, here converting from class(class version works) to hooks, i'm not sure if i have done it correctly because when using 'then' in hooks it says 'Property 'then' does not exist on type '(dispatch: any) => Promise'.ts(2339)'
this is class version which works:
import {
getGraph,
getFloorplan,
changeActiveCamera,
} from "../redux/actions";
const mapStateToProps = (state) => {
return {
currentSite: state.selection.currentSite,
currentCamera: state.selection.currentCamera,
};
};
function mapDispatchToProps(dispatch) {
return {
getGraph: (site) => dispatch(getGraph(site)),
getFloorplan: (site) => dispatch(getFloorplan(site)),
changeActiveCamera: (site, id) => dispatch(changeActiveCamera(site, id)),
};
}
loadGraph() {
if (this.props.currentSite) {
this.props.getFloorplan(this.props.currentSite.identif).then(() => {
console.log("Fetched floorplan");
this.props.getGraph(this.props.currentSite.identif).then(() => {
console.log("Fetched model", this.props.realGraph.model);
// new camera-related node & link status
if (this.props.currentCamera) {
this.props.changeActiveCamera(
this.props.currentSite.identif,
this.props.currentCamera.identif
);
}
});
});
}
}
this is what i have done in order to convert it :
const dispatch = useDispatch();
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const loadGraph = () => {
if (currentSite) {
dispatch(getFloorplan(currentSite.identif)).then(() => {
console.log("Fetched floorplan");
dispatch(getGraph(currentSite.identif)).then(() => {
console.log("Fetched model", realGraph.model);
// new camera-related node & link status
if (currentCamera) {
dispatch(
changeActiveCamera(
currentSite.identif,
currentCamera.identif
)
);
}
});
});
}
};
After seeing video shared in comment, i changed the code and getting new error: 'Uncaught RangeError: Maximum call stack size exceeded
at getFloorplan '
my code:
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const getFloorplan = (site) => dispatch(getFloorplan(site));
const getGraph = (site) => dispatch(getGraph(site));
const changeActiveCamera = (site, id) =>
dispatch(changeActiveCamera(site, id));
const loadGraph = () => {
if (currentSite) {
getFloorplan(currentSite.identif).then(() => {
console.log("Fetched floorplan");
getGraph(currentSite.identif).then(() => {
console.log("Fetched model", realGraph.model);
// new camera-related node & link status
if (currentCamera) {
changeActiveCamera(
currentSite.identif,
currentCamera.identif
);
}
});
});
}
};
I highly doubt this simple solution will work, but here is my suggestions for refactors.
Please note that the way that your old class was written, that's not the recommended way of writing redux. Sure, the code will work, but it's not the "right" way in many people's opinions.
Refactor your actions
I would rather have my code written out like this :
YourComponent.tsx
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const dispatch = useDispatch();
const loadGraph = () => {
useEffect(() => {
if(currentSite)
getFloorPlan(currenSite.identif);
}, [ dispatch, currentSite.identif])
// dispatch is in the dependency array to stop make eslinter complain, you can remove it if you want, others are used to control the number of renders
};
YourComponentActions.ts
// assuming you are using redux-thunk here
const getFloorplan = ( site : YourType ) => async dispatch => {
console.log("Fetched floorplan");
// remove await if getImage is a sync function, try with await first and remove if it doesn't fit
const response = await getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
dispatch(getGraph(response))
}
const getGraph = (site : YourType) => async dispatch => {
console.log("Fetched model", realGraph.model);
// new camera-related node & link status
if (currentCamera) {
changeActiveCamera(currentSite.identif,currentCamera.identif);
}
}
const changeActiveCamera = ( site: YourType, param: YourAnotherType ) => async dispatch => {
// your logic here
}
Last edit of the night. Tried to clean some things up to make it easier to read. also to clarify what is going on around the useEffect. Because I am running react in strict mode everything gets rendered twice. The reference around the useEffect makes sure it only gets rendered 1 time.
Db is a firebase reference object. I am grabbing a list of league of legends games from my database.
one I have all my games in the snapshot variable, I loop through them to process each game.
each game contains a list of 10 players. using a puuId I can find a specific player. We then pull the data we care about in addChamp.
The data is then put into a local map. We continue to update our local map untill we are done looping through our database data.
After this I attempt to change our state variable in the fetchMatches function.
My issue now is that I am stuck in an infinite loop. I think this is because I am triggering another render after the state gets changed.
import { useState, useEffect, /*useCallback,*/ useRef } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var init = new Map()
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
init.set(item.championName, temp)
//setChamps(new Map(champs.set(item.championName, temp)))
}//,[champs])
const pack = /*useCallback( /*async*/ (data) => {
console.log("enter pack")
for(const item of data.participants){
//console.log(champ.assists)
if(item.puuid === player.puuid){
console.log(item.summonerName)
if(init.has(item.championName)){//only checking init??
console.log("update champ")
}
else{
console.log("add champ")
/*await*/ addChamp(item)
}
}
}
}/*,[addChamp, champs, player.puuid])*/
const fetchMatches = async () => {
console.log("enter fetch matches")
Db.collection("summoner").doc(player.name).collection("matches").where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
//console.log("loop")
console.log(doc.id, " => ", doc.data());
console.log("total: ", ++total);
await pack(doc.data());
});
})
.then( () => {
setChamps(init)
})
.catch((error) => {
console.log("error getting doc", error);
});
}
const render1 = useRef(true)
useEffect( () => {
console.log("enter use effect")
if(render1.current){
render1.current = false
}
else{
fetchMatches();
}
})
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
Newest Version. no longer infinitly loops, but values do not display/render.
import { useState, useEffect } from 'react'
import Db from '../Firebase'
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map())
var total = 0
console.log("entered stats")
const addChamp = /*useCallback(*/ (item) => {
console.log("enter add champ")
var min = item.timePlayed/60
//var sec = item.timePlayed%60
var kda = (item.kills + item.assists)/item.deaths
var dub = 0
if(item.win){
dub = 1
}
var temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled/min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1
}
return temp
}
useEffect(() => {
var tempChamp = new Map()
Db.collection("summoner").doc(player.name).collection("matches").where("queueId","==",420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
console.log(doc.id," => ", doc.data());
console.log("total: ", ++total);
for(const person of doc.data().participants){
if(player.puuid === person.puuid){
console.log(person.summonerName);
if(tempChamp.has(person.championName)){
console.log("update ", person.championName);
//add update
}else{
console.log("add ", person.championName);
var data = await addChamp(person);
tempChamp.set(person.championName, data);
}
}
}
})//for each
setChamps(tempChamp)
})
},[player.name, total, player.puuid]);
return(
<div>
<ul>
{[...champs.keys()].map( k => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
)
}
export default TotGenStats
useEffect will be called only once when you will not pass any argument to it and useEffect works as constructor hence its not possible to be called multiple times
useEffect( () => {
},[])
If you pass anything as argument it will be called whenever that argument change is triggered and only in that case useEffect will be called multiple times.
useEffect( () => {
},[arg])
Though whenever you update any state value in that case component will re-render. In order to handle that situation you can use useCallback or useMemo.
Also for map operation directly doing it on state variable is not good idea instead something like following[source]:
const [state, setState] = React.useState(new Map())
const add = (key, value) => {
setState(prev => new Map([...prev, [key, value]]))
}
I have made some edits to your latest code try following:
import { useState, useEffect, useRef } from "react";
import Db from "../Firebase";
const TotGenStats = ({ player }) => {
const [champs, setChamps] = useState(new Map());
const addChamp = (item) => {
let min = item.timePlayed / 60;
let kda = (item.kills + item.assists) / item.deaths;
let dub = null;
if (item.win) {
dub = 1;
} else {
dub = 0;
}
let temp = {
name: item.championName,
avgCs: item.totalMinionsKilled,
csMin: item.totalMinionsKilled / min,
kds: kda,
kills: item.kills,
deaths: item.deaths,
assists: item.assists,
wins: dub,
totalG: 1,
};
setChamps((prev) => new Map([...prev, [item.championName, temp]]));
};
const pack = (data) => {
for (const item of data.participants) {
if (item.puuid === player.puuid) {
if (!champs.has(item.championName)) {
addChamp(item);
}
}
}
};
const fetchMatches = async () => {
Db.collection("summoner")
.doc(player.name)
.collection("matches")
.where("queueId", "==", 420)
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
await pack(doc.data());
});
})
.catch((error) => {});
};
const render1 = useRef(true);
useEffect(() => {
fetchMatches();
});
return (
<div>
<ul>
{[...champs.keys()].map((k) => (
<li key={k}>{champs.get(k).name}</li>
))}
</ul>
</div>
);
};
export default TotGenStats;
I found myself continuously writing the same shape of code for asynchronous calls so I tried to wrap it up in something that would abstract some of the details. What I was hoping was that in my onError callback I could pass a reference of the async function being executed so that some middleware could implement retry logic if it was necessary. Maybe this is a code smell that I'm tackling this the wrong way but I'm curious if it's possible or if there are other suggestions for handling this.
const runAsync = (asyncFunc) => {
let _onBegin = null;
let _onCompleted = null;
let _onError = null;
let self = this;
return {
onBegin(f) {
_onBegin = f;
return this;
},
onCompleted(f) {
_onCompleted = f;
return this;
},
onError(f) {
_onError = f;
return this;
},
async execute() {
if (_onBegin) {
_onBegin();
}
try {
let data = await asyncFunc();
if (_onCompleted) {
_onCompleted(data);
}
} catch (e) {
if (_onError) {
_onError(e ** /*i'd like to pass a function reference here as well*/ ** );
}
return Promise.resolve();
}
},
};
};
await runAsync(someAsyncCall())
.onBegin((d) => dispatch(something(d)))
.onCompleted((d) => dispatch(something(d)))
.onError((d, func) => dispatch(something(d, func)))
.execute()
I'm thinking you could use a custom hook. Something like -
import { useState, useEffect } from 'react'
const useAsync = (f) => {
const [state, setState] =
useState({ loading: true, result: null, error: null })
const runAsync = async () => {
try {
setState({ ...state, loading: false, result: await f })
}
catch (err) {
setState({ ...state, loading: false, error: err })
}
}
useEffect(_ => { runAsync() }, [])
return state
}
Now we can use it in a component -
const FriendList = ({ userId }) => {
const response =
useAsync(UserApi.fetchFriends(userId)) // <-- some promise-returning call
if (response.loading)
return <Loading />
else if (response.error)
return <Error ... />
else
return <ul>{response.result.map(Friend)}</ul>
}
The custom hook api is quite flexible. The above approach is naive, but we can think it through a bit more and make it more usable -
import { useState, useEffect } from 'react'
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Custom hooks are dope. We can make custom hooks using other custom hooks -
const fetchJson = (url = "") =>
fetch(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") => // <-- another hook
useAsync(fetchJson, [url]) // <-- useAsync
const FriendList = ({ userId }) => {
const { loading, error, result } =
useJson("some.server/friends.json") // <-- dead simple
if (loading)
return <Loading .../>
if (error)
return <Error .../>
return <ul>{result.map(Friend)}</ul>
}