The project I'm working on requires a lot of API calls to the backend server using useApiGet. Some components make several API calls (4 or 5), while others only make 1. I want to create a custom hook that can take an array of objects {name, endpoint} and return the API data. This is mostly intended to help with error handling.
useDataLoader.js
const useDataLoader = (arrayOfEndpoints) => {
let dataArray = []
arrayOfEndpoints.forEach(endpoint => {
const [data, error] = useApiGet(endpoint.path)
if (error.res.status === 404 return <NotFound />
dataArray.push({name: endpoint.name, data})
}
return dataArray
}
MyComponent.js
import useDataLoader from ...
export default function MyComponent () {
const [dataA, dataB, dataC, dataD] = useDataLoader([
{name: dataA, path: endpointA},
{name: dataB, path: endpointB},
{name: dataC, path: endpointC},
{name: dataD, path: endpointD},
])
...other stuff
return (...)
}
useDataLoader should return an array of objects. The problem however is that useApiGet is also a hook and cannot be used inside a loop. What is an alternative way to do this?
The only other way I can think of this would be to return a <NotFound /> after every single API call which would create an issue of lots of duplicate code and code complexity (<NotFound /> could not be right after each API call because other hooks would be called conditionally). That would look like this:
export default function MyComponent () {
const [data: dataA, error: errorA] = useApiGet(endpointA)
const [data: dataB, error: errorB] = useApiGet(endpointB)
const [data: dataC, error: errorC] = useApiGet(endpointC)
const [data: dataD, error: errorD] = useApiGet(endpointD)
...other stuff
const errors = [errorA, errorB, errorC, errorD]
errors.forEach(error => {if (error.res.status === 404 return <NotFound />})
return (...)
}
This would need to be done for each component and would not be ideal.
My question is NOT "Why can't I use a React Hook in a loop?", which has been answered many times. This is understood. The question is "how can I make an indefinite number of API calls using this hook instead of writing a LOT of duplicate code?"
Preface
To my mind, you're best off sticking with individual calls to useGetApi. That way, not only are you not violating the rules as described on the page you linked in the comments (although see below, I think those are slightly over-stated for simplicity), but the code is very declarative and straightfoward. If you had useGetApi return the usual [loading, data, error] tuple, the code is;
const [aLoading, a, aError] = useGetApi("a");
const [bLoading, b, aError] = useGetApi("b");
const [cLoading, c, cerror] = useGetApi("c");
// ...
if (aError || bError || cError) {
return <NotFound />;
}
then rendering a (for instance) is:
{aLoading ? <Loading /> : a}
But, the premise of your question is that you don't want to do that. And certainly it would be very easy to add a d to the above but forget to update the if (aError || bError || cError) condition. So the answer below suggests how you might do your data loader concept.
Answer
As you say, the documentation says not to run hooks in loops (including in the beta documentation). But this is an oversimplification to help people avoid getting themselves in trouble. The real issue is stated later in the page of that first link:
So how does React know which state corresponds to which useState call? The answer is that React relies on the order in which Hooks are called.
(their emphasis) followed by:
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them. But what happens if we put a Hook call (for example, the persistForm effect) inside a condition?
As you can see, the real issue is that the same hooks must be called in the same order on every render. As long as your loop is unchangingΒ β it calls the same hooks in the same order every timeΒ β there's no problem with using a loop. In fact, it's impossible for React to tell the difference between:
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [c, setC] = useState(0);
const [d, setD] = useState(0);
and
// Don't do this, it's just for illustration
const stuff = [];
for (let n = 0; n < 4; ++n) {
stuff.push(useState(0));
}
Either way, useState is called four times. (That said, I've asked the React documentation team for input on this.)
With that in mind:
You haven't shown useGetApi, but the usual thing is for it to return a tuple (fixed-length array, like useState does) with three elements: a loading flag, the data (if it has it), or an error (if it has one). That is: [loading, data, error]. If loading is false and error is null, that means data is valid.
Assuming a structure like that for useGetApi, you could have useDataLoader load multiple items and aggregate loading flags and error states for you.
There are lots of ways to spin this.
One thing you could do is have useDataLoader return a count of the number of items that are loading, a count of the number of items with errors, and then an element for each call to useGetApi containing the array it returned. So instead of:
const [aLoading, a, aError] = useGetApi("a");
const [bLoading, b, bError] = useGetApi("b");
const [cLoading, c, cError] = useGetApi("c");
// ...
if (aError || bError || cError) {
return <NotFound />;
}
// ...
you could do:
const [
loadingCount,
errorCount,
[, a], // Or [aLoading, a, aError]
[, b], // Or [bLoading, b, bError]
[, c], // Or [cLoading, c, cError]
] = useDataLoader("a", "b", "c");
// ...
if (errorCount > 0) {
return <NotFound />;
}
if (loadingCount > 0) {
return <Loading />;
}
// ...use `a`, `b`, and `c` here...
That gives your component the full picture, both aggregate and detail, so that if you wanted to handle the loading aspect of a, b, and c discretely, you could:
const [
loadingCount,
errorCount,
[aLoading, a],
[bLoading, b],
[cLoading, c],
] = useDataLoader("a", "b", "c");
// ...
if (errorCount > 0) {
return <NotFound />;
}
then rendering (say) a, we have the same thing we had with individual calls:
{aLoading ? <Loading /> : a}
Or if you know you never care about the individual loading flags or error messages, you could return just the counts and the data instead. For some reason, in my head, that would argue for a fixed number of elements: [loadingCount, errorCount, dataArray]:
const [
loadingCount,
errorCount,
[ a, b, c],
] = useDataLoader("a", "b", "c");
// ...
if (errorCount > 0) {
return <NotFound />;
}
// ...
if (loadingCount > 0) {
return <Loading />;
}
// ...use `a`, `b`, and `c` here...
Here's a rough sketch (and only a rough sketch) of how useDataLoader might look if it provided the full details:
const useDataLoader = (...endpoints) => {
let loadingCount = 0;
let errorCount = 0;
const dataArray = endpoints.map((endpoint) => {
const apiResult = useGetApi(endpoint);
const [loading, , error] = apiResult;
if (loading) {
++loadingCount;
}
if (error) {
++errorCount;
}
return apiResult;
});
// Return the number of loading items, number of error items, and then
// the `[loading, data, error]` arrays for each item in the arguments
return [loadingCount, errorCount, ...dataArray];
};
Here's a live example. Again there are a dozen different was to spin this and I would probably never do it quite this way in reality, but you wanted a single "error" state, so... This loads three items via API calls, where each API call has a 1:10 chance of failing. It shows the items as they're loading, but switches to a single erorr state if any of them fail.
const { useState, useEffect } = React;
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const useGetApi = (endpoint) => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
// Add an artificial, varying delay
delay(Math.floor(Math.random() * 3000)).then(() =>
fetch(`https://jsonplaceholder.typicode.com${endpoint}`)
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
// Make one in 10 calls fail, to test the error path
if (Math.random() < 0.1) {
throw new Error();
}
return res.json();
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
);
}, []);
// Fairly common pattern: loading flag, data, error
return [loading, data, error];
};
const useDataLoader = (...endpoints) => {
let loadingCount = 0;
let errorCount = 0;
const dataArray = endpoints.map((endpoint) => {
const apiResult = useGetApi(endpoint);
const [loading, , error] = apiResult;
if (loading) {
++loadingCount;
}
if (error) {
++errorCount;
}
return apiResult;
});
// Return the number of loading items, number of error items, and then
// the `[loading, data, error]` arrays for each item in the arguments
return [loadingCount, errorCount, ...dataArray];
};
const Loading = () => <em>Loading...</em>;
const Example = () => {
// prettier-ignore
const [
loadingCount,
errorCount,
[aLoading, a],
[bLoading, b],
[cLoading, c],
] = useDataLoader("/posts/1", "/posts/2", "/posts/3");
const [ticker, setTicker] = useState(0);
// This is just here to show re-rendering unrelated to the loading of
// posts works just fine
useEffect(() => {
const timer = setInterval(() => {
setTicker((t) => t + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
if (errorCount > 0) {
return <div>Errors loading {errorCount} item(s)...</div>;
}
return (
<div>
<div>
<strong>Ticker</strong>: {ticker} (just to show state unrelated to the data loading)
</div>
<div>
<strong>A</strong>: {aLoading ? <Loading /> : a.title}
</div>
<div>
<strong>B</strong>: {bLoading ? <Loading /> : b.title}
</div>
<div>
<strong>C</strong>: {cLoading ? <Loading /> : c.title}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div>Run several times, there's a 1:10 chance of an artificial API failure, and 3 API calls per run, so roughly one in three times you run it, you'll see an error.</div>
<hr>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Related
I have two components, such that they are:
const ChildOn = (/*...*/) => {
//...
}
const Parent = () => {
const [is42, setIs42] = useState(true)
return (is42 ? <ChildOff ... > : <ChildOn ... />)
}
The definition of ChildOff is no important.
I want to define them as either of the following, yet I can't decide which:
Declares functions used in children, based on a variable/function in the parent, inside each child.
type ChildOnProp = { setIs42: Dispatch<SetStateAction<boolean>> };
const ChildOn = ({ setIs42 }: ChildOnProp) => {
const f1 = () => { setIs42(true); };
return <Text onPress={f1} />;
};
const Parent = () => {
return is42
? <ChildOff setIs42={setIs42} />
: <ChildOn setIs42={setIs42} />;
};
Defines functions used by children, inside the parent.
type ChildOnProps = { func: () => void }
const ChildOn = ({ func }: ChildOnProps) => {
return <Text onPress={func} />
}
const Parent = () => {
const [is42, setIs42] = useState(true)
const f1 = useCallback(() => { setIs42(true) })
const f2 = useCallback(() => { setIs42(false) })
return (is42 ? <ChildOff func={f2} /> : <ChildOn func={f1} />)
}
While (1) is much prettier to me, (2) seems a lot more efficient. Yet I don't know if I'm apt to judge that, since I've read many articles on React contradicting each other on when it's best to use useCallback.
The React community warns against useless memoizing as it could add complexity without any of the benefits in some situations.
In this case, I think it's worth memoizing the callbacks since it will reduce the number of unnecessary renders of the child components, which otherwise could have negative impact, or even introduce issues down over time.
To do this, there's a common pattern of creating a custom hook, often called useToggle, which memoize common setters.
import {useState, useMemo} from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// Defined once, so guaranteed stability
const setters = useMemo(() => ({
toggle: () => setValue(v => !v),
setFalse: () => setValue(false),
setTrue: () => setValue(true),
setValue,
}), [setValue]);
// Defined each time the value changes, so less than every render.
return useMemo(() => ({
...setters,
value
}), [value, setters]);
}
This can be used in the parent as such:
const Parent = () => {
const { value: is42, setTrue, setFalse } = useToggle(true);
return (is42 ? <ChildOff func={setFalse}> : <ChildOn func={setTrue} />);
}
TypeScript will infer all the types automatically, so it works out of the box.
If there's multiple state values that need a toggle callback, we can easily identify each one by not destructuring.
const firstState = useToggle(true);
const secondState = useToggle(true);
//...
return (
<>
<Foo onClick={firstState.toggle} />
<Bar onClick={secondState.toggle} />
</>
);
As a reference, here's a simpler implementation from Shopify.
As for memoizing too much or too little, Meta (Facebook) is apparently experimenting with memoizing everything by default at transpile time, so that devs wouldn't have to think about it.
I am trying to render a component within a component file that relies on data from an outside API. Basically, my return in my component uses a component that is awaiting data, but I get an error of dataRecords is undefined and thus cannot be mapped over.
Hopefully my code will explain this better:
// Component.js
export const History = () => {
const [dateRecords, setDateRecords] = useState(0)
const { data, loading } = useGetRecords() // A custom hook to get the data
useEffect(() => {
fetchData()
}, [loading, data])
const fetchData = async () => {
try {
let records = await data
setDateRecords(records)
} catch (err) {
console.error(err)
}
}
// Below: Render component to be used in the component return
const GameItem = ({ game }) => {
return <div>{game.name}</div>
}
// When I map over dateRecords, I get an error that it is undefined
const renderRecords = async (GameItem) => {
return await dateRecords.map((game, index) => (
<GameItem key={index} game={game} />
))
}
const GameTable = () => {
return <div>{renderRecords(<GameItem />)}</div>
}
return(
// Don't display anything until dateRecords is loaded
{dateRecords? (
// Only display <GameTable/> if the dateRecords is not empty
{dateRecords.length > 0 && <GameTable/>
)
)
}
If dateRecords is meant to be an array, initialize it to an array instead of a number:
const [dateRecords, setDateRecords] = useState([]);
In this case when the API operation is being performed, anything trying to iterate over dateRecords will simply iterate over an empty array, displaying nothing.
You've set the initial state of dateRecords to 0 which is a number and is not iterable. You should set the initial state to an empty array:
const [dateRecords, setDateRecords] = useState([]);
I would like to filter data based on pressing multiple checkbox buttons. Currently only the most recently pressed button works and shows the output instead of also showing outputs from other buttons which are pressed as well.
The state of checkbox buttons works correctly i.e. when clicked it is true, when unclicked it is false - however I am not sure how to connect it with my find function which fetches the data.
const JobsList = (props) => {
const pageNumber = props.pageNumber || 1;
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [page, setPage] = useState(pageNumber);
const [pages, setPages] = useState(1);
useEffect(() => {
const fetchJobs = async () => {
try {
retrieveJobs();
retrievePages();
pages = retrievePages();
setJobs(jobs);
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
setError("Some error occured");
}
};
fetchJobs();
}, [page]);
const retrievePages = () => {
JobDataService.getPage(pages)
.then((response) => {
setPages(response.data.totalPages);
})
.catch((e) => {
console.log(e);
});
};
const Checkbox = ({ type = "checkbox", name, checked = false, onChange }) => {
return (
<input
type={type}
name={name}
checked={checked}
onChange={onChange}
className="btn--position"
/>
);
};
//plain object as state
const [checkedItems, setCheckedItems] = useState({}); //plain object as state
const filteredItems = [];
const handleChange = (event) => {
// updating an object instead of a Map
setCheckedItems({
...checkedItems,
[event.target.name]: event.target.checked,
filteredItems.
});
console.log("from HANDLECHANGE: ", checkedItems)
// console.log(checkedItems[event.target.checked])
// find(event.target.name)
};
useEffect(() => {
console.log("checkedItems from UseEffect: ", checkedItems);
// console.log(checkedItems)
// find(checkedItems)
}, [checkedItems]);
const checkboxes = [
{
name: "π€΅ββοΈ Finance",
key: "financeKey",
label: "financeLabel",
},
{
name: "π©βπ¨ Marketing",
key: "marketingKey",
label: "marketingLabel",
},
{
name: "π¨βπΌ Sales",
key: "salesKey",
label: "salesLabel",
},
{
name: "π₯· Operations",
key: "operationsKey",
label: "financeLabel",
},
{
name: "π¨βπ» Software Engineering",
key: "softwareEngineeringKey",
label: "softwareEngineeringLabel",
},
];
const retrieveJobs = () => {
JobDataService.getAll(page)
.then((response) => {
console.log(response.data);
setJobs(response.data.jobs);
})
.catch((e) => {
console.log(e);
});
};
const refreshList = () => {
retrieveJobs();
};
const find = (query, by) => {
JobDataService.find(query, by)
.then((response) => {
console.log(response.data);
setJobs(response.data.jobs);
// setPage(response.data.total_results)
setPages(response.data.totalPages);
})
.catch((e) => {
console.log(e);
});
};
return (
<div className="hero-container">
<div>
<div className="allButtons-div">
<div className="buttons-div">
<div>
<label>
{checkedItems[""]}
{/* Checked item name : {checkedItems["check-box-1"]}{" "} */}
</label>
{checkboxes.map((item) => (
<label key={item.key}>
{item.name}
<Checkbox
name={item.name}
checked={checkedItems[item.name]}
onChange={handleChange}
/>
</label>
))}
</div>
</div>
</div>
</div>
</div>
);
The function below fetches data from the MongoDB Realm database
const find = (query, by) => {
JobDataService.find(query, by)
.then((response) => {
setJobs(response.data.jobs);
setPages(response.data.totalPages);
})
.catch((e) => {
console.log(e);
});
};
To answer your question, our find() function should be a lot like your retrieveJobs() and retrievePages() functions - they interact with the data layer of your app. That said, if all we're trying to do is filter the data we already have (let's say that retrieveJobs() and retrievePages() fetches all of the jobs and pages you'll need), then we don't need refetch the data based on what's checked in your UI - we simply need to use JavaScript to filter the results by using things you should already be familiar with like map(), sort(), reduce(), filter(), etc.
To go further, this code has a lot of problems. We're using state probably a little more than we should, we're setting state in multiple places redundantly, we're using useEffect() calls that don't do much, the list goes on. I've been there - trying to do things in a "React" way can sometimes result in the opposite effect, where you're lost in endless useState() and useEffect() calls and trying to figure out where to call what event handler and why. I've gone through and made some fairly obvious changes to your code to hopefully get you on the right track to understanding what's going on a little bit better going forward, but I highly recommend going through the React docs and reading this post by Dan Abramov until you understand it (I had to read and re-read a couple paragraphs in that article over and over before it clicked, but I think it will go a long way for you).
Here's the code, it likely still has a lot of problems but best of luck moving forward!
// Since this is a constant set of data, you don't need to include it in your component; remember
// that React components are just regular old functions, so having this constant array value in your
// component means that it's being created anew every render. Let's move it above the component.
const checkboxes = [
{
name: 'π€΅ββοΈ Finance',
key: 'financeKey',
label: 'financeLabel',
},
{
name: 'π©βπ¨ Marketing',
key: 'marketingKey',
label: 'marketingLabel',
},
{
name: 'π¨βπΌ Sales',
key: 'salesKey',
label: 'salesLabel',
},
{
name: 'π₯· Operations',
key: 'operationsKey',
label: 'financeLabel',
},
{
name: 'π¨βπ» Software Engineering',
key: 'softwareEngineeringKey',
label: 'softwareEngineeringLabel',
},
];
// the same principle applies with this smaller component. It doesn't use
// state or props from JobsList, so we should move the component outside of
// your JobsList component to make sure it's not created over and over again
// on each render; let's move it outside of JobsList
const Checkbox = ({ type = 'checkbox', name, checked = false, onChange }) => {
return (
<input
type={type}
name={name}
checked={checked}
onChange={onChange}
className="btn--position"
/>
);
};
// Since these functions seem to interact with the data layer of your app (depending on how JobDataService works of course),
// why don't we try making them functions that return a value from the data layer? Also, it looks like we're using async/await
// syntax in our useEffect call, why don't we try that here?
const retrievePages = async (pages) => {
try {
const response = await JobDataService.getPage(pages);
return response;
} catch (e) {
console.log(e);
}
};
// as an aside, I'm not sure of the difference between pages and page, but we'll keep this the same for now
const retrieveJobs = async (page) => {
try {
const response = await JobDataService.getAll(page);
return response;
} catch (e) {
console.log(e);
}
};
// to hopefully kind of answer your question, this find() function is a lot like the retrieveJobs and retrievePages functions above:
// it just interacts with your data layer - let's try and make it an async function and pull it out of the component so it can return
// results we need. As I explained above, though, if we grabbed all of our jobs and all of our pages already and just need to filter
// the data, why do we need to make a network call for that? Surely we can just use JS functions like filter(), map(), sort(), and reduce()
// to filter the results into the structures that our app needs
const find = async (query, by) => {
try {
const response = await JobDataService.find(query, by);
return response;
} catch (e) {
console.log(e);
}
};
const JobsList = (props) => {
const pageNumber = props.pageNumber || 1;
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
// if pageNumber is passed as a prop, why do we need to store it in state? Presumably the parent component
// of <JobsList /> will handle keeping track of pageNumber, which is why we pass data as props. Let's comment
// out this useState call
// const [page, setPage] = useState(pageNumber);
const [pages, setPages] = useState(1);
useEffect(() => {
const fetchJobs = async () => {
try {
const jobsData = await retrieveJobs(props.page);
const pageData = await retrievePages(pages);
setJobs(jobsData);
setPages(pageData);
// why do we call retrievePages() twice? also, you've decided to store pages in state, so we'll want to use setPages
// for this instead of a normal assignment. let's comment out this assignment
// pages = retrievePages();
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
setError('Some error occured');
}
};
fetchJobs();
}, [props.page, pages]);
const [checkedItems, setCheckedItems] = useState({});
// this is where we could do things like filter based on the checked items instead of making another network call; we have all of our data,
// we just need to do stuff with it (this is contrived but hopfully you get the idea) - every time React re-renders the JobsList component based on a new set of state or props (think something gets checked or unchecked),
// we'll just filter the data we've already fetched based on that new reality
const filteredJobs = jobs.filter((job) => job.id === checkedItems[job.id]);
const filteredPages = pages.filter((page) => page.id === checkedItems[page.id]);
const handleChange = (event) => {
// updating an object instead of a Map
setCheckedItems({
...checkedItems,
[event.target.name]: event.target.checked,
// not sure what this is, perhaps a typo; let's comment it out
// filteredItems.
});
// this find call needs two arguments, no? let's comment it out for now
// find(event.target.name)
};
// not sure of the purpose behind this second useEffect call, let's comment it out
// useEffect(() => {
// console.log("checkedItems from UseEffect: ", checkedItems);
// // console.log(checkedItems)
// // find(checkedItems)
// }, [checkedItems]);
// we'll ignore this for now as well and comment it out, we should probably be refreshing our data based on state or prop updates
// const refreshList = () => {
// retrieveJobs();
// };
return (
<div className="hero-container">
<div>
<div className="allButtons-div">
<div className="buttons-div">
<div>
<label>
{checkedItems['']}
{/* Checked item name : {checkedItems["check-box-1"]}{" "} */}
</label>
{checkboxes.map((item) => (
<label key={item.key}>
{item.name}
<Checkbox
name={item.name}
checked={checkedItems[item.name]}
onChange={handleChange}
/>
</label>
))}
</div>
</div>
</div>
</div>
</div>
);
};
i'm quite new to react-native, i'm trying to implementing a setting screen in my recipe search app. basically the user can choose different filter to avoid some kind of food (like vegan or no-milk ecc.), i thought to make an array with a number for each filter and then in the search page passing the array and apply the filter adding piece of strings for each filter. the thing is: useEffect render the array i'm passing with async-storage empty on the first render, it fulfill only on the second render, how can i take the filled array instead of the empty one?
const [richiesta, setRichiesta] = React.useState('');
const [data, setData] = React.useState([]);
const [ricerca, setRicerca] = React.useState("");
const [preferenza, setPreferenza] = React.useState([]);
let searchString = `https://api.edamam.com/search?q=${ricerca}&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
getMyValue();
getPreferences(preferenza);
},[])
const getMyValue = async () => {
try{
const x = await AsyncStorage.getItem('preferenza')
setPreferenza(JSON.parse(x));
} catch(err){console.log(err)}
}
const getPreferences = (preferenza) => {
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
//useEffect
useEffect(() => {
getRecipes();
}, [ricerca])
//fetching data
const getRecipes = async () => {
const response = await fetch(searchString);
const data = await response.json();
setData(data.hits);
}
//funzione ricerca (solo scrittura)
const onChangeSearch = query => setRichiesta(query);
//funzione modifica stato di ricerca
const getSearch = () => {
setRicerca(richiesta);
}
//barra ricerca e mapping data
return(
<SafeAreaView style={styles.container}>
<Searchbar
placeholder="Cerca"
onChangeText={onChangeSearch}
value={richiesta}
onIconPress={getSearch}
/>
this is the code, it returns "error" because on the first render the array is empty, but on the second render it fills with the value 1. can anyone help me out please?
By listening to the state of the preferenza. You need to exclude the getPreferences(preferenza); out of the useEffect for the first render and put it in it's own useEffect like this:
...
useEffect(() => {
getMyValue();
}, [])
useEffect(() => {
if( !preferenza.length ) return;
getPreferences(preferenza);
}, [preferenza])
i forgot to put the index of the array in
if(preferenza === 1){
searchString = searchString.concat('&health=vegan')
}
else { console.log("error")}
}
thanks for the answer tho, have a nice day!
I have some issue
1- I defined a function that gets data from API and calling it in useEffect, It's work well
But i got this warning in VScode.
React Hook React.useEffect has a missing dependency: 'getOpenOrders'.
Either include it or remove the dependency array.
2- I implement pagination in the FlatList, So when the user reached the end of data List I calling a function to increment the current page, and based on current page updated, getOpenOrders fetched again because i pass currentPage to useEffect to the dependency array
So the issue here is in getOpenOrders should be contacted previous data with the new data so I use Concat method,
It's work well But some time I got a warning tells me there an duplicated data,
And when I use spread [...old, new] not work and I got a big error because Flatlist keyExtractor issue or something,
So can any hero here review my code and tell me what the wrong here with issue 1 - 2
code snippet
const OpenedAppointments = () => {
const [openedAppointment, setOpenedAppointment] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [lastPage, setLastPage] = useState(1);
const [loading, setLoading] = useState(false);
const [isFetch, setIsFetch] = useState(false);
const loadMoreOrders = () => {
if (currentPage <= lastPage - 1) {
setLoading(true);
setCurrentPage((prevPage) => prevPage + 1);
console.log('loadMore??');
}
};
const _renderFooter = () => {
return loading ? (
<View
style={{
paddingVertical: 10,
}}>
<ActivityIndicator color="#000000" size="large" />
</View>
) : null;
};
const getOpenOrders = () => {
let AuthStr =
'Bearer ,,,,';
const headers = {
'Content-Type': 'application/json',
Authorization: AuthStr,
};
Api.post(
`/open_orders?page=${currentPage}`,
{},
{
headers,
},
)
.then((res) => {
let last_Page = res.data.open_orders.last_page;
let allOpenedOrders = res.data.open_orders.data;
console.log('res:', allOpenedOrders);
console.log('last_Page', last_Page);
setLastPage(last_Page);
setOpenedAppointment((prevOpenedOrders) =>
prevOpenedOrders.concat(allOpenedOrders),
); // issue 2
// setOpenedAppointment((prevOpenedOrders) =>[...prevOpenedOrders, allOpenedOrders]);
setLoading(false);
// For pull to refresh
setIsFetch(false);
})
.catch((err) => console.log('err', err));
};
// For pull to refresh
const _refresh = () => {
setIsFetch(true);
getOpenOrders();
};
React.useEffect(() => {
getOpenOrders();
}, [currentPage]); // warning here "issue 1"
return (
<FlatList
showsVerticalScrollIndicator={false}
contentContainerStyle={{flexGrow: 1}}
data={openedAppointment}
ListEmptyComponent={renderEmpty}
renderItem={renderItems}
keyExtractor={(item,index) => String(index)}
ListFooterComponent={_renderFooter}
onEndReached={loadMoreOrders}
onEndReachedThreshold={1}
// For pull to refresh
onRefresh={_refresh}
refreshing={isFetch}
/>
);
};
export default OpenedAppointments;
For Issue 1:
either add the dependency to the array:
React.useEffect(() => {
getOpenOrders();
}, [currentPage, getOpenOrders]);
or use eslint rule as answered by #Matt Aft, It wont make a difference for your usecase
For issue 2:
I would suggest removing duplicates with a Set:
setOpenedAppointment((prevOpenedOrders) =>
Array.from(new Set([...prevOpenedOrders, ...allOpenedOrders]))
);
This will concat your new Orders and your old Orders with spread syntax (...) and will remove duplicates by creating a new Set. (Sets allow only unique items, therefore will remove duplicates. Then you convert it back to an Array with Array.from so you can use it as before
I think you're missing the spread operator on the second array:
setOpenedAppointment((prevOpenedOrders) =>[...prevOpenedOrders, ...allOpenedOrders]);
also issue 1 is because of the react-hooks/exhaustive-deps rule you have enabled, basically there are two ways you can fix this:
wrap getOpenOrders in a useCallback and add it to the dep array in the useEffect
disable the linter for that line
React.useEffect(() => {
getOpenOrders();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]);