Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export layout algorithms (unstable) #169

Merged
merged 3 commits into from
May 23, 2024

Conversation

bitttttten
Copy link
Contributor

PR

Hey! I wanted to kind of use this repo in a headless way, and I thought I can do that by just using the hooks and working around them. It felt easier to customise (or actually, make possible to customise).

So hopefully simply, this feat just exposes some hooks that I can import :)

@igordanchenko
Copy link
Owner

Hi there! Can you describe your headless use case and the customizations you want to make?

I've been considering re-implementing this library to make it RSC-friendly with zero client-side JS bundle footprint in the next major, and I'm kind of surprised no one has asked for this feature so far. Is that what you are after?

@bitttttten
Copy link
Contributor Author

bitttttten commented May 17, 2024

Exactly that, I want to make an SSR friendly page and currently it's not possible (I don't think..).

On different viewports I want to apply different rowConstraints and different defaultContainerWidth, so I was going to do some fiddling and render N different layouts and do some tailwind magic to hook it up :)

Our component looks like this, so we kinda have 5 layouts.

targetRowHeight is a constant of 220.

        <PhotoAlbum
          layout="rows"
          photos={photos}
          spacing={2}
          targetRowHeight={targetRowHeight}
          defaultContainerWidth={isMobile ? 390 : 1320}
          rowConstraints={
            photos.length <= 4
              ? undefined
              : containerWidth => {
                  if (containerWidth < 440) {
                    return { minPhotos: 1, maxPhotos: 1, singleRowMaxHeight: targetRowHeight }
                  }
                  if (containerWidth < 640) {
                    return { minPhotos: 1, singleRowMaxHeight: targetRowHeight }
                  }
                  if (containerWidth < 1024) {
                    return { minPhotos: 2, singleRowMaxHeight: targetRowHeight }
                  }
                  if (containerWidth < 1280) {
                    return { minPhotos: 3, singleRowMaxHeight: targetRowHeight }
                  }
                  return { minPhotos: 4, singleRowMaxHeight: targetRowHeight }
                }
          }
        />

Although the isMobile check isn't very SSR friendly, so we default to isMobile = false for SSR and so on mobile you load in with a different layout then you when the client hydrates it switches to another layout.

So I was thinking of doing 5 hooks with all these container widths, and working out the rest myself :p

@bitttttten
Copy link
Contributor Author

Does my use case make sense? Or do you need more info? Sorry if I am being a bit too terse, it's Friday :)

@igordanchenko
Copy link
Owner

That's actually a different use case, but I see what you are saying. This library is SSR-compatible in the sense that it renders server-side in an SSR build, but client-side layout shift is difficult to avoid.

To achieve zero CLS, you'd need to render N layouts (N <PhotoAlbum> instances) server-side and hide N - 1 of them with CSS media queries. Please take a look at the following example I built some time ago for Next.js - #79 (comment). I'm sure you can easily implement similar approach using Tailwind classes. Also, ensure you utilize breakpoints prop - it is crucial to avoid CLS.

Will this approach satisfy your use case?

@bitttttten
Copy link
Contributor Author

To achieve zero CLS, you'd need to render N layouts (N instances) server-side and hide N - 1 of them with CSS media queries. Please take a look at the following example I built some time ago for Next.js - #79 (comment).

Gotcha! I understand that but in our use case this seems like it will bloat up the initial page size. Our gallery can be about 50 images, so that would mean we'd x5 that initial payload to in theory 250 images. So I just wanted to really optimise and this is kind of where this idea came from.

The example you sent is really nice although it still relies in window innerwidth which we don't have yet. So that is why I was exploring a CSS powered solution for breakpoints. But perhaps we can edit your shared code and see if we can drop in some tailwind classes :)

@igordanchenko
Copy link
Owner

Gotcha! I understand that but in our use case this seems like it will bloat up the initial page size. Our gallery can be about 50 images, so that would mean we'd x5 that initial payload to in theory 250 images. So I just wanted to really optimise and this is kind of where this idea came from.

What is your main concern about the page size bloat? An increase in the JS bundle size will be negligible. An increase in the HTML payload size could be up to 5x, but gzip will shave off most of it. I can imagine there may be some wasted bandwidth due to the browser downloading some srcSet images that it won't use, but there is a chance it may actually optimize this process - would need to test it.

The example you sent is really nice although it still relies in window innerwidth which we don't have yet. So that is why I was exploring a CSS powered solution for breakpoints.

You can consider using CSS container queries, as they already have pretty decent support across major browsers. For example, here is what you can build with @tailwindcss/container-queries

"use client";

import * as React from "react";
import PhotoAlbum from "react-photo-album";

import photos from "@/app/photos";

const TARGET_ROW_HEIGHT = 220;

const LAYOUTS = [
  { breakpoint: 0, className: "!hidden @[0px]:!flex @[440px]:!hidden" },
  { breakpoint: 440, className: "!hidden @[440px]:!flex @[640px]:!hidden" },
  { breakpoint: 640, className: "!hidden @[640px]:!flex @[1024px]:!hidden" },
  { breakpoint: 1024, className: "!hidden @[1024px]:!flex @[1280px]:!hidden" },
  { breakpoint: 1280, className: "!hidden @[1280px]:!flex" },
];

const BREAKPOINTS = LAYOUTS.map(({ breakpoint }) => breakpoint);

export default function Gallery() {
  const [hydrated, setHydrated] = React.useState<number>();

  const handleContainerRef = React.useCallback((node: Element | null) => {
    setHydrated(node ? BREAKPOINTS.findLastIndex((breakpoint) => breakpoint < node.clientWidth) : undefined);
  }, []);

  return (
    <div ref={handleContainerRef} className="@container">
      {LAYOUTS.map(({ breakpoint, className }, index) =>
        hydrated === undefined || hydrated === index ? (
          <PhotoAlbum
            key={index}
            layout="rows"
            photos={photos}
            spacing={2}
            breakpoints={BREAKPOINTS}
            targetRowHeight={TARGET_ROW_HEIGHT}
            componentsProps={hydrated === undefined ? { containerProps: { className } } : undefined}
            defaultContainerWidth={index > 0 ? breakpoint : BREAKPOINTS[1] / 2}
            rowConstraints={
              photos.length > 4
                ? (containerWidth) => {
                    if (containerWidth < 440) {
                      return { maxPhotos: 1 };
                    }
                    if (containerWidth < 640) {
                      return { singleRowMaxHeight: TARGET_ROW_HEIGHT };
                    }
                    if (containerWidth < 1024) {
                      return { minPhotos: 2, singleRowMaxHeight: TARGET_ROW_HEIGHT };
                    }
                    if (containerWidth < 1280) {
                      return { minPhotos: 3, singleRowMaxHeight: TARGET_ROW_HEIGHT };
                    }
                    return { minPhotos: 4, singleRowMaxHeight: TARGET_ROW_HEIGHT };
                  }
                : undefined
            }
          />
        ) : null,
      )}
    </div>
  );
}

@igordanchenko
Copy link
Owner

Also, if you want to fiddle around with the layout algorithm functions you proposed to export in this PR, you can patch-package this library and see if it allows you to achieve your goal (please share the result!). I will hold off merging this PR for now as I'm not really comfortable exporting layout algorithm functions at this point and making them part of public API.

@igordanchenko
Copy link
Owner

On second thought, there is no harm in exporting layout algorithms with the "unstable_" prefix (no semver coverage). I'll go ahead and push the release out.

@igordanchenko igordanchenko changed the title feat: export hooks Export layout algorithms (unstable) May 23, 2024
@igordanchenko igordanchenko merged commit 37f8f15 into igordanchenko:main May 23, 2024
8 checks passed
Copy link

🎉 This PR is included in version 2.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions github-actions bot added the released Implemented and released label May 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
released Implemented and released
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants