Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into #102-allow-ref-object…
Browse files Browse the repository at this point in the history
…-in-use-event-listener
  • Loading branch information
leroykorterink committed Apr 4, 2023
2 parents d0f3da0 + eae00dc commit 69ee986
Show file tree
Hide file tree
Showing 30 changed files with 5,402 additions and 4,796 deletions.
6 changes: 2 additions & 4 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StorybookConfig } from '@storybook/types';

export default {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'],
staticDirs: ['../public'],
Expand All @@ -7,10 +8,7 @@ export default {
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
framework: '@storybook/react-vite',
docs: {
autodocs: true,
},
Expand Down
9,204 changes: 4,433 additions & 4,771 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@
"@mediamonks/eslint-config-typescript-react": "^1.0.10",
"@mediamonks/prettier-config": "^1.0.1",
"@playwright/experimental-ct-react": "^1.32.1",
"@storybook/addon-essentials": "^7.0.0-beta.36",
"@storybook/addon-interactions": "^7.0.0-beta.36",
"@storybook/addon-links": "^7.0.0-beta.36",
"@storybook/blocks": "^7.0.0-beta.36",
"@storybook/cli": "^7.0.0-beta.36",
"@storybook/react-vite": "^7.0.0-beta.36",
"@storybook/types": "^7.0.0-beta.36",
"@storybook/addon-essentials": "^7.0.0",
"@storybook/addon-interactions": "^7.0.0",
"@storybook/addon-links": "^7.0.0",
"@storybook/blocks": "^7.0.0",
"@storybook/cli": "^7.0.0",
"@storybook/react-vite": "^7.0.0",
"@storybook/types": "^7.0.0",
"@swc/core": "^1.3.24",
"@swc/jest": "^0.2.24",
"@testing-library/jest-dom": "^5.16.5",
Expand Down
3 changes: 3 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default defineConfig({

/* Port to use for Playwright component endpoint. */
ctPort: 3100,

/* Retain video on failure to be able to debug test. */
video: 'retain-on-failure',
},

/* Configure projects for major browsers */
Expand Down
40 changes: 40 additions & 0 deletions src/hooks/useBeforeMount/useBeforeMount.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Meta } from '@storybook/blocks';

<Meta title="hooks/lifecycle/useBeforeMount" />

# useBeforeMount

**Synchronously** run a function before the component is mounted, only once.

This is especially useful for initializing objects that are used in code further down.

It's opposite of `useMount`, which runs asynchronously after the component is mounted.

> To know when a component is rendering for the first time, use the `useIsMounted` hook,
which returned boolean ref can be used inline in other component code or JSX.

## Reference

```ts
function useBeforeMount(callback: () => void): void;
```

### Parameters

* `callback` - A function to be executed during the initial render, before mounting, but not
during subsequent renders.

## Usage

```tsx
function DemoComponent() {
useBeforeMount(() => {
console.log('I will run before the component is mounted');
});

return (
<div>
</div>
);
}
```
82 changes: 82 additions & 0 deletions src/hooks/useBeforeMount/useBeforeMount.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { jest } from '@jest/globals';
import { renderHook } from '@testing-library/react';
import { useEffect, useState } from 'react';
import { useMount } from '../useMount/useMount.js';
import { useBeforeMount } from './useBeforeMount.js';

describe('useBeforeMount', () => {
it('should not crash', async () => {
renderHook(() => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
useBeforeMount(() => {});
});
});

it('should execute a callback on first render', async () => {
const spy = jest.fn();
renderHook(() => {
useBeforeMount(spy);
});
expect(spy).toBeCalledTimes(1);
});

it('should execute during synchronous render, before mount', async () => {
const beforeMount = jest.fn();
const inlineSpy = jest.fn();
const mountedSpy = jest.fn();
renderHook(() => {
useEffect(() => {
mountedSpy();
}, []);

useBeforeMount(beforeMount);

inlineSpy();
});

expect(beforeMount).toBeCalledTimes(1);
expect(mountedSpy).toBeCalledTimes(1);
expect(inlineSpy).toBeCalledTimes(1);
expect(beforeMount.mock.invocationCallOrder[0]).toBeLessThan(
mountedSpy.mock.invocationCallOrder[0],
);
expect(beforeMount.mock.invocationCallOrder[0]).toBeLessThan(
inlineSpy.mock.invocationCallOrder[0],
);
});

it('should not execute a callback on re-renders', async () => {
const spy = jest.fn();
const { rerender } = renderHook(() => {
useBeforeMount(spy);
});
expect(spy).toBeCalledTimes(1);

await Promise.resolve();
rerender();
expect(spy).toBeCalledTimes(1);

await Promise.resolve();
rerender();
expect(spy).toBeCalledTimes(1);
});

it('should only execute once when setState is called during useMount', async () => {
const spy = jest.fn();
const { rerender } = renderHook(() => {
// eslint-disable-next-line react/hook-use-state
const [, setState] = useState(false);

useBeforeMount(spy);

useMount(() => {
setState(true);
});
});
expect(spy).toBeCalledTimes(1);

await Promise.resolve();
rerender();
expect(spy).toBeCalledTimes(1);
});
});
24 changes: 24 additions & 0 deletions src/hooks/useBeforeMount/useBeforeMount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useRef } from 'react';
import { useMount } from '../useMount/useMount.js';

/**
* Executes a callback during the initial render, before mounting, but not during subsequent
* renders.
*
* Opposed to `useEffect` / `useMount`, the callback is executed synchronously,
* before the component is mounted.
*
* @param callback A function to be executed during the initial render, before mounting, but not
* during subsequent renders.
*/
export function useBeforeMount(callback: () => void): void {
const isBeforeMount = useRef(true);

if (isBeforeMount.current) {
callback();
}

useMount(() => {
isBeforeMount.current = false;
});
}
43 changes: 43 additions & 0 deletions src/hooks/useForceRerender/useForceRerender.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Meta } from '@storybook/blocks';

<Meta title="hooks/lifecycle/useForceRerender" />

# useForceRerender

Forces a rerender of the component when the returned function is called.

This should only be used when there is no other state that can be used to trigger a rerender.

Examples could be to force a rerender after a timeout or interval, to compare the current time
(that changes) with the time when the component was last updated.

## Reference

```ts
function useForceRerender(): () => void;
```

### Returns

* `forceRerender` - A function that can be called to force a rerender.

## Usage

```ts
const forceRerender = useForceRerender();

forceRerender();
```

```tsx
function DemoComponent() {
const forceRerender = useForceRerender();

return (
<div>
<div>Time: {Date.now()}</div>
<button onClick={forceRerender}>Update</button>
</div>
);
}
```
35 changes: 35 additions & 0 deletions src/hooks/useForceRerender/useForceRerender.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable react/jsx-no-literals,react/jsx-handler-names */
import type { StoryObj } from '@storybook/react';
import { useForceRerender } from './useForceRerender.js';

export default {
title: 'hooks/lifecycle/useForceRerender',
};

function DemoComponent(): JSX.Element {
const forceRerender = useForceRerender();

return (
<div>
<div className="alert alert-primary">
<h4 className="alert-heading">Instructions!</h4>
<p className="mb-0">Click the &quot;Update&quot; button, and notice the date updating.</p>
</div>
<div className="card border-dark" data-ref="test-area">
<div className="card-header">Test Area</div>
<div className="card-body">
<p>{Date.now()}</p>
<button type="button" className="btn btn-primary" onClick={forceRerender}>
Update
</button>
</div>
</div>
</div>
);
}

export const Demo: StoryObj = {
render() {
return <DemoComponent />;
},
};
39 changes: 39 additions & 0 deletions src/hooks/useForceRerender/useForceRerender.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { jest } from '@jest/globals';
import { act, renderHook } from '@testing-library/react';
import { useForceRerender } from './useForceRerender.js';

describe('useForceRerender', () => {
it('should not crash', async () => {
renderHook(() => useForceRerender());
});

it('should return a function', async () => {
const {
result: { current: forceRerender },
} = renderHook(() => useForceRerender());

expect(forceRerender).toBeInstanceOf(Function);
});

it('should force a rerender', async () => {
const spy = jest.fn();
const {
result: { current: forceRerender },
} = renderHook(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const forceRerender = useForceRerender();

spy();

return forceRerender;
});

expect(spy).toBeCalledTimes(1);

await act(() => {
forceRerender();
});

expect(spy).toBeCalledTimes(2);
});
});
21 changes: 21 additions & 0 deletions src/hooks/useForceRerender/useForceRerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useReducer } from 'react';

// use an arbitrary large number instead of a toggle boolean to avoid potential optimization issues
// when called multiple times in a single render, but have a cap to avoid overflow
const updateReducer = (value: number): number => (value + 1) % Number.MAX_SAFE_INTEGER;

/**
* Forces a rerender of the component when the returned function is called.
*
* This should only be used when there is no other state that can be used to trigger a rerender.
*
* Examples could be to force a rerender after a timeout or interval, to compare the current time
* (that changes) with the time when the component was last updated.
*
* @returns A function that can be called to force a rerender.
*/
export function useForceRerender(): () => void {
const [, update] = useReducer(updateReducer, 0);

return update;
}
Loading

0 comments on commit 69ee986

Please sign in to comment.