This is more of a conceptual question than anything else.
I'm trying to draw a SVG line between two elements in my application. The way I'm currently doing this is by having a top level ref, inside of which I store a ref for each of the child elements indexed by an arbitrary key. I then draw the arrows, deduced from a 2 dimensional array of pairs of these keys, look up the ref, and use the positioning of those elements to create the SVG's coordinates.
The problem with this method is, besides initial render, every render update afterwards uses outdated ref data as the children get rendered (and therefore positioned) after the parent, inside which the SVG layer is contained.
I've already thought of the obvious answers, useEffects, setStates, etc., but there doesn't seem to be a good solution here that I can think of. A lot of the obvious answers don't necessarily work in this case since they cause render loops. Another solution that I initially thought of was breaking down the arrow rendering to within each of the children, but this had problems of its own that initially caused the transition to having all of the refs live at the parent level.
My current solution is to key the SVG Layer component to a state variable, and then change the value of this variable in a useEffect once the final item renders, but this solution feels messy and improper.
Not exact but here's some code of the issue
Parent Component:
export default ({ items }) => {
const [svgKey, setSvgKey] = useState(0);
const pairs = items.reduce((arr, item) => {
for (const subItem of item.subItems) {
arr.push([subItem, item]);
}
}, [])
const itemRefs = useRef({});
return (
<>
{items.map(item => <Row items={items} setSvgKey={setSvgKey} item={item} refs={refs} key={item.key} />}
<SVGLayer key={svgKey} pairs={pairs} refs={refs} />
</>
);
}
SVG Layer
export default ({ pairs, refs }) => (
<svg>
{pairs.map(([a, b], i) => (
<Arrow key={i} a={refs.current[a.key]} b={refs.current[b.key]} />
)}
</svg>
);
Arrow
export default ({ a, b }) => <path d={[math for line coords here]} />;
Row
export default ({ refs, item, items, setSvgKey }) => {
useEffect(() => {
if (item.key === items[items.length - 1].key) {
setSvgKey(key => ++key);
}
});
return (
<div ref={el => (refs.current[item.key] = el)} />
);
}
Related
Sorry for the question.
I am new to react.
I want to get the height of each child then apply the maximum one on all of them.
by the way i do it, i get always the height of the last child.
on other hand i don't know exactly how to force the maximum height on all the children.
i really appreciate the help.
here is my code :
for the parent component :
export default function Parent({data, index}) {
const [testHeight, setTestHeight] = useState([]);
const listRef = useRef(null);
useEffect (() => {
setTestHeight(listRef.current.clientHeight)
})
const {
objects: blocs
} = data
return (
<>
{blocs && Object.values(blocs).map((itemsBlocks, i) => (
<ItemChild dataItem={itemsBlocks}
ref={listRef}
maxHeight= { testHeight}
/>
))}
</>
)
}
for the child component :
const Child = forwardRef(function Child ({dataItem, maxHeight}, ref) {
useEffect(() => {
console.log(ref.current.clientHeight);
})
const {
description,
title
} = dataItem || {}
return (
<>
<div className="class_child" ref={ref} >
{maxHeight}
<p> {title} </p>
<p> {description} </p>
</div>
</>
)
});
export default Child
You have multiple issues.
First off, you are storing the height, from the parent, and, you seem to be giving the same ref to all of your children ?
Each component should have their own ref.
If you want to get all height, you need to change this :
useEffect (() => {
setTestHeight(listRef.current.clientHeight)
})
You are replacing the value of your testHeight unstead of adding values to the array.
You should instead, do this :
useEffect (() => {
setTestHeight((current ) => [...current, listRef.current.clientHeight])
})
I advise you to look how to update hooks by using function, as in my reply, to avoid a lot of later trouble you might meet :)
But, in my opinion, it would be better to update the hook from the children, I'm not sure if you need ref for anything, since you are new to react, I would believe you do not. If you don't need it, then, pass the setTestHeight to children, update it how I showed your, and then you will get all your height, and from the parent, by seeing when your array is full, you will be able to determine your maxheight (that you should store in another hook) and then, update all your children max height
I'm also not sure why your are using forwardref though.
I'm working on something in react and have encountered a challenge I'm not being able to solve myself. I've searched here and others places and I found topics with similar titles but didn't have anything to do with the problem I'm having, so here we go:
So I have an array which will be mapped into React, components, normally like so:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr
return (<>
{arr.map((item, id) => {<ChildComponent props={item} key={id}>})}
</>)
}
but the thing is, there's a state in the parent element which stores the id of one of the ChildComponents that is currently selected (I'm doing this by setting up a context and setting this state inside the ChildComponent), and then the problem is that I have to reference a node inside of the ChildComponent which is currently selected. I can forward a ref no problem, but I also want to assign the ref only on the currently selected ChildComponent, I would like to do this:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and there's a state which holds the id of a selected ChildComponent called selectedObjectId
const selectedRef = createRef();
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={selectedObjectId == id ? selectedRef : null}
>
})}
<someContextProvider />
</>)
}
But I have tried and we can't do that. So how can dynamically assign the ref to only one particular element of an array if a certain condition is true?
You can use the props spread operator {...props} to pass a conditional ref by building the props object first. E.g.
export default ParentComponent = () => {
const selectedRef = useRef(null);
return (
<SomeContextProvider>
{arr.map((item, id) => {
const itemProps = selectedObjectId == id ? { ref: selectedRef } : {};
return (
<ChildComponent
props={item}
key={id}
{...itemProps}
/>
);
})}
<SomeContextProvider />
)
}
You cannot dynamically assign ref, but you can store all of them, and access by id
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and theres a state wich holds the id of a selected ChildComponent called selectedObjectId
let refs = {}
// example of accessing current selected ref
const handleClick = () => {
if (refs[selectedObjectId])
refs[selectedObjectId].current.click() // call some method
}
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={refs[id]}
>
})}
<someContextProvider />
</>)
}
Solution
Like Drew commented in Medets answer, the only solution is to create an array of refs and access the desired one by simply matching the index of the ChildElement with the index of the ref array, as we can see here. There's no way we found to actually move a ref between objects, but performance cost for doing this should not be relevant.
I am building a slider and need assistance with the "best" way of implementing the feature. I have a Slider Component which receives children of SliderItems. I clone the children in Slider Component and add props. When the user clicks next or previous button I use a state isAnimating to determine if the slider is moving and add/remove styles based on isAnimating state but it was causing a re-render of slider items. I need to add animating class without causing a re-render to the enter slide items. Is there a way to implement such feature?
SliderContainer.js
<Slider totalItems={totalItems} itemsInRow={itemsInRow} enableLooping={true} handleSliderMove={handleSliderMove}>
{items.map((item) => {
return <SliderItem key={\`${item.id}\`} data={item} />;
})}
</Slider>
Slider.js
const onSliderControlClick = (direction) => {
const [newIndex, slideOffset] = sliderMove(direction, lowestVisibleIndex, itemsInRow, totalItems);
setisAnimating(true); //Causes rerender
movePercent.current = slideOffset();
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
setisAnimating(false);
setHasMovedOnce(true);
setLowestVisibleIndex(newIndex());
});
}, 750);
};
<div ref={sliderContent} className={`slider-content`} style={getReactAnimationStyle(baseOffset)}>
React.Children.map(children, (child, i) =>
React.cloneElement(child, {
key: child.props.video.id,
viewportIndex: properties.viewportIndex,
viewportPosition: properties.viewportPosition,
})
);
})
</div>
use node-sass library. And make styles.modules.scss file for styling, write down css classes. And conditionally you can apply classes on any element like this.
for example -
className={props.count > 2 ? classes.abc : classes.xyz}
I am using Material-UI and react-window in a project. My issue is, the material-ui menu component does not anchor to the element provided when that element is within a react-window virtualized list. The menu will appear in the upper left corner of the screen instead of anchored to the button that opens it. When using it all in a non-virtualized list, it works as expected. The menu properly anchors to the button that opens it.
Here's an example sandbox. The sandbox is pretty specific to how I'm using the components in question.
Any guidance on how I can resolve this?
Here's a modified version of your sandbox that fixes this:
Here was your initial code in BigList:
const BigList = props => {
const { height, ...others } = props;
const importantData = Array(101 - 1)
.fill()
.map((_, idx) => 0 + idx);
const rows = ({ index, style }) => (
<FancyListItem
index={index}
styles={style}
text="window'd (virtual): "
{...others}
/>
);
return (
<FixedSizeList
height={height}
itemCount={importantData.length}
itemSize={46}
outerElementType={List}
>
{rows}
</FixedSizeList>
);
};
I changed this to the following:
const Row = ({ data, index, style }) => {
return (
<FancyListItem
index={index}
styles={style}
text="window'd (virtual): "
{...data}
/>
);
};
const BigList = props => {
const { height, ...others } = props;
const importantData = Array(101 - 1)
.fill()
.map((_, idx) => 0 + idx);
return (
<FixedSizeList
height={height}
itemCount={importantData.length}
itemSize={46}
outerElementType={List}
itemData={others}
>
{Row}
</FixedSizeList>
);
};
The important difference is that Row is now a consistent component type rather than being redefined with every render of BigList. With your initial code, every render of BigList caused all of the FancyListItem elements to be remounted rather than just rerendered because the function around it representing the "row" type was a new function with each rendering of BigList. One effect of this is that the anchor element you were passing to Menu was no longer mounted by the time Menu tried to determine its position and anchorEl.getBoundingClientRect() was providing an x,y position of 0,0.
You'll notice in the react-window documentation (https://react-window.now.sh/#/examples/list/fixed-size) the Row component is defined outside of the Example component similar to how the fixed version of your code is now structured.
Ryan thanks for your answer! It helped me!
There is another solution:
Defining the parent component as a class component (not a functional component).
My problem was that I was calling the 'Rows' function like so:
<FixedSizeList
height={height}
itemCount={nodes.length}
itemSize={50}
width={width}
overscanCount={10}
>
{({ index, style }) => this.renderListItem(nodes[index], style)}
</FixedSizeList>
The fix was similar to what Ryan suggested:
render() {
...
return <FixedSizeList
height={height}
itemCount={nodes.length}
itemSize={50}
width={width}
overscanCount={10}
itemData={nodes}
>
{this.renderListItem}
</FixedSizeList>
}
renderListItem = ({data,index, style}) => {
...
}
I used the itemData prop to access the nodes array inside the renderListItem function
https://codesandbox.io/s/qYEvQEl0
I try to render a list of draggables, everything seems fine only that I can't figure out how to pass 'index' into rowRenderer
If I do rowRenderer=props => <Row {...props}/>, index is passed in sucessfully.
But if I do:
const SortableRow = SortableElement(Row)
rowRenderer=props => <SortableRow {...props}/> ,
index is blocked somehow, failed to pass into <Row/>
Basically, I don't understand what can go wrong when you wrap your <Row/> component with a HOC? Why some props get to pass in, others not?
Copy the index into a different, custom prop...
rowRenderer = props => {
console.log(props.index);
return <SortableRow {...props} indexCopy={props.index} />;
};
Then, inside the child component, refer to this custom prop instead.
const Row = ({ indexCopy , style }) => {
console.log(indexCopy);
return (
<div style={style}>
<span>drag</span>
<input placeholder={'haha'} />
<span>index={indexCopy || 'undefined'}</span>
</div>
);
};
I'm not too familiar with HOCs, but I suspect that the react-sortable-hoc library is stripping out the implicit index and key values. However, as long as you copy them over into their own custom props, you should be fine.