1

I want to to limit the amount of components a parent container component can fit.

Say I have an item container where I render dozens of Item components. Container has a height limit of 220px and each Item has a height of 50px (for sake of simplicity of this example - real code would have Item of different height).

Using React, I want to limit amount of Item components that container can fit, relative to it's height. Therefore, before placing each Item into the container, I need to verify, whether container doesn't overflow and if it is, stop render more items into it.

I use the following code to achieve this:

  const [canRender, setCanRender] = useState(true);

  useEffect(() => {
    const parent = ref.current.parentElement;
    setCanRender(parent.clientHeight <= parent.scrollHeight);
  }, []);

In a given example, only 3 Item components should be rendered, because container can't fit more of them. My problem is that React renders all components immediately (due to state not being set synchronously?).

Click to see mock example

How can I conditionally render Item component one by one and check for container overflow?

RA.
  • 969
  • 13
  • 36
  • I suppose you could setState the array of Items one by one and have a mount hook that checks scroll height every time? It would most likely defeat the purpose of whatever you're doing though. I'd suggest you use a best guesstimeate for too many items instead and then adjust based on that, or use an infinite scroll plugin or something. Of course, I don't know what you're actually trying to achieve with this, so possibly you have to do this. – user120242 May 24 '20 at 01:17
  • @user120242 Using a hook wouldn't make a difference since I will be dealing with same state problem and Items being rendered in one go. Plugin is not an option for me, since I just want to conditional show or hide items under certain circumstances. So nothing to do with the infinite scroll thing. – RA. May 24 '20 at 01:24
  • The core part of your problem is that you're wanting a lifecycle so you can optimize based on the parent reference. You're not going to be able to get that with a functional syntax since it's stateless. You will need to use a class syntax and use `shouldComponentUpdate` to check the status of the parent. https://stackoverflow.com/questions/40909902/shouldcomponentupdate-in-function-components – Jango May 24 '20 at 01:26
  • I'm not recommending this, but according to what you asked for, you could setState the data array for Items you are rendering for each Item one by one to force a rerender for each Item in sequence. You would obviously hook setState so that it can't bundle the setState updates and hook mount. You would pass a callback function (probably as props) to each Item rendered to hook their mount for doing your scrollHeight checks. Jango's suggestion sounds better, but I don't see how you will deal with explicitly requiring DOM reflow calculations per Item, which is surely going to be a PITA. – user120242 May 24 '20 at 01:33
  • @user120242 Yep, I feel like there is a lack of functionality for such relatively simple task. – RA. May 24 '20 at 01:37
  • I think it's more of an issue of the XY Problem. It's just generally not the way it's designed to be dealt with. I would still ask why you can't try to use a "best guess", since overflow:hidden hides it if you overshoot it, and then readjust your state afterwards when you check the reflowed DOM dimensions, instead of trying to reflow after every DOM element is added and checking one by one. Esp. if it's just a performance thing. A few extra rows isn't going to make a big difference in memory load. – user120242 May 24 '20 at 01:59
  • @user120242 I'm not sure what you mean with the best guess? Next step on this project is to dynamically create another container on overflow, until all Items are there. So the flow would be: `element -> check -> element -> check -> overflow -> create another container -> repeat`... until are items are present in the viewport. That means lots of DOM reflows anyways. – RA. May 24 '20 at 02:11
  • Are you saying you will be partitioning them into separate divs depending on overflow size? Best guess meaning you try to render a capped bulk amount of Items you reasonably "guess" will probably overflow, and then check overflow. You could still do the same thing, but calculate which Items overflowed, and move the Items to your new container element, and just split off the array of Items into your other component. Again, overflow:hidden already hides "overshot" Items, so you don't have to worry about flash of content, when you remove them and render them in the new container. – user120242 May 24 '20 at 02:23
  • Yes, the plan is to partition them into separate containers, but thats future. Like I said, there is no real chance of guessing amount of items that probably will overflow, since they height will be unpredictable in real code (though, they will respect `max-height` of the container), so the reasonable guess would be one `Item` :p – RA. May 24 '20 at 02:42
  • Looks like it wasn't even necessary to do a setState hook to prevent batching. Just setState adding each item one by one and checking scrollHeight in the mount hook was enough. gdh implemented it. – user120242 May 24 '20 at 03:01
  • @user120242 Might be a good solution if list of items is known in advance. Regarding setState hook to prevent batching it would rather be a workaround, since there are plans to batch all setState calls in React 17+. I don't know if this behavior would be configurable. – RA. May 25 '20 at 14:11

1 Answers1

0

Just move the item rendering logic to app.js.

working demo is here

App.js (EDIT: optimised code)

export default function App() {
  const [count, setCount] = useState([]);
  const ref = useRef(null);
  useEffect(() => {
    if (ref.current.clientHeight >= ref.current.scrollHeight) {
      setCount(item => [...item, <Item key={Math.random()} />]);
    }
  }, [count]);

  return (
    <div ref={ref} className="container">
      {count}
    </div>
  );
}

Item.js

const Item = () => {
  return <div className="bar">Bar</div>;
};

export default Item;

p.s. consider not to use math.random for keys

gdh
  • 13,114
  • 2
  • 16
  • 28
  • Thats a good solution, but what if I don't know the `item` array in advance? Say, I'll create a hook that will make certain components use this behavior. How do I find out which items to iterate then? I can think of using a state in the main component and then registering each component on initial render? – RA. May 24 '20 at 17:31
  • @RA wouldn't that not matter? if you have new Items added later, you have to update props/state anyways. Just pull it out of props. reconciliation should handle the optimization and prevent flash of content. Obviously gdh's implementation is hardcoded (and he has misunderstood your goal since he mentioned key=Math.random and his original code wasn't working, hah) and the code has to be adjusted to use (and hook) your props to generate instead, but the idea stays the same (okay right you'll be rewriting the code, but the one by one Item idea is the same). – user120242 May 25 '20 at 14:35
  • @user120242 Array thing was just for demonstration purposes. Say you have a component `A` and component `B`, that are the `children` of `container` *at some point* - thanks to hooks, you can enforce certain behavior of these components. So the idea is to consume `useCheckOverflow` hook in component `A` and component `B`, now it's up to the hook we created to validate whether any of these components can fit into the `container` (usually container = `elementRef.current.parentElement`). If element wont cause overflow - show it, otherwise hide (return null). So we can no arrays here. – RA. May 25 '20 at 22:23
  • Sounds like problem related to Jango's insight. That solution will always have issues, because parent or higher is inherently the one that needs to decide who is allowed to render, and should transform the data to match that, because you are essentially transforming the data used to render children. (Components are generally supposed to be set UI with predictable structure) You'd have to refactor and have callbacks as you mention passed to a parent controller to act in a one-by-one fashion. You'd still basically need something (hardcoded?) "array-like" for check overflow – user120242 May 25 '20 at 22:42
  • @RA I think the question would benefit from you creating a concrete demo displaying the (required) architecture that needs to render in the way you want it to depending on overflow. Otherwise it'll probably just be a discussion that can't get anywhere, because it's too difficult to imagine a PoC of your problem, and for a PoC solution to be provided. – user120242 May 25 '20 at 22:46
  • @user120242 Well, like I said, the problem really boils down to the "notify when element is placed into the DOM" along with some validation. Once element is placed, I would be able to re-check scrollHeight for overflow and depending on that, allow other components to render or not, however, in order to do that, I need to prevent React from placing all components together at one time, since it discards me from looking up what is actually happening in the container. – RA. May 25 '20 at 23:01
  • @user120242 I will try to provide a better demo soon. – RA. May 25 '20 at 23:05
  • @user120242 [There](https://codesandbox.io/s/stoic-snow-5deji) - though the items are being hidden due to the common state check. – RA. May 25 '20 at 23:19
  • So roughly this: https://codesandbox.io/s/sharp-yonath-un9iz it's still basically the same as the one you said won't work, and all I did was move it to another variable and copy/paste the setState count increment code, but I still don't see where in the design it needs to avoid this approach. Normally "x" would be built based on the props or state that component has been given to render or flow data to its children. You could even use this.props.children, if you want this form: .... – user120242 May 26 '20 at 06:51
  • https://codesandbox.io/s/focused-fast-i52kg Using a component. It's important to note, reaching into other components, especially parents and even more especially parent refs, is considered a huge "don't do it" generally in React, and actively discouraged in its design. And the response is usually, "use Props and move the state higher" (with state manipulating callbacks from the parent given as Props to child), because it's almost always the better approach. – user120242 May 26 '20 at 07:12