Measure height of a element in semantic react ui - reactjs

I am trying to measure the height of a component, If i try to get the ref and find the offsetHeight, the ref is returned to be null always. so How to measure the height for a react-semantic-ui component.
<Container ref={rf=>this.container=rf} style={{overflowY:"auto"}}>
<Item.Group divided >
<ItemList items={items}></ItemList>
</Item.Group>
</Container>
here this.container is always null
What other ways are there to measure the height of a react-semantic-ui component ?

#bogdan-surai, can you test the following code:
componentDidMount() {
const container = this.container;
if (!container) {
return;
}
const specs = container.getBoundingClientRect();
console.log(specs);
}

#salman.zare Yep. It doesn't work when componentDidMount just have fired. I guess the cause is height really equals 0 at that moment. But we can run setInterval to get height in few time later.
This code works for me
componentDidMount() {
this.intervalId = setInterval(() => {
const container = this.container;
if (!container) {
return;
}
const specs = container.getBoundingClientRect();
console.log(specs);
console.log(this.scrollComponent.offsetHeight);
this.setState(() => ({ height: specs.height }));
}, 100);
}
Then I see it in the browser's console.
DOMRect {x: 0, y: 120, width: 693, height: 0, top: 120, …}
DOMRect {x: 0, y: 16, width: 678, height: 4068, top: 16, …}

You should use this.container.offsetHeight in componentDidMount or componentDidUpdate methods.
It is possible to wrap your components into <div ref={...}></div>. Then you can get height of the div. If your higher component doesn't have margins, height of div and your component will be the same.
Note:
When the ref attribute is used on a custom component declared as a class, the ref callback receives the mounted instance of the component as its argument.
When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument.
You may not use the ref attribute on functional components because they don’t have instances
Returned values from 1 and 2 have different properties and methods. There is a doc https://reactjs.org/docs/refs-and-the-dom.html

Related

Should I be using useEffect to animate on prop value change with React Native Reanimated?

I need a view to animate in to place when the value of some props change using Reanimated in React Native
I've only been able to find animating on, for example, a user tap using an onPress function to change the value of useSharedValue values. But I can't seem to find how to animate when props change. So currently I'm using useEffect.
export const SomeComponent = ({
top,
left
}) => {
const animation = useSharedValue({
top,
left
});
useEffect(() => {
animation.value = { top, left };
}, [top, left]);
const animationStyle = useAnimatedStyle(() => {
return {
top: withTiming(animation.value.top, {
duration: 1000
}),
left: withTiming(animation.value.left, {
duration: 1000
})
};
});
}
In the above code the props can change value, should I be triggering the animation with useEffect like I am currently? Or is there a better way to do it using just Reanimated.
Interesting. This works? I haven't seen it done this way.
Yes, useEffect is a good way to trigger animations. Usually I would do it like this:
const duration = 1000;
export const SomeComponent = ({
top: topProp,
left: leftProp
}) => {
const top = useSharedValue(topProp);
const left = useSharedValue(leftProp);
useEffect(() => {
top.value = withTiming(topProp, { duration });
left.value = withTiming(leftProp, { duration });
}, [topProp, leftProp]);
const animationStyle = useAnimatedStyle(() => ({
top: top.value,
left: left.value,
}));
return <Animated.View style={animationStyle}>
//...
}
The values are split up so that they're primitives. I don't know if this affects performance - I've never used an object as a shared value before.
The styles come straight from the shared values.
The useEffect waits for props to change and then runs the animations.
I've never tried running the animations within the styles - it seems like it shouldn't work to me, but I could be wrong!

How to use ReactDOM.render inside a loop?

I am trying to calculate the height of a component before it is previewed to the user. I found the following method online :
const measureElement = element => {
// Creates the hidden div appended to the document body
const container = document.getElementById('temporal-root')
// Renders the React element into the hidden div
ReactDOM.render(element, container)
// Gets the element size
const height = container.clientHeight
const width = container.clientWidth
return { height, width }
return { height: 0, width: 0 }
}
And this is how I use it:
highlightsMemoized.forEach(section => {
const sectionHeight =
measureElement(
<ComponentSection
section={section}
style={{ width: '350px' }}
/>
).height
})
The problem is that height has always the same value, which is the one of the latest item in the loop. Is there a way to get the correct height pf each item using this approach?

why would changes to the Ref object trigger rerender in this code

as shown in the react-native animation demo on https://reactnative.dev/docs/animated
const fadeAnim = useRef(new Animated.Value(0)).current // Initial value for opacity: 0
React.useEffect(() => {
Animated.timing(
fadeAnim,
{
toValue: 1,
duration: 10000,
}
).start();
}, [fadeAnim])
return (
<Animated.View // Special animatable View
style={{
...props.style,
opacity: fadeAnim, // Bind opacity to animated value
}}
>
{props.children}
</Animated.View>
);
}
the changes to the fadeAnim would trigger rerender of the component, which I don't understand.
the react documentation never mentions the ref object is part of state either.
am I missing something?
Im not sure but I think it will still work even if you remove fadeAnim in the dependency of useEffect. I never tried using refs as a dependency of hooks before and it works as expected.

React page / layout transitions with useLayoutEffect

I have following structure defined in order to animate transitions between layouts based on pathname.
<LayoutTransition pathname={pathname}>
{pathname.includes('/registration') && <RegistrationLayout />}
{pathname.includes('/dashboard') && <DashboardLayout />}
</LayoutTransition>
RegistrationLayout and DashboardLayout have similar structure inside, but they display different pages based on pathname as opposed to layouts.
Inside my LayoutTransition component I have following logic
useLayoutEffect(() => {
// Root paths are "/registration" "/dashboard"
const rootPathname = pathname.split('/')[1];
const rootPrevPathname = prevPathname.current.split('/')[1];
if (rootPathname !== rootPrevPathname) {
/*
* Logic that:
* 1. Animates component to 0 opacity
* 2. Sets activeChildren to null
* 3. Animates component to 1 opacity
*/
}
return () => {
// Sets activeChildren to current one
setActiveChildren(children);
};
}, [pathname]);
/* renders activeChildren || children */
In general this concept works i.e I see my "current" children while animating out then as activeChildren are set to null, I see my "new" children when animating in.
Only issue is that it seems as if when I set setActiveChildren(children); layout re-renders, I see flicker and page that layout was displaying returns to its initial state.
Is there a way to avoid this and sort of freeze children when we are animating out, so no re-render on them happens?
EDIT: Full code snippet from react-native project
Core idea is that we subscribe to router context, when rootPathname changes we animate current layout (children) out and then animate new ones in.
import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
import { Animated } from 'react-native';
import RouterCtx from '../context/RouterCtx';
import useAnimated from '../hooks/useAnimated';
import { durationSlow, easeInQuad } from '../services/Animation';
/**
* Types
*/
interface IProps {
children: React.ReactNode;
}
/**
* Component
*/
function AnimRouteLayout({ children }: IProps) {
const { routerState } = useContext(RouterCtx);
const { rootPathname } = routerState;
const [activeChildren, setActiveChildren] = useState<React.ReactNode>(undefined);
const [pointerEvents, setPointerEvents] = useState(true);
const prevRootPathname = useRef<string | undefined>(undefined);
const [animatedValue, startAnimation] = useAnimated(1, {
duration: durationSlow,
easing: easeInQuad,
useNativeDriver: true
});
function animationLogic(finished: boolean, value: number) {
setPointerEvents(false);
if (finished) {
if (value === 0) {
setActiveChildren(undefined);
startAnimation(1, animationLogic, { delay: 150 });
}
setPointerEvents(true);
}
}
useLayoutEffect(() => {
if (prevRootPathname.current) {
if (rootPathname !== prevRootPathname.current) {
startAnimation(0, animationLogic);
}
}
return () => {
prevRootPathname.current = rootPathname;
setActiveChildren(children);
};
}, [rootPathname]);
return (
<Animated.View
pointerEvents={pointerEvents ? 'auto' : 'none'}
style={{
flex: 1,
opacity: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1] }),
transform: [
{
scale: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [1.1, 1] })
}
]
}}
>
{activeChildren || children}
</Animated.View>
);
}
export default AnimRouteLayout;
First I'll describe what I believe is happening with your current code by laying out the steps that would occur when a user starts at "/registration" and then switches to "/dashboard":
Initial render of AnimRouteLayout with rootPathname='/registration'
initial layout effect is queued
activeChildren is undefined, so children is returned to be rendered
<RegistrationLayout /> is rendered
the queued layout effect executes
prevRootPathname.current is undefined, so no animation
layout effect cleanup is registered with React
User switches to "/dashboard" triggering render of AnimRouteLayout with rootPathname='/dashboard'
since rootPathname is different, a second layout effect is queued
activeChildren is still undefined, so children is returned to be rendered
<RegistrationLayout /> unmounts and <DashboardLayout /> is rendered
the cleanup for the previous layout effect executes
prevRootPathname.current gets set to '/registration'
activeChildren gets set to the previous children causing another render to be queued
the queued layout effect executes and starts the animation
another render of AnimRouteLayout begins due to the activeChildren state change
an additional layout effect is not queued because rootPathname is not different
activeChildren is returned to be rendered
<DashboardLayout /> unmounts
<RegistrationLayout /> remounts with fresh state and is rendered
animation completes and sets activeChildren back to undefined
AnimRouteLayout renders again and this time <DashboardLayout /> will be rendered
Though it would be possible to manage activeChildren in a manner that prevents the re-mounting, I think there is a cleaner way to approach this problem. Rather than trying to freeze the children, I think you would be better off to freeze the pathname. I did a fair amount of experimentation with these ideas when writing this answer. The terminology I came up with to keep this straight is to distinguish between:
targetPathname The path the user has indicated they want to be on
renderPathname The path currently being rendered
Most of the time these paths will be the same. The exception is during the exit transition when renderPathname will have the value of the previous targetPathname. With this approach you would have something like the following:
<AnimRouteLayout targetPathname={pathname}>
{(renderPathname)=> {
return <>
{renderPathname.includes('/registration') && <RegistrationLayout />}
{renderPathname.includes('/dashboard') && <DashboardLayout />}
</>;
}}
</AnimRouteLayout>
and then AnimRouteLayout just needs to manage renderPathname appropriately:
function AnimRouteLayout({ children, targetPathname }) {
const [renderPathname, setRenderPathname] = useState(targetPathname);
// End of animation would set renderPathname to targetPathname
return children(renderPathname);
}
Since I didn't try to make a working example of this, I make no guarantees that I don't have a syntax accident in the above, but I'm fairly confident the approach is sound.

Mocking clientHeight and scrollHeight in React + Enzyme for test

We have a React component called ScrollContainer than calls a prop function when its content is scrolled to the bottom.
Basically:
componentDidMount() {
const needsToScroll = this.container.clientHeight != this.container.scrollHeight
const { handleUserDidScroll } = this.props
if (needsToScroll) {
this.container.addEventListener('scroll', this.handleScroll)
} else {
handleUserDidScroll()
}
}
componentWillUnmount() {
this.container.removeEventListener('scroll', this.handleScroll)
}
handleScroll() {
const { handleUserDidScroll } = this.props
const node = this.container
if (node.scrollHeight == node.clientHeight + node.scrollTop) {
handleUserDidScroll()
}
}
this.container is set as follows in the render method:
<div ref={ container => this.container = container }>
...
</div>
I want to test this logic using Jest + Enzyme.
I need a way to force the clientHeight, scrollHeight and scrollTop properties to be values of my choosing for the test scenario.
With mount instead of shallow I can get these values but they are always 0. I have yet to find any way to set them to anything non zero. I can set the container on wrapper.instance().container = { scrollHeight: 0 } and etc, but this only modifies the test context not the actual component.
Any suggestions would be appreciated!
Jest spyOn can be used to mock getter and setter from version 22.1.0+. See jest docs
I used below code to mock implementation of document.documentElement.scrollHeight
const scrollHeightSpy = jest
.spyOn(document.documentElement, 'scrollHeight', 'get')
.mockImplementation(() => 100);
It returns 100 as scrollHeight value.
JSDOM doesn't do any actual rendering - it just simulates the DOM structure - so things like element dimensions aren't calculated as you might expect. If you grab dimensions via method calls there are ways to mock these within your test. For example:
beforeEach(() => {
Element.prototype.getBoundingClientRect = jest.fn(() => {
return { width: 100, height: 10, top: 0, left: 0, bottom: 0, right: 0 };
});
});
This obviously won't work in your example. It may be possible to override these properties on the elements and mock changes to them; but I suspect that wouldn't lead to particularly meaningful/useful tests.
See also this thread
In a scenario where you are storing the value as ref, either with createRef or useRef, you might be able to get away with mocking createRef or useRef directly:
import { useRef } from 'react';
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn(),
}));
test('test ref', () => {
useRef.mockReturnValue(/* value of scrollHeight */);
...
});

Resources