Skip to content

Instantly share code, notes, and snippets.

@atomiks
Last active March 24, 2025 01:14
Show Gist options
  • Select an option

  • Save atomiks/520f4b0c7b537202a23a3059d4eec908 to your computer and use it in GitHub Desktop.

Select an option

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>
</>
);
}
@itayganor

Copy link
Copy Markdown

About LazySingleton.jsx,
won't it mount all linked singletons' content when a single one gets triggered?

@sbusch

sbusch commented May 13, 2020

Copy link
Copy Markdown

Here a TypeScript-enabled version of the code above (tested with TypeScript 3.8.3):

// 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.

import React from 'react';

import Tippy, { TippyProps } from '@tippyjs/react';

// Export own set of props (even if they are the same for now) to enable clients to be more future-proof
export type LazyTippyProps = TippyProps;

export const LazyTippy = (props: LazyTippyProps) => {
  const [mounted, setMounted] = React.useState(false);

  const lazyPlugin = {
    fn: () => ({
      onMount: () => setMounted(true),
      onHidden: () => setMounted(false),
    }),
  };

  const computedProps = { ...props };

  computedProps.plugins = [lazyPlugin, ...(props.plugins || [])];

  if (props.render) {
    const render = props.render; // let TypeScript safely derive that render is not undefined
    computedProps.render = (...args) => (mounted ? render(...args) : '');
  } else {
    computedProps.content = mounted ? props.content : '';
  }

  return <Tippy {...computedProps} />;
};

@atomiks

atomiks commented Jul 2, 2020

Copy link
Copy Markdown
Author

Added a fully lazy singleton example

@christopher-caldwell

Copy link
Copy Markdown

@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
Copy Markdown

@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

silouanwright commented Feb 22, 2021

Copy link
Copy Markdown

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

atomiks commented Feb 22, 2021

Copy link
Copy Markdown
Author

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

@silouanwright

silouanwright commented Feb 24, 2021

Copy link
Copy Markdown

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
Copy Markdown

<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

atomiks commented Mar 2, 2021

Copy link
Copy Markdown
Author

ghost commented Nov 7, 2021

Copy link
Copy Markdown

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