I am implementing infinity scroll.
When i first enter the screen observerRef will be checked
The page State goes straight to 1 Send two axios requests
I tried many ways, but couldn't solve it, please help
here is my code
function SearchCookRoom() {
const [page, setPage] = useState(0);
const observerRef = useRef(true);
const preventObserverRef = useRef(true);
const endRef = useRef(false);
const observerHandler = entries => {
// console.log(entries);
const target = entries[0];
if (
!endRef.current &&
target.isIntersecting &&
preventObserverRef.current
) {
preventObserverRef.current = false;
setPage(prev => prev + 1);
}
};
useEffect(() => {
const observer = new IntersectionObserver(observerHandler, {
threshold: 0.5,
});
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => {
observer.disconnect();
};
}, []);
const onSaveEnteredItem = item => {
setEnterdItme(item);
};
const getData = useCallback(async () => {
try {
const allCookRoom = await axios({
url: `${
!enterdItme
? `${LIST_URL}?page=${page}&size=15`
: `${SEARCH_URL}/${enterdItme}?page=${page}&size=15`
}`,
});
if (page === allCookRoom.data.totalPages) {
endRef.current = true;
}
if (enterdItme) {
setCookRoom([]);
setPage(0);
}
setCookRoom(prev => {
return [...new Set([...prev, ...allCookRoom.data.content])];
});
preventObserverRef.current = true;
} catch (error) {
console.log(error);
}
}, [page, enterdItme]);
useEffect(() => {
getData();
}, [enterdItme, page]);
console.log(cookRoom);
return (
<>
// Some code
<li ref={observerRef}/>
</>
);
}
observerRef checks if the scroll is at the bottom of the page
The page State starts at 0 As soon as it is rendered it changes to 1 and sends an axios request
Related
I have a React application which uses a Django backend, I have used webSocket to connect with the backend which updates state when there are some changes. But the changes are very rapid, so only the last changes are visible. I want to show the previous message for a certain time before next message is displayed. Here is my code
import React, { useEffect, useState, useRef } from "react";
const Text = () => {
const [message, setMessage] = useState("");
const webSocket = useRef(null);
useEffect(() => {
webSocket.current = new WebSocket("ws://localhost:8000/ws/some_url/");
webSocket.current.onmessage = (res) => {
const data = JSON.parse(res.data);
setMessage(data.message);
};
return () => webSocket.current.close();
}, []);
return <p>{message}</p>;
};
export default Text;
So the message should be visible for certain time (in seconds, for eg - 5 seconds), then the next message should be shown. Any idea how that could be done?
const Text = () => {
const [messages, setMessages] = useState([]);
const currentMessage = messages[0] || "";
const [timer, setTimer] = useState(null);
// webSocket ref missing? ;-)
useEffect(() => {
webSocket.current = new WebSocket("ws://localhost:8000/ws/some_url/");
webSocket.current.onmessage = (res) => {
const data = JSON.parse(res.data);
setMessages((prevState) => [ ...prevState, data.message]);
};
return () => webSocket.current.close();
}, []);
// Remove the current message in 5 seconds.
useEffect(() => {
if (timer || !messages.length) return;
setTimer(setTimeout(() => {
setMessages((prevState) => prevState.slice(1));
setTimer(null);
}, 5000));
}, [messages, timer]);
return <p>{currentMessage}</p>;
};
You can create a custom hook to handle the message transition. Pass as argument the desired time you want to wait before showing the next message. You can use it in other parts of your code:
useQueu.js
const useQueu = time => {
const [current, setCurrent] = useState(null); //--> current message
const queu = useRef([]); //--> messages
useEffect(() => {
const timeout = setTimeout(() => {
setCurrent(queu.current.shift());
}, time);
return () => clearTimeout(timeout);
}, [current]);
const add = obj => {
if (!current) setCurrent(obj); //--> don't wait - render immediately
else {
queu.current.push(obj);
}
};
return [current, add];
};
Text.js
const Text = () => {
const [message, add] = useQue(5000);
const webSocket = useRef(null);
useEffect(() => {
webSocket.current = new WebSocket("ws://localhost:8000/ws/some_url/");
webSocket.current.onmessage = (res) => {
const data = JSON.parse(res.data);
add(data.message); //--> add new message
};
return () => webSocket.current.close();
}, []);
return <p>{message}</p>;
};
Working example
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]);
I'm trying to make active anchors in navbar navigation on scroll. Everything is working until I don't change page and return back to home page, then when I scroll page I get an error from useEffect hook " 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. " How I should cancel all subscriptions ?
useEffect code :
const [headerText, setHeader] = useState(false);
let mount = false;
useEffect(() => {
if (!mount) {
scrollActiveNav();
scrollStickyNav((header) => {
setHeader(header);
});
}
return () => {
mount = true;
};
}, []);
Sticky navbar function :
const scrollStickyNav = (cb) => {
const scrollSticky = window.addEventListener("scroll", () => {
const header = document.getElementById("navbar");
if (window.pageYOffset >= 80) {
header.classList.add("navbar-sticky");
header.classList.remove("absolute");
cb(true);
} else {
header.classList.remove("navbar-sticky");
header.classList.add("absolute");
cb(false);
}
});
return window.removeEventListener("scroll", scrollSticky);
}
Acitve link anchor in navabar function:
const scrollActiveNav = () => {
const activeNav = window.addEventListener('DOMContentLoaded', () => {
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.add('active');
} else {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.remove('active');
}
});
}, options);
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
return window.removeEventListener("DOMContentLoaded", activeNav);
}
Try change this line let mount = false; for this const mount = useRef(false).
const [headerText, setHeader] = useState(false);
let mount = useRef(false);
useEffect(() => {
if (!mount.current) {
scrollActiveNav();
scrollStickyNav((header) => {
setHeader(header);
});
mount.current = true;
}
}, []);
Did you try to do something like this?
useEffect(() => {
scrollActiveNav();
const activeNav = window.addEventListener('DOMContentLoaded', () => {
const options = {
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const id = entry.target.id;
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.add('active');
} else {
document.querySelector(`.navbar-nav li a[href="${id}"]`).classList.remove('active');
}
});
}, options);
document.querySelectorAll('section[id]').forEach((section) => {
observer.observe(section);
});
});
return () => {
window.removeEventListener("DOMContentLoaded", activeNav);
};
}, []);
First of all, I researched the question a lot, but I could not find a solution. I would appreciate if you help.
functional component
I add the code briefly below. this is not full code
state and props
// blog id
const { id } = props.match.params;
// state
const initialState = {
title: "",
category: "",
admin_id: "",
status: false
};
const [form, setState] = useState(initialState);
const [adminList, setAdminList] = useState([]);
const [articleText, setArticleText] = useState([]);
const [me, setMe] = useState([]);
const [locked, setLocked] = useState(true);
const timerRef = useRef(null);
// queries and mutations
const { loading, error, data } = useQuery(GET_BLOG, {
variables: { id }
});
const { data: data_admin, loading: loading_admin } = useQuery(GET_ADMINS);
const [editBlog, { loading: loadingUpdate }] = useMutation(
UPDATE_BLOG
);
const [lockedBlog] = useMutation(LOCKED_BLOG);
multiple useEffect and functions
useEffect(() => {
if (!loading && data) {
setState({
title: data.blog.title,
category: data.blog.category,
admin_id: data.blog.admin.id,
status: data.blog.status
});
setArticleText({
text: data.blog.text
});
}
console.log(data);
}, [loading, data]);
useEffect(() => {
if (!loading_admin && data_admin) {
const me = data_admin.admins.filter(
x => x.id === props.session.activeAdmin.id
);
setAdminList(data_admin);
setMe(me[0]);
}
}, [data_admin, loading_admin]);
useEffect(() => {
const { id } = props.match.params;
lockedBlog({
variables: {
id,
locked: locked
}
}).then(async ({ data }) => {
console.log(data);
});
return () => {
lockedBlog({
variables: {
id,
locked: false
}
}).then(async ({ data }) => {
console.log(data);
});
};
}, [locked]);
// if loading data
if (loading || loading_admin)
return (
<div>
<CircularProgress className="loadingbutton" />
</div>
);
if (error) return <div>Error.</div>;
// update onChange form
const updateField = e => {
setState({
...form,
[e.target.name]: e.target.value
});
};
// editor update
const onChangeEditor = text => {
const currentText = articleText.text;
const newText = JSON.stringify(text);
if (currentText !== newText) {
// Content has changed
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setArticleText({ text: newText });
if (!formValidate()) {
timerRef.current = setTimeout(() => {
onSubmitAuto();
}, 10000);
}
}
};
// auto save
const onSubmitAuto = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
editBlog({
variables: {
id,
admin_id,
title,
text: articleText.text,
category,
status
}
}).then(async ({ data }) => {
console.log(data);
});
};
// validate
const formValidate = () => {
const { title, category } = form;
return !title || !articleText.text || !category;
};
// clear state
const resetState = () => {
setState({ ...initialState });
};
return (
// jsx
)
first issue, when call onSubmitAuto, first useEffect is running again. i dont want this.
because I just want it to work on the first mount.
second issue, if the articleText state has changed before, when mutation it does not mutate the data in the form state. but if the form state changes first, it mutated all the data. I think this issue is the same as the first issue.
I hope I could explain the problem. :/
Ciao, I have an answer to the first issue: when onSubmitAuto is triggered, it calls editBlog that changes loading. And loading is on first useEffect deps list.
If you don't want that, a fix could be something like that:
const isInitialMount = useRef(true);
//first useEffect
useEffect(() => {
if(isInitialMount.current) {
if (!loading && data) {
setState({
title: data.blog.title,
category: data.blog.category,
admin_id: data.blog.admin.id,
status: data.blog.status
});
setArticleText({
text: data.blog.text
});
}
console.log(data);
if (data !== undefined) isInitialMount.current = false;
}
}, [loading, data]);
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
}
}