Skip to content

Commit

Permalink
fix: fix dependencies detection in getters (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
ceski23 authored Jun 20, 2024
1 parent b9644aa commit 0e77b63
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 5 deletions.
36 changes: 36 additions & 0 deletions src/tests/createStore.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,40 @@ describe('useStoreEffect', () => {

expect(callback).toHaveBeenCalledTimes(2)
})

it('should update computed state when short-circuiting or using complex identifiers', () => {
const store = createStore({
firstName: 'John',
'second.😂$%^&#ĦĔĽĻŎName': undefined as string | undefined,
lastName: 'Smith',
showSecondName: false,
get name() {
return this.showSecondName
? `${this.firstName} ${this['second.😂$%^&#ĦĔĽĻŎName']} ${this.lastName}`
: `${this.firstName} ${this.lastName}`
},
})

expect(store.getState().name).toBe('John Smith')

store.actions.setShowSecondName(true)
store.actions['setSecond.😂$%^&#ĦĔĽĻŎName']('Andrzej')

expect(store.getState().name).toBe('John Andrzej Smith')
})

it('should deduplicate dependency listeners', () => {
const store = createStore({
firstName: 'John',
get name() {
return `${this.firstName} ${this.firstName} ${this.firstName} ${this.firstName} ${this.firstName}`
},
})

const callback = jest.fn()
store.effect(({ name }) => callback(name))
store.actions.setFirstName('Andrzej')

expect(callback).toHaveBeenCalledTimes(2)
})
})
22 changes: 17 additions & 5 deletions src/vanilla/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,24 +115,36 @@ export const createStore = <TState extends object>(stateRaw: TState) => {
return
}

const dependencies = new Set<TKey>()
const proxiedState = new Proxy(state, {
get: (target, dependencyKey, receiver) => {
if (!keyInObject(dependencyKey, target)) {
return undefined
}

subscribe([dependencyKey])(() => {
const newValue = Object.getOwnPropertyDescriptor(stateRaw, key)?.get?.call(target) as TState[TKey]
dependencies.add(dependencyKey)

target[key] = newValue
listeners[key].forEach(listener => listener(newValue))
})
const getterBody = Object.getOwnPropertyDescriptor(stateRaw, key)?.get?.toString()

// Heuristic for detecting dependencies in getter body
getterBody?.match(/this.([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)/ug)
?.map(dependency => dependency.replace('this.', ''))
.forEach(dependency => dependencies.add(JSON.parse(`"${String(dependency)}"`) as TKey))
Array.from(getterBody?.matchAll(/this\[['"`](.*)['"`]\]/g) ?? [])
.forEach(([, dependency]) => dependencies.add(JSON.parse(`"${String(dependency)}"`) as TKey))

return Reflect.get(target, dependencyKey, receiver)
},
})

state[key] = Object.getOwnPropertyDescriptor(stateRaw, key)?.get?.call(proxiedState)

subscribe(Array.from(dependencies))(() => {
const newValue = Object.getOwnPropertyDescriptor(stateRaw, key)?.get?.call(state) as TState[TKey]

state[key] = newValue
listeners[key].forEach(listener => listener(newValue))
})
})

const reset = (...keys: Array<keyof RemoveReadonly<TState>>) => {
Expand Down

0 comments on commit 0e77b63

Please sign in to comment.