How do I nest async React hooks - javascript

This is a massively simplified version a React Hook solution that I am stuck on...
export const useStepA = () => {
const [stepA, setStepA] = useState();
const getStepA = useCallback(async (stepAParam: string) => {
setStepA({ stepBId: '1' });
}, []);
return { getStepA, stepA };
};
export const useStepB = () => {
const [stepB, setStepB] = useState();
const getStepB = useCallback(async (stepBParam: string) => {
setStepB(stepBParam);
}, []);
return { getStepB, stepB };
};
export const useStepC = () => {
const { getStepA, stepA } = useStepA();
const { getStepB, stepB } = useStepB();
const [stepC, setStepC] = useState();
const getStepC = useCallback(
async (stepAParam: string) => {
/* ????? await? useEffect? magic?
getStepA(stepAParam);
getStepB(stepA.stepBId);
*/
setStepC({stepA,stebB});
},
[getStepA, getStepB, stepA]
);
return { getStepC, stepC };
};
In the real world... StepB is dependent on StepA's data, both are fetch calls... StepC takes the contents of StepA and StepB and returns an amalgamation of them...
How I can write the stepC hook to process and wait, then process and wait, then process and return?

While I don't believe this is a very composable pattern to begin with, it is possible to make them work somewhat with a bit of effort if you're careful to make your initial values for stepA and stepB falsy:
export const useStepC = () => {
const { getStepA, stepA } = useStepA();
const { getStepB, stepB } = useStepB();
const [stepC, setStepC] = useState();
const getStepC = useCallback((stepAParam: string) => {
getStepA(stepAParam);
}, [getStepA]);
useEffect(() => {
if (stepA) {
getStepB(stepA.stepBId);
}
}, [stepA, getStepB]);
useEffect(() => {
if (stepA && stepB) {
setStepC({stepA,stebB});
}
}, [stepA, stepB, setStepC])
return { getStepC, stepC };
};
Problems will most likely occur when there are two concurrent promises pending caused by multiple calls to getStepC() in a short period, and because the way useStepA() and useStepB() are implemented, there is no way to resolve these inherent race conditions.

There is a useSWR hook what solve kind of these problems. check this

Related

I tried this code i already passed dependency in useEffect but the api call again and again after 3/4 second

Below code i try to make get response from api and put in Movie Component. but the problem is that api hit again and again i don't why this happened.here screen shot of api call
const [loading, setloading] = useState(false);
const [movielist, setmovielist] = useState([]);
const [err, seterr] = useState("");
const fetchMoviesHandler = useCallback(async () => {
setloading(true);
try {
const reponse = await fetch("https://swapi.dev/api/films");
if (reponse.status != 200) {
seterr("something is wrong");
}
const data = await reponse.json();
const result = data.results;
setmovielist(result);
} catch (errr) {
}
});
useEffect(() => {
fetchMoviesHandler();
}, [fetchMoviesHandler]);
return (
<div>
{movielist.map((movie) => {
return <Movie key={movie.episode_id} title={movie.title} />;
})}
</div>
);
};
This is returning a new instance of the function on every render:
const fetchMoviesHandler = useCallback(async () => {
// your function
});
Which will trigger the useEffect on every render, since this function is in its dependency array.
To tell useCallback to memoize the function and keep the same instance across multiple renders, it also needs a dependency array. For example:
const fetchMoviesHandler = useCallback(async () => {
// your function
}, [setloading, setmovielist, seterr]);
Or, at the very least, an empty dependency array:
const fetchMoviesHandler = useCallback(async () => {
// your function
}, []);
Which would essentially create one instance of the function and always use it for the life of the component.

React recoil, overlay with callback and try finally

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>;
};

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.

In React useEffect, how do you sneak state updates in between long running code?

I'm having trouble getting my loading status to appear before the longRunningCode executes. I tried making it async to no avail.
const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);
const longRunningCode = () => {
const file = // synchronous code to generate a gzipped File object
return file;
}
// also tried
// const longRunningCode = async () => {
// ...
useEffect(() => {
if (myAction) {
setLoadingStatus('Do the thing...')
const result = longRunningCode()
// also tried `await` with async version
// ...
setLoadingStatus(undefined)
setMyAction(undefined)
}
}, [myAction])
//...
return (
<div>
<p>{loadingStatus}</p>
<button onClick={() => setMyAction({})}>Generate file</button>
</div>
)
I was thinking something along the lines of:
const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);
const longRunningCode = () => {
return new Promise(resolve, reject)=>{
const file = // synchronous code to generate a File object
resolve(file);
}
}
useEffect(() => {
if (myAction) {
setLoadingStatus('Do the thing...')
longRunningCode().then(file=>{
// ...
setLoadingStatus(undefined)
setMyAction(undefined)
})
}
}, [myAction])
//...
return <p>{loadingStatus}</p><button onClick={() => setMyAction({})} />
**Edit: ** with setTimeout
const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);
const longRunningCode = () => {
const file = // synchronous code to generate a File object
return file
}
}
useEffect(() => {
if (myAction) {
setLoadingStatus('Do the thing...')
//a 0 second delay timer waiting for longrunningcode to finish
let timer = setTimeout(() =>{
longRunningCode()
setLoadingStatus(undefined)
setMyAction(undefined)
}, 0);
// clear Timmer on unmount
return () => {
clearTimeout(timer);
};
}
}, [myAction])
//...
return <p>{loadingStatus}</p><button onClick={() => setMyAction({})} />
useEffect callbacks are only allowed to return undefined or a destructor function. In your example, you have passed an async function which returns a Promise. It may or may not run, but you will see React warnings that useEffect callbacks are run synchronously.
Instead, define an async function, and then call it.
const [loadingStatus, setLoadingStatus] = useState(undefined);
const [myAction, setMyAction] = useState(undefined);
useEffect(() => {
const doAction = async () => {
if (myAction) {
setLoadingStatus('Do the thing...');
const result = await longRunningCode();
// ...
setLoadingStatus(undefined);
setMyAction(undefined);
}
};
doAction();
}, [myAction]);
//...
return <p>{loadingStatus}</p><button onClick={() => setMyAction({})} />
Otherwise, what you have written will work.

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