Skip to content

Commit

Permalink
Introduce a useSingleton hook (#133)
Browse files Browse the repository at this point in the history
* useSingleton

* Added tests

* Drop onComponent method

* Code style

* Update README.md

* Clarify version for useSingleton, tweak wording
  • Loading branch information
inxilpro authored and atomiks committed Oct 11, 2019
1 parent 5e1090e commit 80a3f9a
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 4 deletions.
36 changes: 34 additions & 2 deletions README.md
Expand Up @@ -291,18 +291,24 @@ You can nest the components like so:
</Tippy>
```

## 📚 `<TippySingleton />`
## 📚 Singleton

Wraps the
[`createSingleton()`](https://atomiks.github.io/tippyjs/addons/#singleton)
method.

Depending on your component tree, you can use one of the following:

### `<TippySingleton />`

If each of your reference elements are adjacent to one another, with no nesting in the tree.

```jsx
import Tippy, {TippySingleton} from '@tippy.js/react';

function App() {
return (
<TippySingleton delay={1000}>
<TippySingleton delay={500}>
<Tippy content="a">
<button />
</Tippy>
Expand All @@ -314,6 +320,32 @@ function App() {
}
```

### `useSingleton()` (v3.1)

If each of your reference elements are not adjacent to one another, or there is nesting in the tree.

```jsx
import Tippy, {useSingleton} from '@tippy.js/react';

function App() {
const singleton = useSingleton({delay: 500});

return (
<>
<Tippy content="a" singleton={singleton}>
<button />
</Tippy>
<button />
<div>
<Tippy content="b" singleton={singleton}>
<button />
</Tippy>
</div>
</>
);
}
```

## 📦 Bundle size

- `popper.js` ≈ 7 kB
Expand Down
26 changes: 25 additions & 1 deletion demo/index.js
@@ -1,6 +1,6 @@
import React, {useState, useEffect} from 'react';
import ReactDOM from 'react-dom';
import Tippy, {TippySingleton} from '../src';
import Tippy, {TippySingleton, useSingleton} from '../src';
import {followCursor} from 'tippy.js';

import 'tippy.js/dist/tippy.css';
Expand Down Expand Up @@ -90,6 +90,28 @@ function Singleton() {
return <TippySingleton delay={500}>{children}</TippySingleton>;
}

function SingletonHook() {
const singleton = useSingleton({delay: 500});
const [count, setCount] = useState(3);

let children = [];
for (let i = 0; i < count; i++) {
children.push(
<Tippy key={i} singleton={singleton} content="Tooltip">
<button>{i}</button>
</Tippy>,
);
}

useEffect(() => {
setInterval(() => {
setCount(count => (count === 5 ? 1 : count + 1));
}, 5000);
}, []);

return <>{children}</>;
}

function FollowCursor() {
return (
<Tippy content="hi" followCursor={true} plugins={[followCursor]}>
Expand All @@ -109,6 +131,8 @@ function App() {
<VisibleProp />
<h2>Singleton dynamic children</h2>
<Singleton />
<h2>Singleton (via useSingleton hook)</h2>
<SingletonHook />
<h2>Plugins</h2>
<FollowCursor />
</>
Expand Down
11 changes: 11 additions & 0 deletions src/Tippy.js
Expand Up @@ -15,12 +15,14 @@ export function Tippy({
className,
plugins,
visible,
singleton,
enabled = true,
multiple = true,
ignoreAttributes = true,
...restOfNativeProps
}) {
const isControlledMode = visible !== undefined;
const isSingletonMode = singleton !== undefined;

const [mounted, setMounted] = useState(false);
const component = useInstance(() => ({
Expand All @@ -39,6 +41,10 @@ export function Tippy({
props.trigger = 'manual';
}

if (isSingletonMode) {
enabled = false;
}

// CREATE
useIsomorphicLayoutEffect(() => {
const instance = tippy(component.ref, props, plugins);
Expand All @@ -53,6 +59,10 @@ export function Tippy({
instance.show();
}

if (isSingletonMode) {
singleton(instance);
}

setMounted(true);

return () => {
Expand Down Expand Up @@ -116,6 +126,7 @@ if (process.env.NODE_ENV !== 'production') {
visible: PropTypes.bool,
enabled: PropTypes.bool,
className: PropTypes.string,
singleton: PropTypes.func,
};
}

Expand Down
45 changes: 45 additions & 0 deletions src/hooks.js
@@ -1,5 +1,6 @@
import {isBrowser, updateClassName} from './utils';
import {useLayoutEffect, useEffect, useRef} from 'react';
import {createSingleton} from 'tippy.js';

export const useIsomorphicLayoutEffect = isBrowser
? useLayoutEffect
Expand Down Expand Up @@ -29,3 +30,47 @@ export function useInstance(initialValue) {

return ref.current;
}

export function useSingleton({
className,
ignoreAttributes = true,
...restOfNativeProps
} = {}) {
const component = useInstance({
instance: null,
instances: [],
renders: 1,
});

const props = {
ignoreAttributes,
...restOfNativeProps,
};

useIsomorphicLayoutEffect(() => {
const {instances} = component;
const instance = createSingleton(instances, props);

component.instance = instance;

return () => {
instance.destroy();
component.instances = instances.filter(i => !i.state.isDestroyed);
};
}, [component.instances.length]);

useIsomorphicLayoutEffect(() => {
if (component.renders === 1) {
component.renders++;
return;
}

component.instance.setProps(props);
});

useUpdateClassName(component, className, component.instances.length);

return instance => {
component.instances.push(instance);
};
}
3 changes: 2 additions & 1 deletion src/index.js
@@ -1,6 +1,7 @@
import tippy from 'tippy.js';
import Tippy from './Tippy';
import TippySingleton from './TippySingleton';
import {useSingleton} from './hooks';

export default Tippy;
export {TippySingleton, tippy};
export {TippySingleton, useSingleton, tippy};

0 comments on commit 80a3f9a

Please sign in to comment.