I need to render a React child component from UV coordinates (normalized coordinates / U & V are typically in [0;1] range)
But I don't know how to get parent dimension during children rendering.
I would like to perform something like (using tsx):
const Child = (props: {u:Number, v:Number}) =>
<circle cx={parentClientWidth*props.u} cy={parentClientHeight*props.v} r="5" fill="black"></circle>;
const Parent = () =>
<svg>
<Child u={0.3} v={0.5} />
</svg>;
I woulder if using a context object could be the right way?...
const Child = (props: {u:Number, v:Number}) => {
const workspace= useContext(WorkspaceContext);
return <circle cx={workspace.width*u} cy={workspace.height*v} r="5"></circle>;
}
Note:
In this simple case I could use percents for my cx and cy coordinates but my real case is much more complicated...
After digging a lot the react documentation, I finally found a way that seems to me not too hacky... But I'm still learning so there might be another better way (regarding performances? readability?...)
Anyway here is the idea of my current solution:
// Shares the dimensions of the workspace through children
const WorkspaceContext = createContext();
/** A child component.
* It should be placed at specified (u,v) within the parent component.
*/
const Child = (props: {u:Number, v:Number}) => {
const workspaceContext = useContext(WorkspaceContext);
return <circle cx={workspaceContext.width*props.u} cy={workspaceContext.height*props.v} r="5" fill="black"></circle>;
};
/** The parent SVG component */
const Parent = () => {
const [dimensions, setDimensions] = useState({
width: undefined,
height:undefined,
outdated: true // Triggers a re render when changed
});
// I'm using the ref callback to get the svg dimensions
const parentRefCallback = (element: SVGSVGElement) => {
if(element) {
const width = element.clientWidth;
const height = element.clientHeight;
if(dimensions.width !== width || dimensions.height !== height) {
setDimensions({width, height, outdated: false});
}
}
};
useEffect(() => {
const handler = () => setDimensions({...dimensions, outdated: true}); // re renders!
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, [])
return <svg ref={parentRefCallback}>
<WorkspaceContext.Provider value={dimensions}>
<Child u={0.3} v={0.5} />
</WorkspaceContext.Provider>
</svg>;
}
Related
I have a component thats opening and showing a modal that I want to reuse because almost everything I need in multiple places. Whats different is 1. data I am iterating through (property names are different) and 2. the button that triggers the modal has different styling. The problem is also that from the parent components I pass a callback, however, I also need to pass a callback to the part where I iterate/render data another callback coming from child component which is why I cannot just render the data iteration as children prop (thus always passing different data). I tried to implement a renderprop but also failed. I hope I explained not too confusing!! How do I do it?
const Parent1 = () => {
const [reportedLine, setReportedLine] = useState(null);
const [availableLines, setAvailableLines] = useState([]);
const [searchResultId, setSearchResultId] = useState('');
return (
<AvailableLinesSelector
data={availableLines}
disabled={searchResultId}
onSelect={setReportedLine}
/>
)
};
const Parent2 = () => {
const [line, setLine] = useState(null);
return (
<AvailableLinesSelector
data={otherData}
disabled={item}
onSelect={setLine}
/>
)
};
const AvailableLinesSelector = ({data, onSelect, disabled}) => {
const [isVisible, setIsVisible] = useState(false);
const [selectedLine, setSelectedLine] = useState('Pick the line');//placeholder should also be flexible
const handleCancel = () => setIsVisible(false);
const handleSelect = (input) => {
onSelect(input)
setSelectedLine(input)
setIsVisible(false);
};
return (
<View>
<Button
title={selectedLine}
//a lot of styling that will be different depending on which parent renders
disabled={disabled}
onPress={() => setIsVisible(true)}
/>
<BottomSheet isVisible={isVisible}>
<View>
{data && data.map(line => (
<AvailableLine //here the properties as name, _id etc will be different depending on which parent renders this component
key={line._id}
line={line.name}
onSelect={handleSelect}
/>
))}
</View>
<Button onPress={handleCancel}>Cancel</Button>
</BottomSheet>
</View>
)
};
You can clone the children and pass additional props like:
React.Children.map(props.children, (child) => {
if (!React.isValidElement(child)) return child;
return React.cloneElement(child, {...child.props, myCallback: callback});
});
Within ParentComponent, I render a chart (ResponsiveLine). I have a function (calculateHeight) calculating the height of some DOM elements of the chart.
To work fine, my function calculateHeight have to be triggered once the chart ResponsiveLine is rendered.
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
How to trigger my function calculateHeight once the chart ResponsiveLine is done rendering?
Here's a simplified code
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
setMarginBottom(calculateHeight(myref));
});
return (
<div ref={myref}>
<ResponsiveLine marginBottom={marginBottom}/>
</div>)
}
EDIT
I can't edit the child ResponsiveLine, it's from a library
You can use the ResizeObserver API to track changes to the dimensions of the box of the div via its ref (specifically the height, which is the block size dimension for content which is in a language with a horizontal writing system like English). I won't go into the details of how the API works: you can read about it at the MDN link above.
The ResponsiveLine aspect of your question doesn't seem relevant except that it's a component you don't control and might change its state asynchronously. In the code snippet demonstration below, I've created a Child component that changes its height after 2 seconds to simulate the same idea.
Code in the TypeScript playground
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.5/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom/client';
// import {useEffect, useRef, useState, type ReactElement} from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {useEffect, useRef, useState} = React;
// You didn't show this function, so I don't know what it does.
// Here's something in place of it:
function calculateHeight (element: Element): number {
return element.getBoundingClientRect().height;
}
function Child (): ReactElement {
const [style, setStyle] = useState<React.CSSProperties>({
border: '1px solid blue',
height: 50,
});
useEffect(() => {
// Change the height of the child element after 2 seconds
setTimeout(() => setStyle(style => ({...style, height: 150})), 2e3);
}, []);
return (<div {...{style}}>Child</div>);
}
function Parent (): ReactElement {
const ref = useRef<HTMLDivElement>(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
if (!ref.current) return;
let lastBlockSize = 0;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!(entry.borderBoxSize && entry.borderBoxSize.length > 0)) continue;
// #ts-expect-error
const [{blockSize}] = entry.borderBoxSize;
if (blockSize === lastBlockSize) continue;
setMarginBottom(calculateHeight(entry.target));
lastBlockSize = blockSize;
}
});
observer.observe(ref.current, {box: 'border-box'});
return () => observer.disconnect();
}, []);
return (
<div {...{ref}}>
<div>height: {marginBottom}px</div>
<Child />
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
reactRoot.render(<Parent />);
</script>
You said,
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
However, parent useEffect does not do that, It fires only after all the children are mounted and their useEffects are fired.
The value of myref is stored in myref.current So your useEffect should be
useEffect(() => {
setMarginBottom(calculateHeight(myref.current));
});
Why don't you send a function to the child component that is called from the useEffect of the child component.
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
someFunction = () => {
setMarginBottom(calculateHeight(myref));
}
return (
<div ref={myref}>
<ResponsiveLine func={someFunction} marginBottom={marginBottom}/>
</div>)
}
// CHILD COMPONENT
const ChildComponent = ({func, marginBotton}) => {
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
func();
}, []);
return <div></div>
}
I am trying to create a component that will track the vertical scroll. The catch is – the actual scroll container is not easily predictable (in this specific case it is neither window, document nor body – it is div#__next, due to CSS overflow rules).
I want to keep the component flexible and self-contained. So I've created a ref with DOM selector as an argument. I know it is far from idiomatic (to say the least), but it suprisingly seems to be working:
// Parent component
import { useRef } from "react"
const Article = (props) => {
const scrollContainerRef = useRef<HTMLElement | null>(
document.querySelector("#__next") // <-- the scroll container reference
)
return (
<SomeContent>
<ScrollToTop treshold={640} ref={scrollContainerRef} />
</SomeContent>
)
// ScrollToTop
const ScrollToTop = forwardRef(
({ treshold }, ref) => {
const [visible, setVisible] = useState(false)
useEffect(() => {
if (ref?.current) {
ref.current.addEventListener("scroll", throttle(toggleVisible, 300))
return () => {
ref.current.removeEventListener("scroll", throttle(toggleVisible, 300))
}
}
}, [])
// …
So what's the problem? the current one is Typescript. I've spent hours trying to get the types right, but to no avail. The parent component is red squigly lines free (unless I pass globalThis, which seems to work at least in CodeSandbox), but the ScrollToTop is compaining whenever I am accessing current property:
Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'.
I've tried to use React.MutableRefObject<HTMLElement | null /* or other T's */>, both in parent and in child, but it didn't help.
Any ideas how to get the types to match? Or is this a silly idea from the beginning?
CodeSandbox demo
Refs might be objects with a .current property, but they might also be functions. So you can't assume that a forwarded ref has a .current property.
I think it's a mistake to use forwardRef at all here. The purpose of forwardRef is to allow a parent component to get access to an element in a child component. But instead, the parent is the one finding the element, and then you're passing it to the child for it to use. I would use a regular state and prop for that:
const Article = (props) => {
const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(() => {
return document.querySelector("#__next");
});
return (
<SomeContent>
<ScrollToTop treshold={640} scrollContainer={scrollContainer} />
</SomeContent>
)
interface ScrollToTopProps {
treshold: number;
scrollContainer: HTMLElement | null;
}
const ScrollToTop = ({ treshold, scrollContainer }: ScrollToTopProps) => {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (scrollContainer) {
const toggle = throttle(toggleVisible, 300);
scrollContainer.addEventListener("scroll", toggle);
return () => {
scrollContainer.removeEventListener("scroll", toggle);
}
}
}, [scrollContainer]);
// ...
}
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]);
I have attached a simplified example that demonstrates my issue:
https://codesandbox.io/s/reactusehook-stack-issue-piq15
I have a parent component that receives a configuration, of which screen should be rendered. the rendered screen should have control over the parent appearance. In the example above I demonstrated it with colors. But the actual use case is flow screen that has next and back buttons which can be controlled by the child.
in the example I define common props for the screens:
type GenericScreenProps = {
setColor: (color: string) => void;
};
I create the first screen, that does not care about the color (parent should default)
const ScreenA = (props: GenericScreenProps) => {
return <div>screen A</div>;
};
I create a second screen that explicitly defines a color when mounted
const ScreenB = ({ setColor }: GenericScreenProps) => {
React.useEffect(() => {
console.log("child");
setColor("green");
}, [setColor]);
return <div>screen B</div>;
};
I create a map to be able to reference the components by an index
const map: Record<string, React.JSXElementConstructor<GenericScreenProps>> = {
0: ScreenA,
1: ScreenB
};
and finally I create the parent, that has a button that swaps the component and sets the default whenever the component changes
const App = () => {
const [screenId, setScreenId] = useState(0);
const ComponentToRender = map[screenId];
const [color, setColor] = useState("red");
React.useEffect(() => {
console.log("parent");
setColor("red"); // default when not set should be red
}, [screenId]);
const onButtonClick = () => setScreenId((screenId + 1) % Object.keys(map).length)
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
<ComponentToRender setColor={setColor} />
</div>
);
};
In this example, the default color should be red, for screen A. and green for the second screen.
However, the color stays red because useEffect is using a stack to execute the code. if you run the code you will see that once the button clicked there will be child followed by parent in log.
I have considered the following solution, but they are not ideal:
each child has to explicitly define the color, no way to enforce it without custom lint rules
convert the parent into a react class component, there has to be a hooks solution
This might be an anti-pattern where child component controls how its parent behave, by I could not identify a way of doing that without replicating the parent container for each screen. the reason I want to keep a single parent is to enable transition between the screens.
If I understood the problem correctly, there is no need to pass down setColor to the children. Making expressions more explicit might make a bit longer code, but I think it helps in readability. As what you shared is a simplified example, please let me know if it fits your real case:
const ScreenA = () => {
return <div>screen A</div>;
};
const ScreenB = () => {
return <div>screen B</div>;
};
const App = () => {
const [screen, setScreen] = useState<"a" | "b">("a");
const [color, setColor] = useState<"red" | "green">("red");
const onButtonClick = () => {
if (screen === "a") {
setScreen("b");
setColor("green");
} else {
setScreen("a");
setColor("red");
}
};
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
{screen === "a" ? <ScreenA /> : <ScreenB />}
</div>
);
};
render(<App />, document.getElementById("root"));