Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@atomiks
Last active February 9, 2023 02:10
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save atomiks/520f4b0c7b537202a23a3059d4eec908 to your computer and use it in GitHub Desktop.
Save atomiks/520f4b0c7b537202a23a3059d4eec908 to your computer and use it in GitHub Desktop.
Lazy Tippy
// Will only render the `content` or `render` elements if the tippy is mounted to the DOM.
// Replace <Tippy /> with <LazyTippy /> component and it should work the same.
const LazyTippy = forwardRef((props, ref) => {
const [mounted, setMounted] = useState(false);
const lazyPlugin = {
fn: () => ({
onMount: () => setMounted(true),
onHidden: () => setMounted(false),
}),
};
const computedProps = {...props};
computedProps.plugins = [lazyPlugin, ...(props.plugins || [])];
if (props.render) {
computedProps.render = (...args) => (mounted ? props.render(...args) : '');
} else {
computedProps.content = mounted ? props.content : '';
}
return <Tippy {...computedProps} ref={ref} />;
});
// Partially lazy
// Will only mount all <Tippy /> content once the singleton itself is mounted.
function PartiallyLazySingleton() {
const [source, target] = useSingleton();
const [mounted, setMounted] = useState(false);
return (
<>
<Tippy
onShow={() => setMounted(true)}
onHidden={() => setMounted(false)}
singleton={source}
/>
<Tippy content={mounted ? <ExpensiveComponent /> : ''} singleton={target}>
<button>Hello</button>
</Tippy>
<Tippy content={mounted ? <ExpensiveComponent /> : ''} singleton={target}>
<button>Hello</button>
</Tippy>
</>
);
}
// Fully lazy
// Will only mount the content if that is the currently used singleton content.
function LazySingleton() {
const [source, target] = useSingleton();
const [mountedRef, setMountedRef] = useState(null);
const ref1 = useRef();
const ref2 = useRef();
return (
<>
<Tippy
onTrigger={(_, event) => setMountedRef(event.currentTarget)}
onHidden={() => setMountedRef(null)}
singleton={source}
/>
<Tippy
content={mountedRef === ref1.current ? <ExpensiveComponent /> : ''}
singleton={target}
>
<button ref={ref1}>Hello</button>
</Tippy>
<Tippy
content={mountedRef === ref2.current ? <ExpensiveComponent /> : ''}
singleton={target}
>
<button ref={ref2}>Hello</button>
</Tippy>
</>
);
}
@christopher-caldwell
Copy link

@atomiks Would you mind adding a TS version of your fully lazy example? I tried for awhile, but couldn't get the ref right.

@lukesmurray
Copy link

@christopher-caldwell

import Tippy, { useSingleton } from "@tippyjs/react";
import React, { useRef, useState } from "react";

function LazySingleton() {
  const [source, target] = useSingleton();
  const [mountedRef, setMountedRef] = useState<EventTarget | null>(null);
  const ref1 = useRef<HTMLButtonElement | null>(null); // set these to the type of your tippy contents.
  const ref2 = useRef<HTMLButtonElement | null>(null);
  return (
    <>
      <Tippy
        onTrigger={(_, event) => setMountedRef(event.currentTarget)}
        onHidden={() => setMountedRef(null)}
        singleton={source}
      />
      <Tippy
        content={mountedRef === ref1.current ? <ExpensiveComponent /> : ""}
        singleton={target}
      >
        <button ref={ref1}>Hello</button>
      </Tippy>
      <Tippy
        content={mountedRef === ref2.current ? <ExpensiveComponent /> : ""}
        singleton={target}
      >
        <button ref={ref2}>Hello</button>
      </Tippy>
    </>
  );
}

@silouanwright
Copy link

silouanwright commented Feb 22, 2021

I created a codesandbox for LazyTippy if you want to give it a whirl: https://codesandbox.io/s/mystifying-monad-vul34

Some things of note:

  • moveTransition doesn't work as expected with LazyTippy (you can see this in my sandbox) so I'm going to have to remove usage of this for now on our platform.
  • You may run into some testing issues (I'm using react testing library so I can only comment on that framework):
    • You may now see an act error: Screen Shot 2021-02-22 at 10 57 51 AM
    • You also may not see the necessary content you expect to be rendered.
    • For my purposes using waitFor now fixed both of these issues: await waitFor(() => expect(screen.getByText('popover content')).toBeTruthy());

@atomiks
Copy link
Author

atomiks commented Feb 22, 2021

Instead of onMount to set the mounted state to true, you could try onShow in the Lazy example for the moveTransition.

@silouanwright
Copy link

silouanwright commented Feb 24, 2021

Using LazyTippy, I'm getting the following warning in my testing suite:

%chide() was called on a destroyed instance. This is a no-op but indicates a potential memory leak.

It won't happen running a single test, and even if I delete the one it breaks on, it'll just break on another.

Update: we unfortunately have to back out of using Tippy as our test suite doesn't allow warnings and I'm afraid we wouldn't be able to figure this out on our own.

@wahlstedt
Copy link

<LazyTippy><LazyTippy><div>Test</div></LazyTippy></LazyTippy>

^--- Nesting multiple lazy tippies gives the error "Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"

Any ideas on how to make nesting work?

@atomiks
Copy link
Author

atomiks commented Mar 2, 2021

@danr-za
Copy link

danr-za commented Nov 7, 2021

Can some add a lazy headless singleton example?
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment