Im trying to make a hook similar to Waypoint.
I simply want to load items and then when the waypoint is out of screen, allow it to load more items if the waypoint is reached.
I can't seem to figure out the logic to have this work properly.
Currently it see the observer state that its on the screen. then it fetches data rapidly.
I think this is because the hook starts at false everytime. Im not sure how to make it true so the data can load. Followed by the opposite when its reached again.
Any ideas.
Here's the hook:
import { useEffect, useState, useRef, RefObject } from 'react';
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
useEffect(() => {
observerRef.current.observe(ref.current);
return () => {
observerRef.current.disconnect();
};
}, [ref]);
return isOnScreen;
}
Here's the use of it:
import React, { useRef } from 'react';
import { WithT } from 'i18next';
import useOnScreen from 'utils/useOnScreen';
interface IInboxListProps extends WithT {
messages: any;
fetchData: () => void;
searchTerm: string;
chatID: string | null;
}
const InboxList: React.FC<IInboxListProps> = ({ messages, fetchData, searchTerm, chatID}) => {
const elementRef = useRef(null);
const isOnScreen = useOnScreen(elementRef);
if (isOnScreen) {
fetchData();
}
const renderItem = () => {
return (
<div className='item unread' key={chatID}>
Item
</div>
);
};
const renderMsgList = ({ messages }) => {
return (
<>
{messages.map(() => {
return renderItem();
})}
</>
);
};
let messagesCopy = [...messages];
//filter results
if (searchTerm !== '') {
messagesCopy = messages.filter(msg => msg.user.toLocaleLowerCase().startsWith(searchTerm.toLocaleLowerCase()));
}
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={elementRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
export default InboxList;
Let's inspect this piece of code
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
We have the following meanings:
.isIntersecting is TRUE --> The element became visible
.isIntersecting is FALSE --> The element disappeared
and
isOnScreen is TRUE --> The element was at least once visible
isOnScreen is FALSE--> The element was never visible
When using a xor (!==) you specify that it:
Was never visible and just became visible
this happens 1 time just after the first intersection
Was visible once and now disappeared
this happens n times each time the element is out of the screen
What you want to do is to get more items each time the element intersects
export default function useOnScreen(ref: RefObject<HTMLElement>, onIntersect: function) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
setIsOnScreen(entry.isIntersecting);
});
}, []);
useEffect(()=?{
if(isOnScreen){
onIntersect();
}
},[isOnScreen,onIntersect])
...
}
and then use it like:
const refetch= useCallback(()=>{
fetchData();
},[fetchData]);
const isOnScreen = useOnScreen(elementRef, refetch);
or simply:
const isOnScreen = useOnScreen(elementRef, fetchData);
If fetchData changes reference for some reason, you might want to use the following instead:
const refetch= useRef(fetchData);
const isOnScreen = useOnScreen(elementRef, refetch);
Remember that useOnScreen has to call it like onIntersect.current()
In InboxList component, what we are saying by this code
if (isOnScreen) {
fetchData();
}
is that, every time InboxList renders, if waypoint is on screen, then initiate the fetch, regardless of whether previous fetch is still in progress.
Note that InboxList could get re-rendered, possibly multiple times, while the fetch is going on, due to many reasons e.g. parent component re-rendering. Every re-rendering will initiate new fetch as long as waypoint is on screen.
To prevent this, we need to keep track of ongoing fetch, something like typical isLoading state variable. Then initiate new fetch only if isLoading === false && isOnScreen.
Alternatively, if it is guaranteed that every fetch will push the waypoint off screen, then we can initiate the fetch only when waypoint is coming on screen, i.e. isOnScreen is changing to true from false :
useEffect(() => {
if (isOnScreen) {
fetchData();
}
}, [isOnScreen]);
However, this will not function correctly if our assumption, that the waypoint goes out of screen on every fetch, does not hold good. This could happen because
pageSize of fetch small and display area can accommodate more
elements
data received from a fetch is getting filtered out due to
client side filtering e.g. searchTerm.
As my assumption. Also you can try this way.
const observeRef = useRef(null);
const [isOnScreen, setIsOnScreen] = useState(false);
const [prevY, setPrevY] = useState(0);
useEffect(()=>{
fetchData();
var option = {
root : null,
rootmargin : "0px",
threshold : 1.0 };
const observer = new IntersectionObserver(
handleObserver(),
option
);
const handleObserver = (entities, observer) => {
const y = observeRef.current.boundingClientRect.y;
if (prevY > y) {
fetchData();
}
setPrevY(y);
}
},[prevY]);
In this case we not focus chat message. we only focus below the chat<div className="item element. when div element trigger by scroll bar the fetchData() calling again and again..
Explain :
In this case we need to use IntersectionObserver for read the element position. we need to pass two parameter for IntersectionObserver.
-first off all in the hanlderObserver you can see boundingClientRect.y. the boundingClientRect method read the element postion. In this case we need only y axis because use y.
when the scrollbar reach div element, y value changed. and then fetchData() is trigger again.
root : This is the root to use for the intersection. rootMargin : Just like a margin property, which is used to provide the margin value to the root either in pixel or in percent (%) . threshold : The number which is used to trigger the callback once the intersection’s area changes to be greater than or equal to the value we have provided in this example .
finally you can add loading status for loading data.
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={observeRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
I hope its correct, i'm not sure. may it's helpful someone. thank you..
Related
I am aware of the multiple similar questions regarding this type of error both on articles and on SO as well but none of them have applied or worked for me up until this point.
So, I am using the React Context API to keep track of a state shared between multiple components:
// imports removed because irrelevant
interface IMarkedPlacesContext {
markedPlaces: Place[]
setMarkedPlaces: React.Dispatch<React.SetStateAction<Place[]>>
markPlaces: (places : Place[]) => Promise<void>
}
interface Props {
children: React.ReactNode
}
const MarkedPlacesContext = createContext<IMarkedPlacesContext>(
{} as IMarkedPlacesContext
)
export function MarkedPlacesContextProvider({ children }: Props) {
const [markedPlaces, setMarkedPlaces] = useState<Place[]>([])
const markPlaces = useCallback( async (places: Place[]) => {
const placesWithRatings : Place[] = await Promise.all(places.map(async (place) => {
const placeRating = await FirestorePlaceRatingService.getPlaceRating(place.placeId)
if (placeRating) {
return({
...place,
rating: placeRating.sumRating/placeRating.reviewCount,
reviewCount: placeRating.reviewCount
})
} else {
return(place)
}
}))
setMarkedPlaces(placesWithRatings)
}, [])
return (
<MarkedPlacesContext.Provider
value={{
markedPlaces,
setMarkedPlaces,
markPlaces,
}}
>
{children}
</MarkedPlacesContext.Provider>
)
}
export function useMarkedPlaces() {
return useContext(MarkedPlacesContext)
}
This component sits on the root level of my app. The function to take note of here is markPlaces which fetches the corresponding rating (if it exists) from my Firestore backend, otherwise, it just gives back the original place object with rating as undefined.
Now the problem occurs when I try to use markPlaces in a useEffect in one of my components:
const PlaceListScreen = () => {
const { markedPlaces, markPlaces, setMarkedPlaces } = useMarkedPlaces()
const { setPlaceInfo, focusPlace } = usePlace()
const { navigate } =
useNavigation<StackNavigationProp<BottomSheetStackParams>>()
useEffect(() => {
if (focusPlace) navigate('PlaceReview')
}, [focusPlace])
// INFINITE LOOP OCCURS HERE! :(
useEffect(() => {
console.log('marking..')
markPlaces(markedPlaces)
}, [])
const handlePressPlace = (place: Place) => {
setPlaceInfo(place)
navigate('PlaceReview')
}
const renderRating = (placeRating: number | undefined) => (
<Subheading style={{flex: 0.1, paddingLeft: 15}}>
{placeRating ? placeRating.toFixed(1) : '-' }
</Subheading>
)
const renderPlaceItem = (place: Place) => (
<TouchableOpacity onPress={() => handlePressPlace(place)}>
<List.Item
title={place.name}
description={place.formattedAddress}
right={() => renderRating(place.rating)}
/>
</TouchableOpacity>
)
return (
<BottomSheetFlatList
data={markedPlaces}
keyExtractor={(place) => place.placeId}
renderItem={(place) => renderPlaceItem(place.item)}
/>
)
}
export default PlaceListScreen
I do not understand why calling markPlaces in this manner causes an infinite loop. Is it something to do with the way I pass in markedPlaces as a param? Or is it something to do with the way I have structured my context?
Any help would be much appreciated :)
UPDATE
I have a hacky work-around of the issue, what I did was to make a local state called places held on the component PlaceListScreen which essentially acts as a reflection of the main value store in the MarkedRestaurants Context. Instead of changing the state held within the context, I change the local state. However, this is an extremely ugly solution which I could see definitely causing problems in the future.
I've been mulling over why having the value stored in context is the cause of the infinite re-renders, but I'm still failing to see why. If someone could help me figure out why this is happening and a more appropriate solution for it that would be awesome.
so im creating a small webApp using reactjs with material ui,
its basically the settings to customize the color, family, weight of any button or text
so im using the Select to display a list of google font families and when the user picks one it gets added to the overall settings object
when i click on the list it takes a few seconds(~4 to 8 to load Im not sure of the issue nor the solution.
So i was wondering if this is due to my internet connection(3Mbs XD) or might it be something else.
a solution may be setting the list to local storage then it only slow loads once or even have a local version of google fonts...
const [googleFonts, setGoogleFonts] = useState();
useEffect(() => {
let isMounted = true;
let cancelToken = axios.CancelToken.source();
const fetchData = async () => {
try {
let res = await client.get();
if (isMounted) {
setGoogleFonts(res.data.items);
}
} catch (e) {
console.error(e);
};
}
fetchData();
return () => {
isMounted = false;
cancelToken.cancel();
}
}, []);
<Select
labelId="FontFamily"
id="FontFamilySelect"
name="FontFamily"
style={{ width: '188px' }}
defaultValue='PT Sans'
value={widgetFont}
onChange={(e) => setWidgetFont(e.target.value)}
>
{googleFonts?.map((option, index) => (
<MenuItem key={index} value={option.family} >
{option.family}
</MenuItem>
))}
</Select>
Thank you
I don't know about the internet connection, because your user experience could vary. But i think there's some issue in this code.
const [googleFonts, setGoogleFonts] = useState();
You code could break at the beginning, so it's better to anticipate some bad outcome. For example,
const [googleFonts, setGoogleFonts] = useState([]);
Make the initial value an array [] might help in your case.
Or add a spinner if it's not loaded try displaying something else.
The reason this is different than your case is that suppose your loading takes 1s. What the user do around that time? And more importantly your UI has to still functional, ex.
spinner drive user attention somewhere else
empty, so not allow user to do anything, but can't be jammed. Maybe your Select is jammed during transition, i don't know.
Follow this code, your UI render when font loaded
import React, { useEffect, useState } from 'react';
const MyComponent = ( ) => {
const [loading, setLoading] = useState(true);
useEffect( ()=> {
document.fonts.load("20px FontFamilyName").then( () => { setLoading(false) } );
}, [])
return (
<React.Fragment>
{ loading
? <div>Loading...</div>
: <main id="mainWrapper">
Reset of Eelements
</main>
}
</React.Fragment>
);
};
export default MyComponent;
Trying to detect if a <section> element is focused in viewport, I'm unable console.log a single true statement. I'm implementing a [isFocused, setIsFocused] hook for this.
This is my window:
I needed so when Section 2 is positioned at the top of the window, a single console.log(true) shows up. But this happens:
This is my implementation:
import React, { useEffect, useRef, useState } from "react";
const SectionII = (props) => {
const sectionRef = useRef();
const [isFocused, setIsFocused] = useState(false);
const handleScroll = () => {
const section = sectionRef.current;
const { y } = section.getBoundingClientRect();
if(!isFocused && y <= 0) {
setIsFocused(true);
console.log(isFocused, y);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section id="mentorship" ref={sectionRef} style={{borderTop: "1px solid"}}>
<h1>Section 2</h1>
<button>Set hash</button>
</section>
);
};
export default SectionII;
Why wouldn't my state by updated to true with setIsFocused(true) inside if(!isFocused && y <= 0)?
Thanks so much for the insight. I'm really stuck.
When you're using any state management in react, you need to ensure that the change is set before attempting to access the new state value. For your example, you immediately console.log(isFocused, y) following your setState function (changes will only appear on the next DOM render). Rather, you should use a callback with the set state function, setIsFocused(true, () => console.log(isFocused, y)).
My image component displays images with a heart over it every time a user submits a search. The heart changes colors if the image is clicked, and should reset to white (default color) when user submits a new search. For some reason, the clicked-color persists even after a search. What am I not understanding about react states? This isn't simply something that changes on the next render. It just stays like that until I change it manually.
const Image = ({image, toggleFav, initialIcon, initialAlt}) => {
const [fav, setFav] = useState(false);
const [heartIcon, setHeartIcon] = useState(initialIcon)
const [heartAlt, setHeartAlt] = useState(initialAlt)
const handleClick = () => {
setFav(fav => !fav);
toggleFav(image.id, fav);
if (heartIcon == "whiteHeartIcon") {
setHeartIcon("redHeartIcon")
}
else {
setHeartIcon("whiteHeartIcon")
}
if (heartAlt == "white heart icon") {
setHeartAlt("red heart icon")
}
else {
setHeartAlt("white heart icon")
}
};
return (
<Grid item xs={4} key={image.id}>
<div className={`${fav ? "fav" : ""}`} onClick={handleClick}>
<div className="imgBox">
<img src={image.url} className="image"/>
<Heart icon={heartIcon} alt={heartAlt} className="heart"/>
</div>
</div>
</Grid>
);
}
This is the handle submit func for the component:
const searchAllImages = async (keyword) => {
const response = await searchImages(keyword);
const imageObjects = response.data.message.map((link, index) => {
let newImage = {
url: link,
id: link,
fav: false
};
return newImage;
});
dispatch({type: 'SET_IMAGES', payload: imageObjects});
};
I render the images through a redux store where it replaces the image state every time a new search is done. The state resides in Store.js where image is initially set to an empty list. The dispatch method comes from Reducer.js where the method is defined.
case "SET_IMAGES":
return {
...state,
images: action.payload
}
Have you tried setting the initial image to a different variable initially, then use a useEffect that checks the image variable for changes, and if it changes assign the value to the variable mentioned above. Anyways more like using useEffect or useCallback for actions.
I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.