diff --git a/__tests__/mutableAtom.test.tsx b/__tests__/mutableAtom.test.tsx
index 8a733e9..07d453f 100644
--- a/__tests__/mutableAtom.test.tsx
+++ b/__tests__/mutableAtom.test.tsx
@@ -1,8 +1,15 @@
import React from 'react'
import { act, render, renderHook, waitFor } from '@testing-library/react'
-import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
+import {
+ Provider,
+ atom,
+ createStore,
+ useAtom,
+ useAtomValue,
+ useSetAtom,
+} from 'jotai'
import assert from 'minimalistic-assert'
-import { mutableAtom } from '../src/mutableAtom'
+import { makeMutableAtom, mutableAtom } from '../src/mutableAtom'
import type { ProxyState } from '../src/mutableAtom/types'
it('should be defined on initial render', async () => {
@@ -274,7 +281,7 @@ it('should reject writing to properties other than `value`', async () => {
}
const { result } = renderHook(useTest)
expect(async () => {
- await act(() => {
+ await act(async () => {
result.current.countProxy.value = 1
})
}).not.toThrow()
@@ -284,6 +291,47 @@ it('should reject writing to properties other than `value`', async () => {
}).toThrow() // 'set' on proxy: trap returned falsish for property 'NOT_VALUE'
})
+it('should allow updating even when component is unmounted', async () => {
+ expect.assertions(2)
+ const store = createStore()
+ const countAtom = atom({ value: 0 })
+ let isMounted = false
+ countAtom.onMount = () => {
+ isMounted = true
+ }
+ const mutableCountAtom = makeMutableAtom(countAtom)
+
+ function useTest() {
+ useAtomValue(mutableCountAtom)
+ }
+ function wrapper({ children }: { children: React.ReactNode }) {
+ return {children}
+ }
+
+ const { unmount } = renderHook(useTest, { wrapper })
+ await waitFor(() => assert(isMounted))
+ unmount()
+ expect(store.get(countAtom).value).toBe(0)
+ await act(async () => {
+ const countProxy = store.get(mutableCountAtom)
+ countProxy.value++
+ })
+ expect(store.get(countAtom).value).toBe(1)
+})
+
+it('should allow updating even when component has not mounted', async () => {
+ expect.assertions(2)
+ const store = createStore()
+ const countAtom = atom({ value: 0 })
+ const mutableCountAtom = makeMutableAtom(countAtom)
+ expect(store.get(countAtom).value).toBe(0)
+ await act(async () => {
+ const countProxy = store.get(mutableCountAtom)
+ countProxy.value++
+ })
+ expect(store.get(countAtom).value).toBe(1)
+})
+
it('should correctly handle updates via writable atom', async () => {
expect.assertions(3)
const mutableCountAtom = mutableAtom(0)
diff --git a/src/mutableAtom/index.ts b/src/mutableAtom/index.ts
index 2daace5..74a007f 100644
--- a/src/mutableAtom/index.ts
+++ b/src/mutableAtom/index.ts
@@ -1,7 +1,7 @@
import { atom } from 'jotai'
-import type { Getter, Setter } from 'jotai'
+import type { Getter, PrimitiveAtom, Setter } from 'jotai'
import { proxy, snapshot, subscribe } from 'valtio'
-import {
+import type {
Options,
PromiseOrValue,
ProxyState,
@@ -16,48 +16,39 @@ export function mutableAtom(
value: Value,
options: Options = defaultOptions
) {
- const { proxyFn } = { ...defaultOptions, ...options }
-
const valueAtom = atom({ value })
if (process.env.NODE_ENV !== 'production') {
valueAtom.debugPrivate = true
}
+ return makeMutableAtom(valueAtom, options)
+}
+
+export function makeMutableAtom(
+ valueAtom: PrimitiveAtom>,
+ options: Options = defaultOptions
+) {
+ const { proxyFn } = { ...defaultOptions, ...options }
const storeAtom = atom(
() =>
({
- isMounted: false,
+ hasMounted: false,
proxyState: null,
unsubscribe: null,
} as Store),
- (get, set, isOnMount: boolean) => {
- if (isOnMount) {
- createProxyState(get, (fn) => fn(set))
- } else {
- onAtomUnmount(get)
- }
+ (get, set) => {
+ // switch to synchronous imperative updates on mount
+ createProxyState(get, (fn) => fn(set))
}
)
- storeAtom.onMount = (setOnMount) => {
- // switch to synchronous imperative updates on mount
- setOnMount(true)
- return () => setOnMount(false)
- }
+ storeAtom.onMount = (setInit) => setInit()
if (process.env.NODE_ENV !== 'production') {
storeAtom.debugPrivate = true
}
- /**
- * unsubscribe on atom unmount
- */
- function onAtomUnmount(get: Getter) {
- get(storeAtom).unsubscribe?.()
- get(storeAtom).isMounted = false
- }
-
/**
* sync the proxy state with the atom
*/
@@ -80,13 +71,9 @@ export function mutableAtom(
const { value } = get(valueAtom)
store.proxyState ??= proxyFn({ value })
store.proxyState.value = value
- const unsubscribe = subscribe(store.proxyState, onChange(get, setCb), true)
store.unsubscribe?.()
- store.unsubscribe = () => {
- store.unsubscribe = null
- unsubscribe()
- }
- store.isMounted = true
+ store.unsubscribe = subscribe(store.proxyState, onChange(get, setCb), true)
+ store.hasMounted = true
return store.proxyState
}
@@ -94,8 +81,8 @@ export function mutableAtom(
* return the proxy if it exists, otherwise create and subscribe to it
*/
function ensureProxyState(get: Getter, setCb: SetCb) {
- const { isMounted, proxyState } = get(storeAtom)
- if (proxyState === null || !isMounted) {
+ const { hasMounted, proxyState } = get(storeAtom)
+ if (proxyState === null || !hasMounted) {
return createProxyState(get, setCb)
}
return proxyState
@@ -104,21 +91,13 @@ export function mutableAtom(
/**
* wrap the proxy state in a proxy to ensure rerender on value change
*/
- function wrapProxyState(
- proxyState: ProxyState,
- get: Getter,
- setCb: SetCb
- ) {
+ function wrapProxyState(proxyState: ProxyState) {
return new Proxy(proxyState, {
get: (target, property) => {
- if (property === 'value') {
- ensureProxyState(get, setCb)
- }
return target[property as keyof ProxyState]
},
set(target, property, value) {
if (property === 'value') {
- ensureProxyState(get, setCb)
target[property] = value
return true
}
@@ -135,7 +114,7 @@ export function mutableAtom(
get(valueAtom) // subscribe to value updates
const setCb = makeSetCb(setSelf)
const proxyState = ensureProxyState(get, setCb)
- return wrapProxyState(proxyState, get, setCb)
+ return wrapProxyState(proxyState)
},
(get, set, writeFn: WriteFn) => writeFn(get, set)
)
@@ -162,7 +141,7 @@ const makeSetCb =
/**
* delays execution until next microtask
- * */
+ */
function defer(fn?: () => PromiseOrValue) {
return Promise.resolve().then(fn)
}
diff --git a/src/mutableAtom/types.ts b/src/mutableAtom/types.ts
index a7349d0..1ecee8e 100644
--- a/src/mutableAtom/types.ts
+++ b/src/mutableAtom/types.ts
@@ -24,7 +24,7 @@ export type SetSelf = SetAtom
export type Store = {
unsubscribe: CleanupFn | null
- isMounted: boolean
+ hasMounted: boolean
proxyState: ProxyState | null
}