Can you use an async function to set initial state with useState - javascript

My component relies on local state (useState), but the initial value should come from an http response.
Can I pass an async function to set the initial state? How can I set the initial state from the response?
This is my code
const fcads = () => {
let good;
Axios.get(`/admin/getallads`).then((res) => {
good = res.data.map((item) => item._id);
});
return good;
};
const [allads, setAllads] = useState(() => fcads());
But when I try console.log(allads) I got result undefined.

If you use a function as an argument for useState it has to be synchronous.
The code your example shows is asynchronous - it uses a promise that sets the value only after the request is completed
You are trying to load data when a component is rendered for the first time - this is a very common use case and there are many libraries that handle it, like these popular choices: https://www.npmjs.com/package/react-async-hook and https://www.npmjs.com/package/#react-hook/async. They would not only set the data to display, but provide you a flag to use and show a loader or display an error if such has happened
This is basically how you would set initial state when you have to set it asynchronously
const [allads, setAllads] = useState([]);
const [loading, setLoading] = useState(false);
React.useEffect(() => {
// Show a loading animation/message while loading
setLoading(true);
// Invoke async request
Axios.get(`/admin/getallads`).then((res) => {
const ads = res.data.map((item) => item._id);
// Set some items after a successful response
setAllAds(ads):
})
.catch(e => alert(`Getting data failed: ${e.message}`))
.finally(() => setLoading(false))
// No variable dependencies means this would run only once after the first render
}, []);
Think of the initial value of useState as something raw that you can set immediately. You know you would be display handling a list (array) of items, then the initial value should be an empty array. useState only accept a function to cover a bit more expensive cases that would otherwise get evaluated on each render pass. Like reading from local/session storage
const [allads, setAllads] = useState(() => {
const asText = localStorage.getItem('myStoredList');
const ads = asText ? JSON.parse(asText) : [];
return ads;
});

You can use the custom hook to include a callback function for useState with use-state-with-callback npm package.
npm install use-state-with-callback
For your case:
import React from "react";
import Axios from "axios";
import useStateWithCallback from "use-state-with-callback";
export default function App() {
const [allads, setAllads] = useStateWithCallback([], (allads) => {
let good;
Axios.get("https://fakestoreapi.com/products").then((res) => {
good = res.data.map((item) => item.id);
console.log(good);
setAllads(good);
});
});
return (
<div className="App">
<h1> {allads} </h1>
</div>
);
}
Demo & Code: https://codesandbox.io/s/distracted-torvalds-s5c8c?file=/src/App.js

Related

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

In React, failing to stop Axios request when component unmounts

With data fetching in React, the following is a common warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in ParentComponent
I've read multiple posts and suggestions on how to handle this, and none are working currently.
For this, we have the function useAxiosApi which fetches data asynchronously, and ParentComponent which is the component that uses the useAxiosApi() and needs the data. ParentComponent is the component being unmounted / being referenced in the warnings.
Parent Component
import useAxiosApi...
function ParentComponent({ info }) {
const dataConfig = { season: info.season, scope: info.scope };
const [data, isLoading1, isError1] = useAxiosApi('this-endpoint', [], dataConfig);
return (
{isLoading && <p>We are loading...</p>}
{!isLoading &&
... use the data to render something...
}
)
}
useAxiosApi
import axios from 'axios';
import { useState } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
const resources = {};
const useAxiosApi = (endpoint, initialValue, config) => {
// Set Data-Fetching State
const [data, setData] = useState(initialValue);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// For Live Search on keystroke, Save Fetches and Skip Fetch if Already Made
if (endpoint === 'liveSearch' && resources[config.searchText]) {
return [resources[config.searchText], false, false];
}
// Otherwise, Continue Forward
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
console.log('isMounted: ', isMounted);
if (isMounted) {
setData(result.data);
}
// If LiveSearch, store the response to "resources"
if (endpoint === 'liveSearch') {
resources[config.searchText] = result.data;
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
};
// Call Function
fetchData();
// Cancel Request if needed in cleanup function
return () => {
console.log('Unmount or New Search? About to call source.cancel()');
isMounted = false; // is this doing its job?
source.cancel();
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
export default useAxiosApi;
createUrl is simply a function that takes the endpoint and dataConfig and creates the url that axios will fetch from. Note that our cancelTokens seem to be working in conjunction with the Live search, as new searches are cancelling the old search queries, and the saving of data results into resources for the one specific endpoint liveSearch works as well.
However, our problem is that when ParentComponent is unmounted quickly, before the data fetch is complete, we still receive the Cant perform a React state update warning. I've checked the console.logs(), and console.log('isMounted: ', isMounted) is always returning true, even if we unmount the component quickly after it is mounted / before data fetching is complete.
We're at a loss on this, as using the isMounted variable is the way that I've seen this problem handled before. Perhaps there's a problem with the useDeepCompareEffect hook? Or maybe we're missing something else.
Edit: Weve also tried to create the isMounted variable from inside of ParentComponent, and pass that as a parameter into the useAxiosApi function, however this did not work for us either... In general, it would be much better if we can handle this warning via an update to our useAxiosApi function, as opposed to in the ParentComponent.
Edit2: It seems like the cancelToken only works when a duplicate API call is fired off to the same endpoint. This is good for our liveSearch, however it means that all of the other fetches are not cancelled.

React Custom Hook make return Function executable on an Event

I have created a custom hook for handling the uploads to AWS s3 bucket but I am facing a small problem. I did not want my hook to execute the logic directly so I created an executable function that I am then returning. The only problem is that the state is not being set from inside the handleUpload function
Here is what I am trying to do:
import React, { useState } from "react";
const useS3Aws = () => {
const [isLoading, setIsLoading] = useState(false);
const [uploadedUrl, setUploadedUrl] = useState("");
const handleUpload = async (file: any, s3FolderPath: string) => {
setIsLoading(true); // the problem is here (state not being updated)
// handle the uploading with AWS s3
// code ......
// setting the state with returned live url from s3
setUploadedUrl("the live url"); // the problem is here (state not being updated)
console.log(uploadedUrl); // it prints an empty string
setIsLoading(true); // the problem is here (state not being updated)
};
return [
isLoading,
uploadedUrl,
(file: any, s3FolderPath: string) => handleUpload(file, s3FolderPath),
] as const;
};
const UploadSong = () => {
const [isLoading, uploadedUrl, handleUpload] = useS3Aws();
const handleSong = async () => {
const file = {
// object data
};
handleUpload(file, "music");
console.log(uploadedUrl); // it is empty
};
return (
<div>
<p>Live URL: {uploadedUrl ? uploadedUrl : "No Link"}</p>
<button onClick={() => handleSong()}>
Click me
</button>
</div>
);
}
export default UploadSong;
Playground Link
Your hook looks fine, but you aren't using it's returned values right.
<button onClick={() => handleSong}>
That doesn't actually call handleSong on click. This calls a function that merely returns the handleSong function on click.
You want:
<button onClick={() => handleSong()}>
Or if you don't want to pass arguments, you can just:
<button onClick={handleSong}>
One point where you may be confused here:
console.log(uploadedUrl); // it is empty
When you set state, it's asynchronous. State is being set properly, but any local variables will reflect the previous state, because the re-render has not yet happened. Usually this doesn't matter at all, because as you can see here the UI updates immediately when you click the working button.

React hooks not set state at first time

i have a problem with hooks.
i'm using react-hooks, i have a button which at onClick getting data from api and setState with it.
Problem is:
when i click to button first time i get response from api but can't set it to state. When click to button second time i can setState. Why it happened ?
here is my component look like:
function App() {
const [a, setA] = useState(null);
const fetchData = () => {
let data = {
id: 1
}
axios.post(baseUrl, data)
.then(res => {
if (res.data) {
setA(res.data)
}
console.log(a)
})
}
return (
<div className="App">
<div>
<button onClick={fetchData}></button>
</div>
</div>
);
}
export default App;
i tried to using fetchData function like that:
function fetchData() {
let data = {
id: 1
}
axios.post(baseUrl, data)
.then(res => {
if (res.data) {
setA(res.data)
}
console.log(a)
})
}
but it's not helped too
a is a const. It's impossible to change it, so there's no way your console.log statement at the end of fetchData could ever log out something different than the value that a had when fetchData was created.
So that's not what setA does. The point of setA isn't to change the value of the const, but to cause the component to rerender. On that new render, a new set of variables will be created, and the new a will get the new value. Move your console.log out to the body of your component, and you'll see it rerender with the new value.
In short: Your code appears to be already working, you just have the wrong logging.
If your scope is to fetch data, use this:
const [data, setData] = useState("");
useEffect(async () => {
const result = await axios(
'here will be your api',
);
setData(result.data);
});
Now your response will be stored in data variable.
I would not use an effect for it, effects are useful if the props or state changes and can thereby substitute lifecycle methods like componentDidMount, componentDidUpdate, componentWillUnmount, etc.. But in your case these props haven't changed yet (you want to change your state though). Btw, be aware that #Asking's approach will fetch data on EVERY rerender (props or state change). If you want to use useEffect, be sure to add the second parameter to tell React when to update.
Normally, your code should work, I haven't tested but looks legit. Have you used developer tools for react to check if the state/hook has changed? Because if you say it did not work because of the console.log printing: Have in mind that setA() is an async function. The state was most likely not yet changed when you try to print it. If you haven't react developer tools (which I highly recommend) you can check the real state by adding this code in the function:
useEffect(() => console.log(a), [a]);
I have a few real improvements to your code though:
function App() {
const [a, setA] = useState(null);
const fetchData = useCallback(async () => {
let data = {
id: 1
}
const res = await axios.post(baseUrl, data);
setA(res.data);
}, []);
return (
<div className="App">
<div>
<button onClick={fetchData}></button>
</div>
</div>
);
}
export default App;
By adding useCallback you ensure that the function is memorized and not declared again and again on every rerender.
#Nicholas Tower has already answered your question. But if you are looking for code
function App() {
const [a, setA] = useState(null);
const fetchData = () => {
let data = {
id: 1
}
axios.post(baseUrl, data)
.then(res => {
if (res.data) {
setA(res.data)
}
})
}
console.log(a)
return (
<div className="App">
<div>
<button onClick={fetchData}></button>
</div>
</div>
);
}
export default App;
just place log before return (. This should do

How to handle/chain synchronous side effects that depend on another with React hooks

I'm trying to rewrite my application from redux to the new context + hooks, but unfortunately I'm having a hard finding a good way to handle series of synchronous side-effects that are depending on the response of the previous.
In my current redux application I make heavy use of synchronous/chained actions and API requests which I typically handle through redux-saga or thunks. So when the response of the first API request is returned, that data is used for the next API request etc.
I've made a custom hook "useFetch" (in this example it does not do much, since it's a simplified version, also I had to make a small adjustment for it to work on codesandbox - see the code below). The problem with that is that due to the "rules of hooks", I cannot use a custom hook inside the useEffect hook. So how to await the response of the first request before doing the next etc, if you have your own hook for fetching data? And even if I ended up giving up on useFetch abstraction and create a vanilla fetch request, how to avoid ending up with a bloated mess of many useEffects hooks? Can this be done a little more elegantly, or is context + hooks still to premature to compete with redux saga/thunk for handling side effects?
The example code below is kept very simple. What it should try to simulate is that:
query person api endpoint to get the person
once we have the person
response, query the job endpoint (using the person id in real world
scenario)
once we have the person and job, based on the response
from the person and job endpoints, query the collegues endpoint to
find the persons collegues at a specific job.
Here's the code. Added a delay to useFetch hook to simulate latency in real world:
import React, { useEffect, useState } from "react";
import { render } from "react-dom";
import "./styles.css";
const useFetch = (url, delay = 0) => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
// const result = await fetch(url, {
// method: "GET",
// headers: { "Content-Type": "application/json" }
// });
//const response = await result.json();
const response = await import(url);
setTimeout(function() {
setData(response);
}, delay);
};
fetchData();
}, [url]);
return data;
};
function App() {
const [person, setPerson] = useState();
const [job, setJob] = useState();
const [collegues, setCollegues] = useState();
// first we fetch the person /api/person based on the jwt most likely
const personData = useFetch("./person.json", 5000);
// now that we have the person data, we use the id to query for the
// persons job /api/person/1/jobs
const jobData = useFetch("./job.json", 3000);
// now we can query for a persons collegues at job x /api/person/1/job/1/collegues
const colleguesData = useFetch("./collegues.json", 1000);
console.log(personData);
console.log(jobData);
console.log(colleguesData);
// useEffect(() => {
// setPerson(useFetch("./person.json", 5000));
// }, []);
// useEffect(() => {
// setJob(useFetch("./job.json", 3000));
// }, [person]);
// useEffect(() => {
// setCollegues(useFetch("./collegues.json",1000));
// }, [job]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Running example: https://codesandbox.io/s/2v44lron3n?fontsize=14 (you might need to make a change - space or remove a semicolon - to make it work)
Hopefully something like this (or a better solution) is possible, or I will simply not be able to migrate from the awesome redux-saga/thunks to context + hooks.
Best Answer: https://www.youtube.com/watch?v=y55rLsSNUiM
Hooks wont replace the way you handle async actions, they are just an abstraction to some things you were used to do, like calling componentDidMount, or handling state, etc.
In the example you are giving, you don't really need a custom hook:
function App() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const job = await import("./job.json");
const collegues = await import("./collegues.json");
const person = await import("./person.json");
setData({
job,
collegues,
person
})
};
fetchData()
}, []);
return <div className="App">{JSON.stringify(data)}</div>;
}
That being said, maybe if you provide an example of an actual redux-saga or thunks code you have, that you want to refactor, we can see what the steps are to accomplish that.
Edit:
That being said if you still want to do something like this, you can take a look at this:
https://github.com/dai-shi/react-hooks-async
import React from 'react';
import { useFetch } from 'react-hooks-async/dist/use-async-task-fetch';
const UserInfo = ({ id }) => {
const url = `https://reqres.in/api/users/${id}?delay=1`;
const { pending, error, result, abort } = useFetch(url);
if (pending) return <div>Loading...<button onClick={abort}>Abort</button></div>;
if (error) return <div>Error:{error.name}{' '}{error.message}</div>;
if (!result) return <div>No result</div>;
return <div>First Name:{result.data.first_name}</div>;
};
const App = () => (
<div>
<UserInfo id={'1'} />
<UserInfo id={'2'} />
</div>
);
EDIT
This is an interesting approach https://swr.now.sh/#dependent-fetching
This is a common scenario in real life,where you want to wait for the first fetch is finished and then do the next fetch.
Please check out the new codesandbox:
https://codesandbox.io/s/p92ylrymkj
I used generator when you do fetch request. The data is retrieved in the correct order. After you click fetch data button, go to console and have a look.
Hope this is what you are looking for.
I believe you face this problem because of poor implementation of a fetch hook and the example is too basic for us to understand. You should make it clear if you'll use job, collegues and person in the same component. If so, it's always wiser to separate them.
That being said, let me give you an example of my own fetch hook:
const { loading, error, value } = useResource<Person>(getPerson, personId)
I have such a hook, which has it's own state for loading, error, value etc.
It takes two arguments:
- fetch method
- fetch method argument
Having such a structure, you can chain your resources together.
useResource implementation is just creating a state and in useEffect, checking if property change etc. If it changed, it calls the fetch method.

Categories

Resources