Skip to content

Commit

Permalink
Merge branch 'main' into fix/dev3_methods
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi committed Apr 7, 2024
2 parents 80b8633 + 88303ac commit 92ac0e3
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 54 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test-old-typescript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ jobs:
fail-fast: false
matrix:
typescript:
- 5.3.2
- 5.4.4
- 5.3.3
- 5.2.2
- 5.1.6
- 5.0.4
Expand Down
23 changes: 12 additions & 11 deletions docs/guides/debugging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ Which returns atoms value that is deeply freezed with `Object.freeze`.
freezeAtom(anAtom): AtomType
```
`freezeAtom` takes an existing atom and returns a new derived "frozen" atom.
The atom will be deeply frozen by `Object.freeze`.
`freezeAtom` takes an existing atom and make it "frozen".
It returns the same atom.
The atom value will be deeply frozen by `Object.freeze`.
It is useful to find bugs where you unintentionally tried
to change objects (states) which can lead to unexpected behavior.
You may use `freezeAtom` with all atoms to prevent this situation.
Expand All @@ -156,14 +157,14 @@ const objAtom = freezeAtom(atom({ count: 0 }))
### freezeAtomCreator
```js
import { atom } from 'jotai'
import { freezeAtomCreator } from 'jotai/utils'
If you need, you can define a factory for `freezeAtom`.
const createFrozenAtom = freezeAtomCreator(atom)
const objAtom = createFrozenAtom({ count: 0 })
```
```ts
import { freezeAtom } from 'jotai/utils'

Instead of create a frozen atom from an existing atom,
`freezeAtomCreator` takes an atom creator function and returns a new function.
You can use this not only for `atom`, but also for other `atomWith*` creators such as `atomWithReduer`.
export function freezeAtomCreator<
CreateAtom extends (...args: unknown[]) => Atom<unknown>,
>(createAtom: CreateAtom): CreateAtom {
return ((...args: unknown[]) => freezeAtom(createAtom(...args))) as never
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-vitest": "^0.4.1",
"jest-leak-detector": "^29.7.0",
"jsdom": "^24.0.0",
"json": "^11.0.0",
"prettier": "^3.2.5",
Expand Down
64 changes: 42 additions & 22 deletions src/vanilla/utils/freezeAtom.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { atom } from '../../vanilla.ts'
import type { Atom } from '../../vanilla.ts'
import type { Atom, WritableAtom } from '../../vanilla.ts'

const cache1 = new WeakMap()
const memo1 = <T>(create: () => T, dep1: object): T =>
(cache1.has(dep1) ? cache1 : cache1.set(dep1, create())).get(dep1)
const frozenAtoms = new WeakSet<Atom<any>>()

const deepFreeze = (obj: unknown) => {
if (typeof obj !== 'object' || obj === null) return
Expand All @@ -18,25 +15,48 @@ const deepFreeze = (obj: unknown) => {

export function freezeAtom<AtomType extends Atom<unknown>>(
anAtom: AtomType,
): AtomType {
return memo1(() => {
const frozenAtom = atom(
(get) => deepFreeze(get(anAtom)),
(_get, set, arg) => set(anAtom as never, arg),
)
return frozenAtom as never
}, anAtom)
): AtomType

export function freezeAtom(
anAtom: WritableAtom<unknown, unknown[], unknown>,
): WritableAtom<unknown, unknown[], unknown> {
if (frozenAtoms.has(anAtom)) {
return anAtom
}
frozenAtoms.add(anAtom)

const origRead = anAtom.read
anAtom.read = function (get, options) {
return deepFreeze(origRead.call(this, get, options))
}
if ('write' in anAtom) {
const origWrite = anAtom.write
anAtom.write = function (get, set, ...args) {
return origWrite.call(
this,
get,
(...setArgs) => {
if (setArgs[0] === anAtom) {
setArgs[1] = deepFreeze(setArgs[1])
}

return set(...setArgs)
},
...args,
)
}
}
return anAtom
}

/**
* @deprecated Define it on users end
*/
export function freezeAtomCreator<
CreateAtom extends (...params: never[]) => Atom<unknown>,
CreateAtom extends (...args: unknown[]) => Atom<unknown>,
>(createAtom: CreateAtom): CreateAtom {
return ((...params: never[]) => {
const anAtom = createAtom(...params)
const origRead = anAtom.read
anAtom.read = function (get, options) {
return deepFreeze(origRead.call(this, get, options))
}
return anAtom
}) as never
console.warn(
'[DEPRECATED] freezeAtomCreator is deprecated, define it on users end',
)
return ((...args: unknown[]) => freezeAtom(createAtom(...args))) as never
}
73 changes: 53 additions & 20 deletions tests/react/vanilla-utils/freezeAtom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,76 @@
import { StrictMode } from 'react'
import { render } from '@testing-library/react'
import { it } from 'vitest'
import { fireEvent, render } from '@testing-library/react'
import { afterEach, beforeEach, describe, it, vi } from 'vitest'
import { useAtom } from 'jotai/react'
import { atom } from 'jotai/vanilla'
import { freezeAtom, freezeAtomCreator } from 'jotai/vanilla/utils'

it('freezeAtom basic test', async () => {
const objAtom = atom({ count: 0 })
const objAtom = atom({ deep: {} }, (_get, set, _ignored?) => {
set(objAtom, { deep: {} })
})

const Component = () => {
const [obj] = useAtom(freezeAtom(objAtom))

return <div>isFrozen: {`${Object.isFrozen(obj)}`}</div>
const [obj, setObj] = useAtom(freezeAtom(objAtom))
return (
<>
<button onClick={setObj}>change</button>
<div>
isFrozen: {`${Object.isFrozen(obj) && Object.isFrozen(obj.deep)}`}
</div>
</>
)
}

const { findByText } = render(
const { getByText, findByText } = render(
<StrictMode>
<Component />
</StrictMode>,
)

await findByText('isFrozen: true')

fireEvent.click(getByText('change'))
await findByText('isFrozen: true')
})

it('freezeAtomCreator basic test', async () => {
const createFrozenAtom = freezeAtomCreator(atom)
const objAtom = createFrozenAtom({ count: 0 })
describe('freezeAtomCreator', () => {
let savedConsoleWarn: any
beforeEach(() => {
savedConsoleWarn = console.warn
console.warn = vi.fn()
})
afterEach(() => {
console.warn = savedConsoleWarn
})

const Component = () => {
const [obj] = useAtom(objAtom)
it('freezeAtomCreator basic test', async () => {
const createFrozenAtom = freezeAtomCreator(atom)
const objAtom = createFrozenAtom({ deep: {} }, (_get, set, _ignored?) => {
set(objAtom, { deep: {} })
})

return <div>isFrozen: {`${Object.isFrozen(obj)}`}</div>
}
const Component = () => {
const [obj, setObj] = useAtom(objAtom)
return (
<>
<button onClick={setObj}>change</button>
<div>
isFrozen: {`${Object.isFrozen(obj) && Object.isFrozen(obj.deep)}`}
</div>
</>
)
}

const { findByText } = render(
<StrictMode>
<Component />
</StrictMode>,
)
const { getByText, findByText } = render(
<StrictMode>
<Component />
</StrictMode>,
)

await findByText('isFrozen: true')
await findByText('isFrozen: true')

fireEvent.click(getByText('change'))
await findByText('isFrozen: true')
})
})
14 changes: 14 additions & 0 deletions tests/vanilla/memoryleaks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import LeakDetector from 'jest-leak-detector'
import { expect, it } from 'vitest'
import { atom, createStore } from 'jotai/vanilla'

it('should not have memory leaks with an atom', async () => {
const store = createStore()
let detector: LeakDetector
;(() => {
const objAtom = atom({})
detector = new LeakDetector(store.get(objAtom))
})()
const isLeaking = await detector.isLeaking()
expect(isLeaking).toBe(false)
})
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3466,6 +3466,19 @@ iterator.prototype@^1.1.2:
reflect.getprototypeof "^1.0.4"
set-function-name "^2.0.1"

jest-get-type@^29.6.3:
version "29.6.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==

jest-leak-detector@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728"
integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==
dependencies:
jest-get-type "^29.6.3"
pretty-format "^29.7.0"

"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
Expand Down

0 comments on commit 92ac0e3

Please sign in to comment.