Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gitbook/reactjrx/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* [React](utils/react/README.md)
* [useMount](utils/react/usemount.md)
* [useEffectOnce](utils/react/useeffectonce.md)
* [useUnmount](utils/react/useunmount.md)
* [useConstant](utils/react/useconstant.md)
* [useLiveRef](utils/react/useliveref.md)
* [useDebounced](utils/react/usedebounced.md)
Expand Down
6 changes: 5 additions & 1 deletion gitbook/reactjrx/utils/react/useeffectonce.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# useEffectOnce

React lifecycle hook that runs an effect only once.
React lifecycle hook that runs an effect once, when the component mounts.

{% hint style="warning" %}
In development, under `<StrictMode>`, React mounts each component twice. So the effect runs twice, with its cleanup between the two runs. Make the cleanup undo the effect.
{% endhint %}

### Usage <a href="#usage" id="usage"></a>

Expand Down
4 changes: 4 additions & 0 deletions gitbook/reactjrx/utils/react/usemount.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

React lifecycle hook that calls a function after the component is mounted.

{% hint style="warning" %}
In development, under `<StrictMode>`, React mounts each component twice. So `useMount` calls `fn` twice. Make `fn` safe to call twice, or use `useEffectOnce` and undo the work in its cleanup.
{% endhint %}

### Usage <a href="#usage" id="usage"></a>

```jsx
Expand Down
24 changes: 24 additions & 0 deletions gitbook/reactjrx/utils/react/useunmount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# useUnmount

React lifecycle hook that calls a function when the component unmounts. It calls the `fn` of the last committed render.

{% hint style="warning" %}
In development, under `<StrictMode>`, React mounts each component twice. So `useUnmount` also calls `fn` once after the first mount, while the component stays mounted. Do not end work there that the component still uses, such as a subject that it reads.
{% endhint %}

### Usage

```jsx
import {useUnmount} from 'reactjrx';

const Demo = ({ onClose }) => {
useUnmount(() => onClose());
return null;
};
```

### Reference

```ts
useUnmount(fn: () => any);
```
60 changes: 60 additions & 0 deletions src/lib/utils/react/useUnmount.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render, renderHook } from "@testing-library/react"
import { act, Suspense, startTransition, use, useState } from "react"
import { describe, expect, it, vi } from "vitest"
import { useUnmount } from "./useUnmount"

describe("useUnmount", () => {
it("should call the fn of the last render when the component unmounts", () => {
const first = vi.fn()
const latest = vi.fn()
const { rerender, unmount } = renderHook(({ fn }) => useUnmount(fn), {
initialProps: { fn: first },
})

rerender({ fn: latest })
unmount()

expect(first).not.toHaveBeenCalled()
expect(latest).toHaveBeenCalledTimes(1)
})

it("should call the committed fn when React drops a render", async () => {
const never = new Promise<never>(() => {})
const Suspends = () => {
"use no memo"

use(never)

return null
}
const committed = vi.fn()
const dropped = vi.fn()
const rendered: Array<() => void> = []
let setFn: (fn: () => void) => void = () => {}

const Comp = () => {
"use no memo"

const [fn, set] = useState<() => void>(() => committed)
setFn = set
rendered.push(fn)
useUnmount(fn)

return (
<Suspense fallback={null}>{fn === dropped && <Suspends />}</Suspense>
)
}

const { unmount } = render(<Comp />)

await act(async () => {
startTransition(() => setFn(() => dropped))
})

unmount()

expect(rendered).toContain(dropped)
expect(dropped).not.toHaveBeenCalled()
expect(committed).toHaveBeenCalledTimes(1)
})
})
7 changes: 2 additions & 5 deletions src/lib/utils/react/useUnmount.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useRef } from "react"
import { useEffectOnce } from "./useEffectOnce"
import { useLiveRef } from "./useLiveRef"

// biome-ignore lint/suspicious/noExplicitAny: Expected any
export const useUnmount = (fn: () => any): void => {
const fnRef = useRef(fn)

// update the ref each render so if it change the newest callback will be invoked
fnRef.current = fn
const fnRef = useLiveRef(fn)

useEffectOnce(() => () => fnRef.current())
}
Loading