React recoil, overlay with callback and try finally - javascript

I want to render overlay on the long running operations.
Consider I have the following code
let spinnerState = useRecoilValue(overlayState);
return <BrowserRouter>
<Spin indicator={<LoadingOutlined />} spinning={spinnerState.shown} tip={spinnerState.content}>.........</BrowserRouter>
What I do in different components
const [, setOverlayState] = useRecoilState(overlayState);
const onButtonWithLongRunningOpClick = async () => {
Modal.destroyAll();
setOverlayState({
shown: true,
content: text
});
try {
await myApi.post({something});
} finally {
setOverlayState(overlayStateDefault);
}
}
How can I refactor this to use such construction that I have in this onbuttonclick callback? I tried to move it to the separate function, but you cannot use hooks outside of react component. It's frustrating for me to write these try ... finally every time. What I basically want is something like
await withOverlay(async () => await myApi.post({something}), 'Text to show during overlay');

Solution
Write a custom hook that includes both UI and API. This pattern is widely used in a large app but I couldn't find the name yet.
// lib/buttonUi.js
const useOverlay = () => {
const [loading, setLoading] = useState(false);
return {loading, setLoading, spinnerShow: loading };
}
export const useButton = () => {
const overlay = useOverlay();
const someOp = async () => {
overlay.setLoading(true);
await doSomeOp();
/* ... */
overlay.setLoading(false);
}
return {someOp, ...overlay}
}
// components/ButtonComponent.jsx
import { useButton } from 'lib/buttonUi';
const ButtonComponent = () => {
const {spinnerShow, someOp} = useButton();
return <button onClick={someOp}>
<Spinner show={spinnerShow} />
</button>
}
export default ButtonComponent;

create a custom hook that handles the logic for showing and hiding the overlay.
import { useRecoilState } from 'recoil';
const useOverlay = () => {
const [, setOverlayState] = useRecoilState(overlayState);
const withOverlay = async (fn: () => Promise<void>, content: string) => {
setOverlayState({ shown: true, content });
try {
await fn();
} finally {
setOverlayState(overlayStateDefault);
}
};
return withOverlay;
};
You can then use the useOverlay hook in your components
import { useOverlay } from './useOverlay';
const Component = () => {
const withOverlay = useOverlay();
const onButtonWithLongRunningOpClick = async () => {
await withOverlay(async () => await myApi.post({ something }), 'Text to show during overlay');
};
return <button onClick={onButtonWithLongRunningOpClick}>Click me</button>;
};

Related

How to avoid unnecessary API calls with useEffect?

I'm still beginner to ReactJS and I'm having trouble rendering a list.
I don't know why, all the time calls are being made to my API. Since I don't put any dependency on useEffect, that is, I should only render my function once.
I don't understand why this is happening. Can you tell me what I'm doing wrong?
Here's my code I put into codesandbox.io
import React from "react";
import axios from "axios";
import "./styles.css";
const App = () => {
const BASE_URL = "https://pokeapi.co/api/v2";
const [pokemons, setPokemons] = React.useState([]);
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
data.results.map((pokemon) => getPokeType(pokemon));
};
const getPokeType = async (pokemon) => {
const { data } = await axios.get(pokemon.url);
setPokemons((prev) => [...prev, data]);
};
React.useEffect(() => {
getAllPokemons();
}, []);
console.log(pokemons);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{pokemons.map((pokemon) => (
<p key={pokemon.id} style={{ color: "blue" }}>
{pokemon.name}
</p>
))}
</div>
);
};
export default App;
Thank you very much in advance.
Your issue is that you are calling setPokemons inside getPokeType (which is called for each data in part). Your useEffect is called just once (as expected) and the ${BASE_URL}/pokemon call is executed just once too. But getPokeType is called 20 times and the pokemons state is changed 20 times as well (once for each instance from data.results).
What I would recommend in your case (instead of what you have now) is:
Create a list of all the pokemons and
Set the state just once at the end.
So something like:
...
const getPokeType = async (pokemon) => {
const { data } = await axios.get(pokemon.url);
return data;
};
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
const pokemons = await Promise.all(
data.results.map((pokemon) => getPokeType(pokemon))
);
setPokemons(pokemons);
};
React.useEffect(() => {
getAllPokemons();
}, []);
...
I was just having the same issue in my project the way I solved is by moving the function definition inside the useEffect
React.useEffect(() => {
const getAllPokemons = async () => {
const { data } = await axios.get(`${BASE_URL}/pokemon`);
data.results.map((pokemon) => getPokeType(pokemon));
};
getAllPokemons();
}, []);
If this solves your problem please accept the answer.

How to call client-server side render and static generation render

I want to make static generation for top products with getStaticProps.
now a section of my rendering is not needed to static generation. for example: comments, related products.
full code:
export default function Gift(props) {
let [relatedProducts, setRelatedProducts] = useState([]);
const getRelatedProducts = () => {
api.get(`gift/id/${props.id}/relateds/count/10`).then(res => {
console.log(res.data.data);
setRelatedProducts(res.data.data)
})
}
//called n times. looping !!!
getRelatedProducts();
return (
<GiftProvider value={props}>
<ProductPage/>
<RelatedProducts title="related products" products={relatedProducts}/>
<ProductGeneralProperties/>
<ProductComment/>
</GiftProvider>
);
}
export async function getStaticPaths() {
const gifts = await getTopGifts()
const paths = gifts.map((gift) => ({
params: {slug: gift.slug}
}))
return {paths, fallback: 'blocking'}
}
export async function getStaticProps(context) {
const slug = context.params.slug
const gift = await getGiftWithSlug(slug)
return {
props: gift,
}
}
but with below code my codes renders multi times:
export default function Gift(props) {
let [relatedProducts, setRelatedProducts] = useState([]);
const getRelatedProducts = () => {
api.get(`gift/id/${props.id}/relateds/count/10`).then(res => {
console.log(res.data.data);
setRelatedProducts(res.data.data)
})
}
getRelatedProducts();
You can use useEffect hook to call the api
useEffect(() => {
const getRelatedProducts = () => {
api.get(`gift/id/${props.id}/relateds/count/10`).then(res => {
console.log(res.data.data);
setRelatedProducts(res.data.data)
})
}
getRelatedProducts();
},[])

Using React Context with AsyncStorage using hooks

My goal is to use custom hooks created from Context to pass and modify stored values
The final goal is to use something like useFeedContext() to get or modify the context values
What I am actually getting is either the functions that I call are undefined or some other problem ( I tried multiple approaches)
I tried following this video basics of react context in conjunction with this thread How to change Context value while using React Hook of useContext but I am clearly getting something wrong.
Here is what I tried :
return part of App.js
<FeedProvider mf={/* what do i put here */}>
<Navigation>
<HomeScreen />
<ParsedFeed />
<FavScreen />
</Navigation>
</FeedProvider>
Main provider logic
import React, { useState, useEffect, useContext, useCallback } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
const FeedContext = React.createContext();
const defaultFeed = [];
const getData = async (keyName) => {
try {
const jsonValue = await AsyncStorage.getItem(keyName);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log(e);
}
};
const storeData = async (value, keyName) => {
console.log(value, keyName);
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(keyName, jsonValue);
} catch (e) {
console.log(e);
}
};
export const FeedProvider = ({ children, mf }) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
const [feedLoaded, setFeedLoaded] = useState(false);
let load = async () => {
let temp = await AsyncStorage.getItem("mainUserFeed");
temp != null
? getData("mainUserFeed").then((loadedFeed) => setMainFeed(loadedFeed))
: setMainFeed(defaultFeed);
setFeedLoaded(true);
};
useEffect(() => {
load();
}, []);
useCallback(async () => {
if (!feedLoaded) {
return await load();
}
}, [mainFeed]);
const setFeed = (obj) => {
setMainFeed(obj);
storeData(mainFeed, "mainUserFeed");
};
return (
<FeedContext.Provider value={{ getFeed: mainFeed, setFeed }}>
{children}
</FeedContext.Provider>
);
};
//export const FeedConsumer = FeedContext.Consumer;
export default FeedContext;
The custom hook
import { useContext } from "react";
import FeedContext from "./feedProviderContext";
export default function useFeedContext() {
const context = useContext(FeedContext);
return context;
}
What I would hope for is the ability to call the useFeedContext hook anywhere in the app after import like:
let myhook = useFeedContext()
console.log(myhook.getFeed) /// returns the context of the mainFeed from the provider
myhook.setFeed([{test:1},{test:2}]) /// would update the mainFeed from the provider so that mainFeed is set to the passed array with two objects.
I hope this all makes sense, I have spend way longer that I am comfortable to admit so any help is much appreciated.
If you want to keep using your useFeedContext function, I suggest to move it into the your 'Provider Logic' or I'd call it as 'FeedContext.tsx'
FeedContext.tsx
const FeedContext = createContext({});
export const useFeedContext = () => {
return useContext(FeedContext);
}
export const AuthProvider = ({children}) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
...
return (
<FeedContext.Provider value={{mainFeed, setMainFeed}}>
{children}
</FeedContext.Provider>
);
};
YourScreen.tsx
const YourScreen = () => {
const {mainFeed, setMainFeed} = useFeedContext();
useEffect(() => {
// You have to wait until mainFeed is defined, because it's asynchronous.
if (!mainFeed || !mainFeed.length) {
return;
}
// Do something here
...
}, [mainFeed]);
...
return (
...
);
};
export default YourScreen;

How can I run multiple queries on load of functional component using UseEffect and get result in render method?

I have the following functional component where, on load of the component, it needs to loop through an array and run some async queries to populdate a new array I want to display in render method.
import React, { useEffect, useState, useContext } from 'react';
import { AccountContext } from '../../../../providers/AccountProvider';
import { GetRelationTableCount } from '../../../../api/GetData';
import { getTableAPI } from '../../../../api/tables';
const RelatedRecordsPanel = (props) => {
const { userTokenResult } = useContext(AccountContext);
const { dataItem } = props;
const [relatedTableItems, setRelatedTableItems] = useState([]);
useEffect(() => {
const tempArray = [];
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
tempArray.push(TableIconInfo);
})
);
};
fetchData();
setRelatedTableItems(tempArray)
}, []);
return (
<div>
{relatedTableItems.length > 0 ? <div>{relatedTableItems.name}</div> : null}
</div>
);
};
In the above code, the queries run correctly and if I do a console.log in the loop, I can see if fetches the data fine, however, the array is always [] and no data renders. How do I write this async code such that it completes the queries to populate the array, so that I can render properly?
Thx!
You aren't using the return value of the Promise.all and since all your APIs are async, the tempArray is not populated by the time you want to set it into state
You can update it like below by waiting on the Promise.all result and then using the response
useEffect(() => {
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
return TableIconInfo;
})
);
};
fetchData().then((res) => {
setRelatedTableItems(res);
});
}, []);

How to prevent a state update on a react onClick function (outside useEffect)?

When I use useEffect I can prevent the state update of an unmounted component by nullifying a variable like this
useEffect(() => {
const alive = {state: true}
//...
if (!alive.state) return
//...
return () => (alive.state = false)
}
But how to do this when I'm on a function called in a button click (and outside useEffect)?
For example, this code doesn't work
export const MyComp = () => {
const alive = { state: true}
useEffect(() => {
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return
setSomeState('hey')
// warning, because alive.state is true here,
// ... not the same variable that the useEffect one
}
}
or this one
export const MyComp = () => {
const alive = {}
useEffect(() => {
alive.state = true
return () => (alive.state = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.state) return // alive.state is undefined so it returns
setSomeState('hey')
}
}
When a component re-renders, it will garbage collect the variables of the current context, unless they are state-full. If you want to persist a value across renders, but don't want to trigger a re-renders when you update it, use the useRef hook.
https://reactjs.org/docs/hooks-reference.html#useref
export const MyComp = () => {
const alive = useRef(false)
useEffect(() => {
alive.current = true
return () => (alive.current = false)
}
const onClickThat = async () => {
const response = await letsbehere5seconds()
if (!alive.current) return
setSomeState('hey')
}
}

Categories

Resources