diff --git a/packages/core/__tests__/select.test.tsx b/packages/core/__tests__/select.test.tsx
index 1701d7c..ca1933b 100644
--- a/packages/core/__tests__/select.test.tsx
+++ b/packages/core/__tests__/select.test.tsx
@@ -1,7 +1,8 @@
import { create } from '../create'
import { select } from '../select'
-import { waitFor } from '@testing-library/react'
+import { renderHook, waitFor } from '@testing-library/react'
import { longPromise } from './test-utils'
+import { Suspense } from 'react'
describe('select', () => {
it('should derive state from a single dependency', async () => {
@@ -101,7 +102,7 @@ describe('select', () => {
await longPromise(100)
return (await value) + 1
})
- const selectedState2 = selectedState.select(async (value) => (await value) + 1)
+ const selectedState2 = selectedState.select(async (value) => value + 1)
const listener = jest.fn()
selectedState2.listen(listener)
await waitFor(() => {
@@ -154,7 +155,7 @@ describe('select', () => {
it('should select state from async initial state', async () => {
const state = create(longPromise(100))
const selectedState = state.select(async (value) => {
- return (await value) + 2
+ return value + 2
})
await waitFor(() => {
expect(selectedState.get()).toBe(2)
@@ -169,4 +170,39 @@ describe('select', () => {
expect(selectedState.get()).toBe(2)
})
})
+
+ it('should select state from async state and do not change second time as it just boolean value', async () => {
+ const state = create(longPromise(100))
+ const selectedState = state.select((value) => {
+ const result = value > 0
+ expect(value).not.toBeUndefined()
+ return result
+ })
+ const render = jest.fn()
+
+ const { result } = renderHook(
+ () => {
+ render()
+ const value = selectedState()
+ return value
+ },
+ { wrapper: ({ children }) => {children} },
+ )
+
+ await waitFor(() => {
+ expect(result.current).toBe(false)
+ expect(selectedState.get()).toBe(false)
+ // re-render twice, as it hit suspense, because value is not resolved yet
+ expect(render).toHaveBeenCalledTimes(2)
+ })
+
+ state.set(1)
+
+ await waitFor(() => {
+ expect(result.current).toBe(true)
+ expect(selectedState.get()).toBe(true)
+ // next time it re-render only once, as value is already resolved
+ expect(render).toHaveBeenCalledTimes(3)
+ })
+ })
})
diff --git a/packages/core/create-state.ts b/packages/core/create-state.ts
index 6f44efe..3996490 100644
--- a/packages/core/create-state.ts
+++ b/packages/core/create-state.ts
@@ -45,7 +45,7 @@ export function createState(options: GetStateOptions): FullState {
return this
}
state.select = function (selector, isSelectorEqual = isEqualBase) {
- return select([state], selector, isSelectorEqual)
+ return select([state as never], selector, isSelectorEqual)
}
state.get = get
state.set = set as State['set']
diff --git a/packages/core/select.ts b/packages/core/select.ts
index ce9e64f..9bc7c7d 100644
--- a/packages/core/select.ts
+++ b/packages/core/select.ts
@@ -2,8 +2,8 @@ import { stateScheduler } from './create'
import { createState } from './create-state'
import { subscribeToDevelopmentTools } from './debug/development-tools'
import type { Cache, GetState, IsEqual } from './types'
-import { canUpdate, handleAsyncUpdate } from './utils/common'
-import { isUndefined } from './utils/is'
+import { AbortError, canUpdate, handleAsyncUpdate } from './utils/common'
+import { isPromise, isUndefined } from './utils/is'
type StateDependencies> = {
[K in keyof T]: GetState
@@ -21,8 +21,32 @@ export function select = []>(
const cache: Cache = {}
function computedValue(): T {
- const values = states.map((state) => state.get()) as S
- return selector(...values)
+ // const values = states.map((state) => state.get()) as S
+
+ const values: unknown[] = []
+ let hasPromise = false
+ for (const state of states) {
+ const value = state.get()
+ if (isPromise(value)) {
+ hasPromise = true
+ }
+ values.push(value)
+ }
+ if (hasPromise) {
+ return new Promise((resolve, reject) => {
+ Promise.all(values).then((resolvedValues) => {
+ // check if some of value is undefined
+ // eslint-disable-next-line sonarjs/no-nested-functions
+ if (resolvedValues.some((element) => isUndefined(element))) {
+ return reject(new AbortError())
+ }
+ const resolved = selector(...(resolvedValues as S))
+ resolve(resolved)
+ })
+ }) as T
+ }
+ const result = selector(...(values as S))
+ return result
}
function getValue(): T {
diff --git a/packages/core/types.ts b/packages/core/types.ts
index b67d937..a4171a2 100644
--- a/packages/core/types.ts
+++ b/packages/core/types.ts
@@ -48,7 +48,7 @@ export interface GetState {
* Select particular slice of the state.
* It will create "another" state in read-only mode (without set).
*/
- select: (selector: (state: Awaited | T) => S, isEqual?: IsEqual) => GetState
+ select: (selector: (state: Awaited) => S, isEqual?: IsEqual) => GetState
}
export interface State extends GetState {
diff --git a/packages/core/utils/common.ts b/packages/core/utils/common.ts
index af00d06..c736f6d 100644
--- a/packages/core/utils/common.ts
+++ b/packages/core/utils/common.ts
@@ -12,7 +12,7 @@ export class AbortError extends Error {
/**
* Cancelable promise function, return promise and controller
*/
-function cancelablePromise(promise: Promise, previousController?: AbortController): CancelablePromise {
+export function cancelablePromise(promise: Promise, previousController?: AbortController): CancelablePromise {
if (previousController) {
previousController.abort()
}