I'm implementing infinite scrolling with search bar.
So I need to bring 10 items each time I call API, incrementing page number by 1.
I used useState to increase pageNumber and called setPageNum before I call 'fetchMore' function in the child component.
The problem is that it always initialized to 1 and it returns the same state, 2 only.
I've been trying every way that I know for about 5hrs but got stucked.
What am I doing wrong and how would I be able to fix this?
const InputTodo = ({ setTodos }) => {
const [inputText, setInputText] = useState("");
const [suggestionList, setSuggestionList] = useState([]);
const [params, setParams] = useState(null);
const [pageNum, setPageNum] = useState(1);
const debouncedText = useDebounce(inputText, 500);
useEffect(() => {
if (!debouncedText) {
return;
}
async function fetchData() {
const { data } = await getSearchSuggestion(debouncedText, 1);
const { result, total, limit, page } = data;
setPageNum(page);
setSuggestionList(result || []);
setParams({
total,
limit
});
}
fetchData();
}, [debouncedText]);
const handleFetchMore = useCallback(
async (query) => {
if (params.total < params.limit * (pageNum - 1)) {
return;
}
try {
const { data } = await getSearchSuggestion(query, pageNum + 1);
setSuggestionList((prev) => [...prev, ...data.result]);
setPageNum(data.page);
} catch (error) {
console.log(error);
}
},
[pageNum, params]
);
//child component
const InputDropdown = ({
query,
suggestions,
pageNum,
setPageNum,
limit,
total,
onFetchMore
}) => {
const [isBottom, setIsBottom] = useState(null);
const bottomObserver = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
if (total < limit * pageNum) {
return;
}
setPageNum((prev) => prev + 1);
onFetchMore(query);
}
},
{ threshold: 0.25 }
);
bottomObserver.current = observer;
}, []);
The problem might be that you are changing the state in the useEffect hook. Since the dependency array of useEffect does not have pageNum in it, it wont run again.
I think it's because the prev value in setPageNum((prev) => prev + 1) is initialised only when the component mount and it's not refreshed.
To solve this problem you can call a handleTrigerObserver() function outside of your useEffect :
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
handleObserverTrigger();
}
},
{ threshold: 0.01 },
);
you can store this function in a useCallback if you want to optimise the recalculate of this function on each render :
const handleObserverTrigger = useCallback( async () => {
const newData = await getPagedData(intersectionCounter * 50, 50, filters)
setData([ ...newData]);
setPageNum(pageNum + 1);
}, [ getPagedData, pageNum, filters]);
Here pageNum value will refresh on each render. in your observale it's set one time
Related
That's the warning in the console,
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.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;
So I'm working on a website that has infinite scrolling and sorting/filtering. The issue is that when the sortBy method is being changed, I need to reset the offset for the pagination for the infinite scrolling page.
Here's the code for the component:
function Listings({ setListingCount, sortBy }) {
const [listings, setListings] = useState([]);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const [apiError, setApiError] = useState(false);
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(true);
const limit = 12;
const observer = useRef();
const lastListingElementRef = useCallback((node) => {
if (isLoading) {
return;
}
if (observer.current) {
observer.current.disconnect();
}
observer.current = new IntersectionObserver((entries) =>
{
if (entries[0].isIntersecting && hasMore) {
setOffset((prevOffset) => prevOffset + limit);
}
});
if (node) {
observer.current.observe(node);
}
}, [isLoading, hasMore]);
function resetListings() {
setListings([]);
setIsInitialLoading(true);
setApiError(false);
setOffset(0);
setHasMore(true);
setListingCount('0');
}
useEffect(() => {
resetListings();
}, [sortBy]);
useEffect(async () => {
try {
setIsLoading(true);
const response = await axios.get(
'listings',
{
params: {
offset,
limit,
sb: sortBy
}
}
);
setListings((prevListingIds) => [
...new Set(
[...prevListingIds, ...response.data.rows]
)
]);
setListingCount(response.data.count);
setHasMore(response.data.rows.length > 0);
setIsLoading(false);
setIsInitialLoading(false);
} catch (error) {
setApiError(true);
setIsLoading(false);
setIsInitialLoading(false);
}
}, [sortBy, offset]);
return();
}
The first useEffect is being used to reset the states if the sortBy changes. The second useEffect is being used to load more data whenever the offset changes or if the sortBy changes. What's happening currently is when the user changes the sortBy when offset is not 0 (let's say 12), then the second useEffect is making a get request with offset = 12 before the first useEffect is able to reset the offset back to 0. But once the offset is reset to 0 by the first useEffect, another get request is made with offset = 0, so now it renders 24 listings instead of 12, and it's in the wrong order.
How should I be dealing with useEffects that aren't guaranteed to run in the order that I expect it to? Is there another way to implement this logic? Thank you for any help that can be provided!
I have this code which updates the state count every 1 seconds.
How can I access the value of the state object in setInterval() ?
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
export default function Timer({objectId}) {
const [object, setObject] = useState({increment: 1});
const [count, setCount] = useState(0);
useEffect(() => {
callAPI(); // update state.object.increment
const timer = setInterval(() => {
setCount(count => count + object.increment); // update state.count with state.object.increment
}, 1000);
return () => clearTimeout(timer); // Help to eliminate the potential of stacking timeouts and causing an error
}, [objectId]); // ensure this calls only once the API
const callAPI = async () => {
return await axios
.get(`/get-object/${objectId}`)
.then(response => {
setObject(response.data);
})
};
return (
<div>{count}</div>
)
}
The only solution I found is this :
// Only this seems to work
const timer = setInterval(() => {
let increment = null;
setObject(object => { increment=object.increment; return object;}); // huge hack to get the value of the 2nd state
setCount(count => count + increment);
}, 1000);
In your interval you have closures on object.increment, you should use useRef instead:
const objectRef = useRef({ increment: 1 });
useEffect(() => {
const callAPI = async () => {
return await axios.get(`/get-object/${objectId}`).then((response) => {
objectRef.current.increment = response.data;
});
};
callAPI();
const timer = setInterval(() => {
setCount((count) => count + objectRef.current);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [objectId]);
Is it safe to do something like the following?
const [foo, setFoo] = useState(undefined)
useEffect(() => {
dispatch(someFunc()).then(response => {
let { someFoo } = response
setFoo(someFoo)
})
}, []) // or }, [bar])
useEffect(() => {
dispatch(anotherFunc()).then(response => {
let { anotherFoo } = response
setFoo(anotherFoo)
})
}, [bar])
The effects are executed in the given order and only the "foo" from the last effect setter will be visible in the UI. For instance, the following component will output bar - 1:
const Component = ({ bar }) => {
const [foo, setFoo] = useState(undefined);
console.log("render", bar, foo);
useEffect(() => {
console.log("effect 1");
let someFoo = bar + 1;
setFoo(someFoo);
}, [bar]);
useEffect(() => {
console.log("effect 2");
let anotherFoo = bar - 1;
setFoo(anotherFoo);
}, [bar]);
return (
<div>
{bar} sets {foo}
</div>
);
};
https://codesandbox.io/s/proud-leaf-g55n9?file=/src/App.js:76-482
EDIT: If you use [] as the dependency array, it will only execute once. If there's an async function inside the effect like fetch, the last executed setFoo will prevail. The following example will display random results in each click:
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(1);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(2);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
Example 2:
https://codesandbox.io/s/jovial-architecture-0ybcz?file=/src/App.js
I'm learning to React Hooks.
And I'm struggling initialize data that I fetched from a server using a custom hook.
I think I'm using hooks wrong.
My code is below.
const useFetchLocation = () => {
const [currentLocation, setCurrentLocation] = useState([]);
const getCurrentLocation = (ignore) => {
...
};
useEffect(() => {
let ignore = false;
getCurrentLocation(ignore);
return () => { ignore = true; }
}, []);
return {currentLocation};
};
const useFetch = (coords) => {
console.log(coords);
const [stores, setStores] = useState([]);
const fetchData = (coords, ignore) => {
axios.get(`${URL}`)
.then(res => {
if (!ignore) {
setStores(res.data.results);
}
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
let ignore = false;
fetchData(ignore);
return () => {
ignore = true;
};
}, [coords]);
return {stores};
}
const App = () => {
const {currentLocation} = useFetchLocation();
const {stores} = useFetch(currentLocation); // it doesn't know what currentLocation is.
...
Obviously, it doesn't work synchronously.
However, I believe there's the correct way to do so.
In this case, what should I do?
I would appreciate if you give me any ideas.
Thank you.
Not sure what all the ignore variables are about, but you can just check in your effect if coords is set. Only when coords is set you should make the axios request.
const useFetchLocation = () => {
// Start out with null instead of an empty array, this makes is easier to check later on
const [currentLocation, setCurrentLocation] = useState(null);
const getCurrentLocation = () => {
// Somehow figure out the current location and store it in the state
setTimeout(() => {
setCurrentLocation({ lat: 1, lng: 2 });
}, 500);
};
useEffect(() => {
getCurrentLocation();
}, []);
return { currentLocation };
};
const useFetch = coords => {
const [stores, setStores] = useState([]);
const fetchData = coords => {
console.log("make some HTTP request using coords:", coords);
setTimeout(() => {
console.log("pretending to receive data");
setStores([{ id: 1, name: "Store 1" }]);
}, 500);
};
useEffect(() => {
/*
* When the location is set from useFetchLocation the useFetch code is
* also triggered again. The first time coords is null so the fetchData code
* will not be executed. Then, when the coords is set to an actual object
* containing coordinates, the fetchData code will execute.
*/
if (coords) {
fetchData(coords);
}
}, [coords]);
return { stores };
};
function App() {
const { currentLocation } = useFetchLocation();
const { stores } = useFetch(currentLocation);
return (
<div className="App">
<ul>
{stores.map(store => (
<li key={store.id}>{store.name}</li>
))}
</ul>
</div>
);
}
Working sandbox (without the comments) https://codesandbox.io/embed/eager-elion-0ki0v