Skip to content

Commit

Permalink
attempt 2: refactor with store hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
dmaskasky committed Jan 17, 2025
1 parent b27f4a5 commit e774763
Show file tree
Hide file tree
Showing 9 changed files with 1,139 additions and 413 deletions.
26 changes: 5 additions & 21 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": [
"@typescript-eslint",
"react",
"prettier",
"react-hooks",
"import",
"jest"
],
"plugins": ["@typescript-eslint", "react", "prettier", "react-hooks", "import", "jest"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
Expand All @@ -39,6 +32,7 @@
"import/no-unresolved": ["error", { "commonjs": true, "amd": true }],
"import/export": "error",
"import/no-duplicates": ["error"],
"prettier/prettier": ["error", { "printWidth": 100 }],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
Expand All @@ -47,23 +41,13 @@
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"jest/consistent-test-it": [
"error",
{ "fn": "it", "withinDescribe": "it" }
],
"@typescript-eslint/no-unused-expressions": "off",
"jest/consistent-test-it": ["error", { "fn": "it", "withinDescribe": "it" }],
"import/order": [
"error",
{
"alphabetize": { "order": "asc", "caseInsensitive": true },
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object"
],
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object"],
"newlines-between": "never",
"pathGroups": [
{
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"singleQuote": true,
"bracketSameLine": true,
"tabWidth": 2,
"printWidth": 80
"printWidth": 100
}
117 changes: 57 additions & 60 deletions __tests__/atomEffect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
import React, { createElement, useEffect } from 'react'
import { act, render, renderHook, waitFor } from '@testing-library/react'
import { Provider, useAtom, useAtomValue, useSetAtom } from 'jotai/react'
import { atom, createStore, getDefaultStore } from 'jotai/vanilla'
import { atom } from 'jotai/vanilla'
import { atomEffect } from '../src/atomEffect'
import {
ErrorBoundary,
assert,
createDebugStore,
delay,
increment,
incrementLetter,
} from './test-utils'

it('should run the effect on vanilla store', () => {
const store = createStore().unstable_derive(
(getAtomState, setAtomState, ...rest) => [
getAtomState,
(atom, atomState) =>
setAtomState(
atom,
Object.assign(atomState, {
label: atom.debugLabel,
})
),
...rest,
]
)
const store = createDebugStore()
const countAtom = atom(0)
countAtom.debugLabel = 'count'

const effectAtom = atomEffect((_, set) => {
set(countAtom, increment)
return () => {
set(countAtom, 0)
}
})
effectAtom.debugLabel = 'effect'
const unsub = store.sub(effectAtom, () => void 0)

const unsub = store.sub(effectAtom, () => {})
expect(store.get(countAtom)).toBe(1)
unsub()
expect(store.get(countAtom)).toBe(0)
Expand Down Expand Up @@ -88,6 +79,7 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom
})

let didMount = false
const store = createDebugStore()
function useTest() {
const [count, setCount] = useAtom(countAtom)
useAtomValue(effectAtom)
Expand All @@ -96,7 +88,10 @@ it('should run the effect on mount and cleanup on unmount and whenever countAtom
}, [count])
return setCount
}
const { result, rerender, unmount } = renderHook(useTest)
const wrapper = ({ children }: { children: React.ReactNode }) => (
<Provider store={store as any}>{children}</Provider>
)
const { result, rerender, unmount } = renderHook(useTest, { wrapper })
function incrementCount() {
const setCount = result.current
setCount(increment)
Expand Down Expand Up @@ -145,8 +140,8 @@ it('should not cause infinite loops when effect updates the watched atom', () =>
runCount++
set(watchedAtom, increment)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})

const incrementWatched = () => store.set(watchedAtom, increment)
// initial render should run the effect once
Expand All @@ -167,8 +162,8 @@ it('should not cause infinite loops when effect updates the watched atom asynchr
set(watchedAtom, increment)
}, 0)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
// changing the value should run the effect again one time
store.set(watchedAtom, increment)
expect(runCount).toBe(2)
Expand All @@ -186,8 +181,8 @@ it('should allow synchronous recursion with set.recurse for first run', () => {
}
recurse(watchedAtom, increment)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
expect({ runCount, watched: store.get(watchedAtom) }).toEqual({
runCount: 4, // 2
watched: 3, // 2
Expand All @@ -209,8 +204,8 @@ it('should allow synchronous recursion with set.recurse', () => {
}
recurse(watchedAtom, increment)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
store.set(watchedAtom, increment)
expect(store.get(watchedAtom)).toBe(5)
expect(runCount).toBe(6)
Expand All @@ -232,8 +227,8 @@ it('should allow multiple synchronous recursion with set.recurse', () => {
recurse(watchedAtom, increment)
recurse(watchedAtom, increment)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
store.set(watchedAtom, increment)
expect({ runCount, value: store.get(watchedAtom) }).toEqual({
runCount: 6,
Expand Down Expand Up @@ -269,8 +264,8 @@ it('should batch updates during synchronous recursion with set.recurse', () => {
])
set.recurse(updateAtom)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
store.set(watchedAtom, increment)
expect(store.get(lettersAndNumbersAtom)).toEqual(['a0', 'b1'])
expect(runCount).toBe(4)
Expand All @@ -292,8 +287,8 @@ it('should allow asynchronous recursion with task delay with set.recurse', async
recurse(watchedAtom, increment)
})
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
await waitFor(() => assert(done))
expect(store.get(watchedAtom)).toBe(3)
expect(runCount).toBe(4)
Expand All @@ -314,8 +309,8 @@ it('should allow asynchronous recursion with microtask delay with set.recurse',
recurse(watchedAtom, increment)
})
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
await waitFor(() => assert(store.get(watchedAtom) >= 3))
expect(store.get(watchedAtom)).toBe(3)
expect(runCount).toBe(4)
Expand All @@ -324,23 +319,26 @@ it('should allow asynchronous recursion with microtask delay with set.recurse',
it('should work with both set.recurse and set', () => {
expect.assertions(3)
let runCount = 0
const watchedAtom = atom(0)
const valueAtom = atom(0)
const countAtom = atom(0)
const effectAtom = atomEffect((get, set) => {
const value = get(watchedAtom)
const value = get(valueAtom)
if (value >= 5) {
throw new Error()
}
get(countAtom)
runCount++
if (value === 0 || value % 3) {
set.recurse(watchedAtom, increment)
set.recurse(valueAtom, increment)
set(countAtom, increment)
return
}
set(watchedAtom, increment)
set(valueAtom, increment)
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
expect(store.get(countAtom)).toBe(3)
expect(store.get(watchedAtom)).toBe(4)
expect(store.get(valueAtom)).toBe(4)
expect(runCount).toBe(4)
})

Expand All @@ -357,8 +355,8 @@ it('should disallow synchronous set.recurse in cleanup', () => {
})
return cleanup
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
store.set(anotherAtom, increment)
expect(() => store.set(anotherAtom, increment)).toThrowError(
'set.recurse is not allowed in cleanup'
Expand All @@ -383,8 +381,8 @@ it('should return value from set.recurse', () => {
return
}
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
expect(results).toEqual([1, 2, 3, 4, 5])
})

Expand Down Expand Up @@ -657,8 +655,8 @@ it('should batch synchronous updates as a single transaction', () => {
letters + String(numbers),
])
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})

expect(runCount).toBe(1)
expect(store.get(lettersAndNumbersAtom)).toEqual(['a0'])
Expand Down Expand Up @@ -725,11 +723,9 @@ it('should abort the previous promise', async () => {
const completedRuns: number[] = []
const resolves: (() => void)[] = []
const countAtom = atom(0)
const abortControllerAtom = atom<{ abortController: AbortController | null }>(
{
abortController: null,
}
)
const abortControllerAtom = atom<{ abortController: AbortController | null }>({
abortController: null,
})
const effectAtom = atomEffect((get) => {
const currentRun = runCount++
get(countAtom)
Expand Down Expand Up @@ -821,8 +817,8 @@ it('should not infinite loop with nested atomEffects', async () => {
get(readOnlyAtom)
})

const store = getDefaultStore()
store.sub(effect2Atom, () => void 0)
const store = createDebugStore()
store.sub(effect2Atom, () => {})

await waitFor(() => assert(delayedIncrement))

Expand Down Expand Up @@ -854,8 +850,8 @@ it('should not rerun with get.peek', () => {
get.peek(countAtom)
runCount++
})
const store = getDefaultStore()
store.sub(effectAtom, () => void 0)
const store = createDebugStore()
store.sub(effectAtom, () => {})
store.set(countAtom, increment)
expect(runCount).toBe(1)
})
Expand All @@ -872,10 +868,7 @@ it('should trigger the error boundary when an error is thrown', async () => {
let didThrow = false
function wrapper() {
return (
<ErrorBoundary
componentDidCatch={() => (didThrow = true)}
children={<TestComponent />}
/>
<ErrorBoundary componentDidCatch={() => (didThrow = true)} children={<TestComponent />} />
)
}
const originalConsoleError = console.error
Expand All @@ -893,13 +886,17 @@ it('should trigger an error boundary when an error is thrown in a cleanup', asyn
expect.assertions(1)

const refreshAtom = atom(0)
refreshAtom.debugLabel = 'refresh'

const effectAtom = atomEffect((get, _set) => {
get(refreshAtom)
return () => {
throw new Error('effect cleanup error')
}
})
const store = createStore()
effectAtom.debugLabel = 'effect'

const store = createDebugStore()
function TestComponent() {
useAtomValue(effectAtom)
return <div>test</div>
Expand Down Expand Up @@ -945,7 +942,7 @@ it('should not suspend the component', () => {
}
return null
}
const store = createStore()
const store = createDebugStore()
render(<App />, {
wrapper: ({ children }) => createElement(Provider, { children, store }),
})
Expand Down
Loading

0 comments on commit e774763

Please sign in to comment.