I'm new to the whole React and React hooks staff, I'm trying to reset the value in useState to default if new filters are selected.
const [apartments, setApartments] = React.useState([])
const [page, setPage] = React.useState(1)
const fetchData = (onInit = true, loadMore = true) => {
let setQuery = ''
if (!loadMore) {
setApartments([]) // <--- how to reset to empty?
setPage(1) // <--- Not setting value to 1
}
if (!onInit || query()) {
filtersWrapper.current.querySelectorAll('.select-wrapper select').forEach(item => {
if (item.value) {
setQuery += `&${ item.name }=${ item.value }`
}
})
}
fetch(apiUrl + `?page=${ page }&pageSize=12${ setQuery }`)
.then(res => res.json())
.then(data => {
setApartments([...apartments, ...data.apartments] || [])
setHasNextPage(data.hasNextPage)
setPage(prev => prev + 1)
})
}
Identify the page to pass to fetch from the loadMore argument, not from the state value. Similarly, identify from the argument what to pass to setApartments. This way, all the state gets updated at once, inside the fetch.
const fetchData = (onInit = true, loadMore = true) => {
let setQuery = ''
if (!onInit || query()) {
filtersWrapper.current.querySelectorAll('.select-wrapper select').forEach(item => {
if (item.value) {
setQuery += `&${item.name}=${item.value}`
}
})
}
const pageToNavigateTo = loadMore ? page : 1;
fetch(apiUrl + `?page=${pageToNavigateTo}&pageSize=12${setQuery}`)
.then(res => res.json())
.then(data => {
const initialApartments = loadMore ? apartments : [];
setApartments([...initialApartments, ...data.apartments]);
setHasNextPage(data.hasNextPage);
setPage(pageToNavigateTo + 1);
})
// .catch(handleErrors); // don't forget this
}
I'd also recommend changing the
filtersWrapper.current.querySelectorAll('.select-wrapper select').forEach
from DOM methods to setting React state instead.
Related
I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};
I have states :
const { id } = useParams<IRouterParams>();
const [posts, setPosts] = useState<IPost[]>([]);
const [perPage, setPerPage] = useState(5);
const [fetchError, setFetchError] = useState("");
const [lastPostDate, setLastPostDate] = useState<string | null>(null);
// is any more posts in database
const [hasMore, setHasMore] = useState(true);
and useEffect :
// getting posts from server with first render
useEffect(() => {
console.log(posts);
fetchPosts();
console.log(hasMore, lastPostDate);
return () => {
setHasMore(true);
setLastPostDate(null);
setPosts([]);
mounted = false;
return;
};
}, [id]);
When component change (by id), I would like to clean/reset all states.
My problem is that all states are still the same, this setState functions in useEffect cleaning function doesn't work.
##UPDATE
// getting posts from server
const fetchPosts = () => {
let url;
if (lastPostDate)
url = `http://localhost:5000/api/posts/getPosts/profile/${id}?limit=${perPage}&date=${lastPostDate}`;
else
url = `http://localhost:5000/api/posts/getPosts/profile/${id}?limit=${perPage}`;
api
.get(url, {
headers: authenticationHeader(),
})
.then((resp) => {
if (mounted) {
if (resp.data.length === 0) {
setFetchError("");
setHasMore(false);
setPosts(resp.data);
return;
}
setPosts((prevState) => [...prevState, ...resp.data]);
if (resp.data.length < perPage) setHasMore(false);
setLastPostDate(resp.data[resp.data.length - 1].created_at);
setFetchError("");
}
})
.catch((err) => setFetchError("Problem z pobraniem postów."));
};
if your component isnt unmounted, then the return function inside useEffect will not be called.
if only the "id" changes, then try doing this instead:
useEffect(() => {
// ... other stuff
setHasMore(true);
setLastPostDate(null);
setPosts([]);
return () => { //...code to run on unmount }
},[id]);
whenever id changes, the codes inside useEffect will run. thus clearing out your states.
OK, I fixed it, don't know if it is the best solution, but works...
useEffect(() => {
setPosts([]);
setHasMore(true);
setLastPostDate(null);
return () => {
mounted = false;
return;
};
}, [id]);
// getting posts from server with first render
useEffect(() => {
console.log(lastPostDate, hasMore);
hasMore && !lastPostDate && fetchPosts();
}, [lastPostDate, hasMore]);
Following is the data useEffect is returning in console log:
{
"sql": {
"external": false,
"sql": [
"SELECT\n date_trunc('day', (\"line_items\".created_at::timestamptz AT TIME ZONE 'UTC')) \"line_items__created_at_day\", count(\"line_items\".id) \"line_items__count\"\n FROM\n public.line_items AS \"line_items\"\n GROUP BY 1 ORDER BY 1 ASC LIMIT 10000",
[]
],
"timeDimensionAlias": "line_items__created_at_day",
"timeDimensionField": "LineItems.createdAt",
"order": {
"LineItems.createdAt": "asc"
}
I want to be able to render the above in my react app.
const ChartRenderer = ({ vizState }) => {
let ur = encodeURIComponent(JSON.stringify(vizState.query));
let u = "http://localhost:4000/cubejs-api/v1/sql?query=" + ur;
console.log(u)
useEffect(() => {
if(u !=="http://localhost:4000/cubejs-api/v1/sql?query=undefined") {
fetch(u)
.then(response => (response.json()))
.then(data => console.log(JSON.stringify(data, null, 4)))
}},[u]);
const { query, chartType, pivotConfig } = vizState;
const component = TypeToMemoChartComponent[chartType];
const renderProps = useCubeQuery(query);
return component && renderChart(component)({ ...renderProps, pivotConfig })
};
You would have to use a state variable to persist data and update it whenever the API returns some data. In a functional component, you can use the useState hook for this purpose.
const ChartRenderer = ({ vizState }) => {
// useState takes in an initial state value, you can keep it {} or null as per your use-case.
const [response, setResponse] = useState({});
let ur = encodeURIComponent(JSON.stringify(vizState.query));
let u = "http://localhost:4000/cubejs-api/v1/sql?query=" + ur;
console.log(u)
useEffect(() => {
if(u !=="http://localhost:4000/cubejs-api/v1/sql?query=undefined") {
fetch(u)
.then(response => (response.json()))
.then(data => {
// You can modify the data here before setting it to state.
setResponse(data);
})
}},[u]);
// Use 'response' here or pass it to renderChart
const { query, chartType, pivotConfig } = vizState;
const component = TypeToMemoChartComponent[chartType];
const renderProps = useCubeQuery(query);
return component && renderChart(component)({ ...renderProps, pivotConfig })
};
You can read more about useState in the official documentation here.
Is it an acceptable practice to have two custom react hooks in the same component, one after another?
The issue I am dealing with is as follows:
The first custom hook useBudgetItems will load, but the subsequent one will be undefined. I think I understand why it's happening (my budgetSettings property inside my useBudgetSettings loads after the console.log() statement), but I am not sure how to get around this and whether this is the right approach.
const BudgetCost ({ projectId }) => {
const { budgetCost, loaded } = useBudgetCost({ key: projectId });
const { budgetSettings } = useBudgetSettings({ key: projectId });
const [totalBudget, setTotalBudget] = useState(budgetCost.totalBudget);
const [budgetCosts, setbudgetCosts] = useState(budgetCost.items);
// This will be undefined
console.log(budgetSettings)
if(!loaded) return <div/>
return (
...
...
)
});
My useBudgetCost custom hook is as follow (the useBudgetSettings isn't much different in the mechanics.
const useBudgetCost = ({ key, notifyOnChange }) => {
const [loaded, setIsLoaded] = useState(false)
const { budgetCost, setBudgetCost } = useContext(ProjectContext)
useEffect(() => {
if(key)
return getBudgetCost(key);
},[key]);
const getBudgetCost = (key) => {
let { budgetCosts, loaded } = UseBudgetCostsQuery(key);
setBudgetCost(budgetCosts);
setIsLoaded(loaded);
}
let onBudgetCostChange = (update) => {
let tempBudgetCostItems = copyArrayReference(budgetCost);
tempBudgetCostItems = {
...tempBudgetCostItems,
...update
}
setBudgetCost(tempBudgetCostItems)
if (notifyOnChange)
notifyOnChange(update)
}
return {
loaded,
budgetCost,
onBudgetCostChange
}
}
useBudgetSettings component:
const useBudgetSetting = ({ key }) => {
const [loaded, setIsLoaded] = useState(false)
const { budgetSettings, setBudgetSettings } = useContext(ProjectCondext)
const globalContext = useContext(GlobalReferenceContext);
useEffect(() => {
if(key)
return getBudgetSetting(key);
},[key]);
const getBudgetSetting = (key) => {
let { budgetSettings, loaded } = UseBudgetSettingsQuery(key);
console.log(budgetSettings);
setBudgetSettings(budgetSettings);
setIsLoaded(loaded);
}
const getBudgetReferences = (overrideWithGlobal = false) => {
if(overrideWithGlobal)
return globalContext.getBudgetReferences();
return budgetSettings.map((item) => { return { value: item.key, label: item.costCode } });
}
const getCategoryText = (key) => _.get(_.find(getBudgetReferences(), (bc) => bc.value === key), 'label');
return {
loaded,
budgetSettings,
getCategoryText,
getBudgetReferences
}
}
I am trying to implement infinite scroller using intersection observer in react but the problem i am facing is that in the callback of intersection observer i am not able to read latest value of current 'page' and the 'list' so that i can fetch data for next page.
import ReactDOM from "react-dom";
import "./styles.css";
require("intersection-observer");
const pageSize = 30;
const threshold = 5;
const generateList = (page, size) => {
let arr = [];
for (let i = 1; i <= size; i++) {
arr.push(`${(page - 1) * size + i}`);
}
return arr;
};
const fetchList = page => {
return new Promise(resolve => {
setTimeout(() => {
return resolve(generateList(page, pageSize));
}, 1000);
});
};
let options = {
root: null,
threshold: 0
};
function App() {
const [page, setPage] = useState(1);
const [fetching, setFetching] = useState(false);
const [list, setlist] = useState(generateList(page, pageSize));
const callback = entries => {
if (entries[0].isIntersecting) {
observerRef.current.unobserve(
document.getElementById(`item_${list.length - threshold}`)
);
setFetching(true);
/* at this point neither the 'page' is latest nor the 'list'
*they both have the initial states.
*/
fetchList(page + 1).then(res => {
setFetching(false);
setPage(page + 1);
setlist([...list, ...res]);
});
}
};
const observerRef = useRef(new IntersectionObserver(callback, options));
useEffect(() => {
if (observerRef.current) {
observerRef.current.observe(
document.getElementById(`item_${list.length - threshold}`)
);
}
}, [list]);
return (
<div className="App">
{list.map(l => (
<p key={l} id={`item_${l}`}>
{l}
</p>
))}
{fetching && <p>loading...</p>}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
current behaviour: value of 'page' and 'list' is always equals to the initial state and not the latest value. Infinite scroll is not working after page 2
expected behaviour: In callback function it should read updated value of state 'page' and 'list'.
Here is the working sandbox of this demo https://codesandbox.io/s/sweet-sun-rbcml?fontsize=14&hidenavigation=1&theme=dark
There are primary two problems here, closures and querying the DOM directly.
To solve the closures problem, use functional useState and references:
const listLengthRef = useRef(list.length);
const pageRef = useRef(page);
const callback = useCallback(entries => {
if (entries[0].isIntersecting) {
observerRef.current.unobserve(
document.getElementById(`item_${listLengthRef.current - threshold}`)
);
setFetching(true);
fetchList(pageRef.current + 1).then(res => {
setFetching(false);
setPage(page => page + 1);
setlist(list => [...list, ...res]);
});
}
}, []);
const observerRef = useRef(new IntersectionObserver(callback, options));
useEffect(() => {
listLengthRef.current = list.length;
}, [list]);
useEffect(() => {
pageRef.current = page;
}, [page]);
Although this code works, you should replace document.getElementById with reference, in this case, it will be a reference to the last element of the page.
You can make use of the React setState callback method to guarantee that you will receive the previous value.
Update your callback function as the following and it should work.
const callback = entries => {
if (entries[0].isIntersecting) {
setFetching(true);
setPage(prevPage => {
fetchList(prevPage + 1).then(res => {
setFetching(false);
setlist(prevList => {
observerRef.current.unobserve(document.getElementById(`item_${prevList.length - threshold}`));
return ([...prevList, ...res]);
});
})
return prevPage + 1;
})
}
};
I think the problem is due to the ref keeps reference to the old observer. you need to refresh observer everytime your dependencies gets updated. it relates to closure in js. I would update your app to move the callback inside useEffect
function App() {
const [page, setPage] = useState(1);
const [fetching, setFetching] = useState(false);
const [list, setlist] = useState(generateList(page, pageSize));
const observerRef = useRef(null);
useEffect(() => {
const callback = entries => {
if (entries[0].isIntersecting) {
observerRef.current.unobserve(
document.getElementById(`item_${list.length - threshold}`)
);
setFetching(true);
/* at this point neither the 'page' is latest nor the 'list'
*they both have the initial states.
*/
console.log(page, list);
fetchList(page + 1).then(res => {
setFetching(false);
setPage(page + 1);
setlist([...list, ...res]);
});
}
};
observerRef.current = new IntersectionObserver(callback, options);
if (observerRef.current) {
observerRef.current.observe(
document.getElementById(`item_${list.length - threshold}`)
);
}
}, [list]);
return (
<div className="App">
{list.map(l => (
<p key={l} id={`item_${l}`}>
{l}
</p>
))}
{fetching && <p>loading...</p>}
</div>
);
}