Skip to content

Commit

Permalink
Merge pull request #250 from mediamonks/feature/#249-better-support-f…
Browse files Browse the repository at this point in the history
…or-hot-reloading-with-assertandunwraprefs

#249 Create validateAndUnwrapRefs function
  • Loading branch information
leroykorterink authored Oct 23, 2023
2 parents 60e74a6 + 3309d7b commit d7b124f
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type { UnwrapRefs } from '../unwrapRefs/unwrapRefs.types.js';
* The useRefs Proxy only creates a field in the getter, this means that a field's
* value will still be undefined when you never reference that field. The return
* type of this function doesn't reflect that behavior.
*
* @deprecated use `validateAndUnwrapRefs` instead to avoid errors during hot reloading
*/
export function assertAndUnwrapRefs<T extends Refs>(refs: T): NonNullableRecord<UnwrapRefs<T>> {
const unwrappedRefs = unwrapRefs(refs);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Meta } from '@storybook/blocks';

<Meta title="hooks/useRefs/utils/validateAndUnwrapRefs" />

# validateAndUnwrapRefs

Utility to validate and unwrap RefObjects in an object

## Reference

```ts
export function validateAndUnwrapRefs<T extends Refs>(refs: T): NonNullableRecord<UnwrapRefs<T>>;
```

### Returns

Object with NonNullable unwrapped RefObjects

## Usage

```tsx
type DemoComponentRefs = Refs<{
heading: HTMLHeadingElement;
buttons: Array<HTMLButtonElement | null>;
}>;

function DemoComponent() {
const refs = useRefs<DemoComponentRefs>();

const onClick = useCallback(() => {
const [isValid, { heading, buttons }] = validateAndUnwrapRefs(refs);

if (isValid) {
return;
}

console.log(heading); // HTMLHeadingElement
console.log(buttons); // Array<HTMLButtonElement | null>
}, [refs]);

return (
<>
<h1 ref={refs.heading}>{heading}</h1>

{items.map((item, index) => (
<button key={item} ref={arrayRef(refs.buttons, index)} onClick={onClick}>
{item}
</button>
))}
</>
);
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useRefs } from '../../useRefs.js';
import type { MutableRefs } from '../../useRefs.types.js';
import { validateAndUnwrapRefs } from './validateAndUnwrapRefs.js';

type TestRefs = MutableRefs<{
item1: string;
item2: number;
}>;

describe('unwrapRefs', () => {
it('should be invalid when ref object contains null field', () => {
const { result } = renderHook(() => useRefs<TestRefs>());

result.current.item1.current = null;
result.current.item2.current = 2;

expect(validateAndUnwrapRefs(result.current)).toEqual([false, undefined]);
});

it('should be invalid when ref object contains undefined field', () => {
const { result } = renderHook(() => useRefs<TestRefs>());

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
result.current.item1.current = undefined;
result.current.item2.current = 2;

expect(validateAndUnwrapRefs(result.current)).toEqual([false, undefined]);
});

it("should be valid when ref object doesn't contain null value", () => {
const { result } = renderHook(() => useRefs<TestRefs>());

result.current.item1.current = 'test';
result.current.item2.current = 2;

expect(validateAndUnwrapRefs(result.current)).toEqual([true, { item1: 'test', item2: 2 }]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
isNonNullableRecord,
type NonNullableRecord,
} from '../../../../_utils/isNonNullableRecord/isNonNullableRecord.js';
import type { Refs } from '../../useRefs.types.js';
import { unwrapRefs } from '../unwrapRefs/unwrapRefs.js';
import type { UnwrapRefs } from '../unwrapRefs/unwrapRefs.types.js';

/**
* Unwraps refs and assert every field is not null
*
* NOTE: this function asserts fields known during runtime on the refs parameter.
* The useRefs Proxy only creates a field in the getter, this means that a field's
* value will still be undefined when you never reference that field. The return
* type of this function doesn't reflect that behavior.
*/
export function validateAndUnwrapRefs<T extends Refs>(
refs: T,
): [false, undefined] | [true, NonNullableRecord<UnwrapRefs<T>>] {
const unwrappedRefs = unwrapRefs(refs);
if (!isNonNullableRecord(unwrappedRefs)) {
return [false, undefined];
}

return [true, unwrappedRefs];
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './hooks/useRefs/useRefs.types.js';
export * from './hooks/useRefs/utils/assertAndUnwrapRefs/assertAndUnwrapRefs.js';
export * from './hooks/useRefs/utils/unwrapRefs/unwrapRefs.js';
export * from './hooks/useRefs/utils/unwrapRefs/unwrapRefs.types.js';
export * from './hooks/useRefs/utils/validateAndUnwrapRefs/validateAndUnwrapRefs.js';
export * from './hooks/useRegisterRef/useRegisterRef.js';
export * from './hooks/useResizeObserver/useResizeObserver.js';
export * from './hooks/useStaticValue/useStaticValue.js';
Expand Down

0 comments on commit d7b124f

Please sign in to comment.