Skip to content

Commit

Permalink
fix: correct refs adding and removing
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Dec 25, 2023
1 parent 43dd323 commit 47cbc58
Show file tree
Hide file tree
Showing 6 changed files with 9,560 additions and 43 deletions.
Binary file modified .DS_Store
Binary file not shown.
93 changes: 53 additions & 40 deletions README.md
Expand Up @@ -17,72 +17,82 @@
---

> Keep in mind that useRef doesn't notify you when its content changes.
Mutating the .current property doesn't cause a re-render.
If you want to run some code when React attaches or detaches a ref to a DOM node,
you may want to use ~~a callback ref instead~~ .... __useCallbackRef__ instead.
> Mutating the .current property doesn't cause a re-render.
> If you want to run some code when React attaches or detaches a ref to a DOM node,
> you may want to use ~~a callback ref instead~~ .... **useCallbackRef** instead.
[Hooks API Reference](https://reactjs.org/docs/hooks-reference.html#useref)

Read more about `use-callback` pattern and use cases:
- https://dev.to/thekashey/the-same-useref-but-it-will-callback-8bo
Read more about `use-callback` pattern and use cases:

- https://dev.to/thekashey/the-same-useref-but-it-will-callback-8bo

This library exposes helpers to handle any case related to `ref` _lifecycle_

- `useCallbackRef` - react on hook change
- `mergeRefs` - merge multiple refs together. For, actually, fork
- `transformRef` - transform one ref to anther
- `refToCallback` - convert RefObject to an old callback-style ref
- `assignRef` - assign value to the ref, regardless of it's form
- `useCallbackRef` - react on a ref change (replacement for `useRef`)
- `createCallbackRef` - - low level version of `useCallbackRef`
- `useMergeRefs` - merge multiple refs together creating a stable return ref
- `mergeRefs` - low level version of `useMergeRefs`
- `useTransformRef` - transform one ref to another (replacement for `useImperativeHandle`)
- `transformRef` - low level version of `useTransformRef`
- `useRefToCallback` - convert RefObject to an old callback-style ref
- `refToCallback` - low level version of `useRefToCallback`
- `assignRef` - assign value to the ref, regardless it is RefCallback or RefObject

All functions are tree shakable, but even together it's __less then 300b__.
All functions are tree shakable, but even together it's **less then 300b**.

# API
💡 Some commands are hooks based, and returns the same refs/functions every render.

💡 Some commands are hooks based, and returns the same refs/functions every render.
But some are not, to be used in classes or non-react code.

## useRef API

🤔 Use case: every time you have to react to ref change

API is 99% compatible with React `createRef` and `useRef`, and just adds another argument - `callback`,
which would be called on __ref update__.
which would be called on **ref update**.

#### createCallbackRef - to replace React.createRef
- `createCallbackRef(callback)` - would call provided `callback` when ref is changed.

- `createCallbackRef(callback)` - would call provided `callback` when ref is changed.

#### useCallbackRef - to replace React.useRef

- `useCallbackRef(initialValue, callback)` - would call provided `callback` when ref is changed.

> `callback` in both cases is `callback(newValue, oldValue)`. Callback would not be called if newValue and oldValue is the same.
```js
import {useRef, createRef, useState} from 'react';
import {useCallbackRef, createCallbackRef} from 'use-callback-ref';
import { useRef, createRef, useState } from 'react';
import { useCallbackRef, createCallbackRef } from 'use-callback-ref';

const Component = () => {
const [,forceUpdate] = useState();
const [, forceUpdate] = useState();
// I dont need callback when ref changes
const ref = useRef(null);
const ref = useRef(null);

// but sometimes - it could be what you need
const anotherRef = useCallbackRef(null, () => forceUpdate());
useEffect( () => {

useEffect(() => {
// now it's just possible
}, [anotherRef.current]) // react to dom node change
}
}, [anotherRef.current]); // react to dom node change
};
```

💡 You can use `useCallbackRef` to convert RefObject into RefCallback, creating bridges between the old and the new code

```js
// some old component
const onRefUpdate = (newValue) => {...}
const onRefUpdate = (newRef) => {...}
const refObject = useCallbackRef(null, onRefUpdate);
// ...
<SomeNewComponent ref={refObject}/>
```

## assignRef

🤔 Use case: every time you need to assign ref manually, and you dont know the shape of the ref

`assignRef(ref, value)` - assigns `values` to the `ref`. `ref` could be RefObject or RefCallback.
Expand All @@ -92,58 +102,61 @@ const refObject = useCallbackRef(null, onRefUpdate);
🚫 ref(value) // but what if it's a object ref?
import {assignRef} from "use-callback-ref";
✅ assignRef(ref, value);
✅ assignRef(ref, value);
```

## useTransformRef (to replace React.useImperativeHandle)
🤔 Use case: ref could be different.

🤔 Use case: ref could be different.
`transformRef(ref, tranformer):Ref` - return a new `ref` which would propagate all changes to the provided `ref` with applied `transform`

```js
// before
const ResizableWithRef = forwardRef((props, ref) =>
<Resizable {...props} ref={i => i && ref(i.resizable)}/>
);
const ResizableWithRef = forwardRef((props, ref) => <Resizable {...props} ref={(i) => i && ref(i.resizable)} />);

// after

const ResizableWithRef = forwardRef((props, ref) =>
<Resizable {...props} ref={transformRef(ref, i => i ? i.resizable : null)}/>
);
const ResizableWithRef = forwardRef((props, ref) => (
<Resizable {...props} ref={transformRef(ref, (i) => (i ? i.resizable : null))} />
));
```

## refToCallback

`refToCallback(ref: RefObject): RefCallback` - for compatibility between the old and the new code.
For the compatibility between `RefCallback` and RefObject use `useCallbackRef(undefined, callback)`
For the compatibility between `RefCallback` and RefObject use `useCallbackRef(undefined, callback)`

## useMergeRefs

`mergeRefs(refs: arrayOfRefs, [defaultValue]):ReactMutableRef` - merges a few refs together

When developing low level UI components, it is common to have to use a local ref but also support an external one using React.forwardRef. Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility.

```js
import React from 'react'
import {useMergeRefs} from 'use-callback-ref'
import React from 'react';
import { useMergeRefs } from 'use-callback-ref';

const MergedComponent = React.forwardRef((props, ref) => {
const localRef = React.useRef();
// ...
// both localRef and ref would be populated with the `ref` to a `div`
return <div ref={useMergeRefs([localRef, ref])} />
})
return <div ref={useMergeRefs([localRef, ref])} />;
});
```

💡 - `useMergeRefs` will always give you the same return, and you don't have to worry about `[localRef, ref]` unique every render.

## mergeRefs
## mergeRefs

`mergeRefs(refs: arrayOfRefs, [defaultValue]):ReactMutableRef` - merges a few refs together
is a non-hook based version. Will produce the new `ref` every run, causing the old one to unmount, and be _populated_ with the `null` value.

> mergeRefs are based on https://github.com/smooth-code/react-merge-refs, just exposes a RefObject, instead of a callback
`mergeRefs` are "safe" to use as a part of other hooks-based commands, but don't forget - it returns a new object every call.
`mergeRefs` are "safe" to use as a part of other hooks-based commands, but don't forget - it returns a new object every call.

# Similar packages:

- [apply-ref](https://github.com/mitchellhamilton/apply-ref) - `applyRefs` is simular to `mergeRef`, `applyRef` is similar to `assignRef`
- [useForkRef](https://react-hooks.org/docs/use-fork-ref) - `useForkRef` is simular to `useMergeRefs`, but accepts only two arguments.
- [react-merge-refs](https://github.com/gregberge/react-merge-refs) - `merge-refs` is simular to `useMergeRefs`, but not a hook and does not provide "stable" reference.
Expand All @@ -153,5 +166,5 @@ is a non-hook based version. Will produce the new `ref` every run, causing the o
> Is it a rocket science? No, `RefObject` is no more than `{current: ref}`, and `use-callback-ref` is no more than `getter` and `setter` on that field.
# License
MIT

MIT
19 changes: 19 additions & 0 deletions __tests__/index.tsx
Expand Up @@ -173,5 +173,24 @@ describe('Specs', () => {
mount(<TestComponent />);
expect(ref1.current).toBe('xx');
});

it('updating refs on the fly', () => {
const ref1 = createRef();
const ref2 = createRef();

const TestComponent = ({ r }: { r: React.RefObject<any> }) => {
const ref = useMergeRefs([null, r]);
ref.current = 'xx';

return null;
};

const wrapper = mount(<TestComponent r={ref1} />);
expect(ref1.current).toBe('xx');

wrapper.setProps({ r: ref2 });
expect(ref1.current).toBe(null);
expect(ref2.current).toBe('xx');
});
});
});
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -52,9 +52,10 @@
],
"keywords": [
"react",
"hoot",
"hook",
"useRef",
"createRef"
"createRef",
"merge refs"
],
"husky": {
"hooks": {
Expand Down
33 changes: 32 additions & 1 deletion src/useMergeRef.ts
Expand Up @@ -4,6 +4,8 @@ import { assignRef } from './assignRef';
import { ReactRef } from './types';
import { useCallbackRef } from './useRef';

const currentValues = new WeakMap<any, ReactRef<any>[]>();

/**
* Merges two or more refs together providing a single interface to set their value
* @param {RefObject|Ref} refs
Expand All @@ -19,5 +21,34 @@ import { useCallbackRef } from './useRef';
* }
*/
export function useMergeRefs<T>(refs: ReactRef<T>[], defaultValue?: T): React.MutableRefObject<T | null> {
return useCallbackRef<T>(defaultValue || null, (newValue) => refs.forEach((ref) => assignRef(ref, newValue)));
const callbackRef = useCallbackRef<T>(defaultValue || null, (newValue) =>
refs.forEach((ref) => assignRef(ref, newValue))
);

// handle refs changes - added or removed
React.useLayoutEffect(() => {

This comment has been minimized.

Copy link
@iiroj

iiroj Jan 8, 2024

Thank you for this very useful library! After a recent update I noticed that since this part of the code is using React.useLayoutEffect, rendering the component during SSR results in the warning:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.

This is typically worked around by something like:

const useIsomorphicEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect

Would you be open to such a change? I realize the warning is only a warning and most likely doesn't matter, but it's still annoying. Reference: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85

EDIT: Sorry, by component I was referring to react-remove-scroll which internally uses this.

This comment has been minimized.

Copy link
@theKashey

theKashey Jan 9, 2024

Author Owner

Would you be open to such a change?

Absolutely.

I was under impression that this warning was removed from React, and it actually was, but this (canary) version hasn't reached anyone except Nextjs users, so this trick is still required to silence the error.

This comment has been minimized.

Copy link
@iiroj

iiroj Jan 9, 2024

Thank you! I'll leave it up to you, since I'm satisfied the warning can be ignored in this case. FWIW the context for me is a Next.js app so I'll try upgrading.

const oldValue = currentValues.get(callbackRef);

if (oldValue) {
const prevRefs = new Set(oldValue);
const nextRefs = new Set(refs);
const current = callbackRef.current;

prevRefs.forEach((ref) => {
if (!nextRefs.has(ref)) {
assignRef(ref, null);
}
});

nextRefs.forEach((ref) => {
if (!prevRefs.has(ref)) {
assignRef(ref, current);
}
});
}

currentValues.set(callbackRef, refs);
}, [refs]);

return callbackRef;
}

0 comments on commit 47cbc58

Please sign in to comment.