react initial state calculated from DOM - reactjs

I am using https://www.npmjs.com/package/react-slick and I'm trying to make it est "slidesToShow" dynamically based on available width.
I've managed to get it working, but only after window resize. I need to supply it with an initial state calculated from an element width. The problem is that the element doesn't exist at that point.
This is my code (the interesting parts):
const Carousel = (props: ICarouselProps) => {
const slider = useRef(null);
const recalculateItemsShown = () => {
const maxItems = Math.round(
slider.current.innerSlider.list.parentElement.clientWidth / 136, // content width is just hardcoded for now.
);
updateSettings({...props.settings, slidesToShow: maxItems});
};
useEffect(() => {
window.addEventListener('resize', recalculateItemsShown);
return () => {
window.removeEventListener('resize', recalculateItemsShown);
};
});
const [settings, updateSettings] = useState({
...props.settings,
slidesToShow: //How do I set this properly? slider is null here
});
return (
<div id="carousel" className="carousel">
<Slider {...settings} {...arrows} ref={slider}>
<div className="test-content/>
<div className="test-content/>
/* etc. */
<div className="test-content/>
<div className="test-content/>
<div className="test-content/>
</Slider>
</div>
);
};
export default Carousel;
if I call updateSettings in my useEffect I get an infinite loop.
So I would have to either:
Somehow get the width of an element that hasn't yet been created
or
call updateSettings once after the very first render.

You could have a function that returns maxItems and use that everywhere, so:
const getMaxItems = () => Math.round(slider.current.innerSlider.list.parentElement.clientWidth / 136)
You use its return result within recalculateItemsShown to update the settings.
const recalculateItemsShown = () => {
updateSettings({...props.settings, slidesToShow: getMaxItems()});
};
And you also use its return value to set the state initially.
const [settings, updateSettings] = useState({
...props.settings,
slidesToShow: getMaxItems()
});
If the element doesn't exist initially, you could use useEffect with an empty array as the second argument. This tells useEffect to watch changes to that array and call it everytime it changes, but since its an empty array that never changes, it will only run once - on initial render.
useEffect(() => {
updateSettings({...props.settings, slidesToShow: getMaxItems()});
}, []);
You can read more about skipping applying events here: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

I believe the uselayouteffect hook is designed for this exact use case.
https://reactjs.org/docs/hooks-reference.html#uselayouteffect
It works exactly the same way as useEffect except that it fires after the dom loads so that you can calculate the element width as needed.

Related

How to get the width of an element using ref in react

I'm trying to get the width of an element, and to do this I'm using the following...
export default () => {
const { sidebarOpen } = useContext(AuthContext)
const containerRef = useRef()
const getWidth = () => myRef.current.getBoundingClientRect().width
const [width, setWidth] = useState(0)
console.log(sidebarOpen)
useEffect(() => {
const handleResize = () => setWidth(getWidth())
if(myRef.current) setWidth(getWidth())
window.addEventListener("resize", handleResize)
console.log('abc', myRef.current.offsetWidth)
return () => window.removeEventListener("resize", handleResize)
}, [myRef, sidebarOpen])
console.log(width)
return (
<div ref={containerRef}>
...
</div>
)
}
When the width of the screen is changed in dev tools page resize, it works fine, but when the value of sidebar changes from 'true' to 'false' (or vice-versa). It returns the previous value of the container width. Does anyone know what I'm doing wrong, I need to get the most up to date value of the container width every time it changes, whether this is caused by a change to 'sidebarOpen' or not.
By default, a div element takes the full width of its parent.
I guess you're using flexbox for the sidebar, so when it closes, the element that contains the content (not the sidebar) expands to the full width of the available space.
This might be the issue that you're facing.
The div with flexbox (on the left we have the Sidebar component and on the right the content):
And right now, when there is no Sidebar:
As another example, let's create a div that has no other property rather than backgroundColor:
Eg:
<div style={{backgroundColor: 'red'}}>Hello</div>
And the result:

React Infinite Loading hook, previous trigger

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..

How to use DOMRect with useEffect in React?

I am trying to get the x and y of an element in React. I can do it just fine using DOMRect, but not in the first render. That's how my code is right now:
const Circle: React.FC<Props> = ({ children }: Props) => {
const context = useContext(ShuffleMatchContext);
const circle: React.RefObject<HTMLDivElement> = useRef(null);
const { width, height } = useWindowDimensions();
useEffect(() => {
const rect = circle.current?.getBoundingClientRect();
context.setQuestionPosition({
x: rect!.x,
y: rect!.y,
});
}, [width, height]);
return (
<>
<Component
ref={circle}
>
<>{children}</>
</Component>
</>
);
};
export default Circle;
The problem is that on the first render, domRect returns 0 to everything inside it. I assume this behavior happens because, in the first render, you don't have all parent components ready yet. I used a hook called "useWindowDimensions," and in fact, when you resize the screen, domRect returns the expected values. Can anyone help?
You should use useLayoutEffect(). It allows you to get the correct DOM-related values (i.e. the dimensions of a specific element) since it fires synchronously after all DOM mutations.
useLayoutEffect(() => {
const rect = circle.current?.getBoundingClientRect();
context.setQuestionPosition({
x: rect!.x,
y: rect!.y,
});
}, [width, height]);

Keep Component from Rerendering with Custom Hooks

I have a custom hook useWindowSize that listens to changes of the window size. Once the window size is below a certain threshold, some text should disappear in the Menu. This is implemented in another custom hook useSmallWindowWidth that takes the returned value of useWindowSize and checks whether it is smaller than the threshold.
However, even though only the nested state changes, my component rerenders constantly when the window size changes. Rerendering the menu takes about 50ms on average which adds up if I want to make other components responsive as well.
So how could I keep the component from rerendering? I tried React.memo by passing return prev.smallWindow === next.smallWindow; inside a function, but it didnt work.
My attempt so far:
//this hook is taken from: https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
function useSmallWindowWidth() {
const [width] = useWindowSize();
const smallWindowBreakpoint = 1024;
return width < smallWindowBreakpoint;
}
function Menu() {
const smallWindow = useSmallWindowWidth();
return (
<div>
<MenuItem>
<InformationIcon/>
{smallWindow && <span>Information</span>}
</MenuItem>
<MenuItem>
<MenuIcon/>
{smallWindow && <span>Menu</span>}
</MenuItem>
</div>
);
}
You can try wrapping all your JSX inside a useMemo
function App() {
return useMemo(() => {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}, []);
}
Put in the array in the second argument of the useMemo what variable should make your jsx rerender. If an empty array is set (like in the exemple), the jsx never rerenders.

Tooltip delay on hover with RXJS

I'm trying to add tooltip delay (300msemphasized text) using rxjs (without setTimeout()). My goal is to have this logic inside of TooltipPopover component which will be later be reused and delay will be passed (if needed) as a prop.
I'm not sure how can I add "delay" logic inside of TooltipPopover component using rxjs?
Portal.js
const Portal = ({ children }) => {
const mount = document.getElementById("portal-root");
const el = document.createElement("div");
useEffect(() => {
mount.appendChild(el);
return () => mount.removeChild(el);
}, [el, mount]);
return createPortal(children, el);
};
export default Portal;
TooltipPopover.js
import React from "react";
const TooltipPopover = ({ delay??? }) => {
return (
<div className="ant-popover-title">Title</div>
<div className="ant-popover-inner-content">{children}</div>
);
};
App.js
const App = () => {
return (
<Portal>
<TooltipPopover>
<div>
Content...
</div>
</TooltipPopover>
</Portal>
);
};
Then, I'm rendering TooltipPopover in different places:
ReactDOM.render(<TooltipPopover delay={1000}>
<SomeChildComponent/>
</TooltipPopover>, rootEl)
Here would be my approach:
mouseenter$.pipe(
// by default, the tooltip is not shown
startWith(CLOSE_TOOLTIP),
switchMap(
() => concat(timer(300), NEVER).pipe(
mapTo(SHOW_TOOLTIP),
takeUntil(mouseleave$),
endWith(CLOSE_TOOLTIP),
),
),
distinctUntilChanged(),
)
I'm not very familiar with best practices in React with RxJS, but this would be my reasoning. So, the flow would be this:
on mouseenter$, start the timer. concat(timer(300), NEVER) is used because although after 300ms the tooltip should be shown, we only want to hide it when mouseleave$ emits.
after 300ms, the tooltip is shown and will be closed mouseleave$
if mouseleave$ emits before 300ms pass, the CLOSE_TOOLTIP will emit, but you could avoid(I think) unnecessary re-renders with the help of distinctUntilChanged

Resources