Skip to content

feat!: implement injectHydration; remove manualHydration #195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions packages/atoms/src/classes/Ecosystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
MaybeCleanup,
NodeFilter,
ParamlessTemplate,
PartialAtomInstance,
Selectable,
SelectorGenerics,
EventMap,
Expand Down Expand Up @@ -975,22 +974,6 @@ export class Ecosystem<Context extends Record<string, any> | undefined = any>
}
}

/**
* Should only be used internally
*/
public _consumeHydration(instance: PartialAtomInstance) {
const hydratedValue = this.hydration?.[instance.id]

if (typeof hydratedValue === 'undefined') return

// hydration must exist here. This cast is fine:
delete (this.hydration as Record<string, any>)[instance.id]

return instance.t.hydrate
? instance.t.hydrate(hydratedValue)
: hydratedValue
}

/**
* `a`ddJob - add a job to the scheduler without scheduling. The scheduler
* should be already either running or scheduled when calling this.
Expand Down
12 changes: 9 additions & 3 deletions packages/atoms/src/classes/instances/AtomInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ export class AtomInstance<
*/
public a: boolean | undefined = undefined

/**
* injected`H`ydration - whether `injectHydration` was called in the atom
* state factory. If it was, we don't hydrate afterward.
*/
public H = false

/**
* `I`njectors - tracks injector calls from the last time the state factory
* ran. Initialized on-demand
Expand Down Expand Up @@ -377,10 +383,10 @@ export class AtomInstance<
this.j()

// hydrate if possible
const hydration = this.e._consumeHydration(this)
if (!this.H) {
const hydration = this.e.hydration?.[this.id]

if (!this.t.manualHydration && typeof hydration !== 'undefined') {
this.set(hydration)
if (typeof hydration !== 'undefined') this.h(hydration)
}

flushBuffer(n)
Expand Down
1 change: 0 additions & 1 deletion packages/atoms/src/classes/templates/AtomTemplateBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export abstract class AtomTemplateBase<
public readonly dehydrate?: AtomConfig<G['State']>['dehydrate']
public readonly tags?: string[]
public readonly hydrate?: AtomConfig<G['State']>['hydrate']
public readonly manualHydration?: boolean
public readonly ttl?: number

/**
Expand Down
1 change: 1 addition & 0 deletions packages/atoms/src/injectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './injectAtomValue'
export * from './injectCallback'
export * from './injectEcosystem'
export * from './injectEffect'
export * from './injectHydration'
export * from './injectMappedSignal'
export * from './injectMemo'
export * from './injectPromise'
Expand Down
44 changes: 44 additions & 0 deletions packages/atoms/src/injectors/injectHydration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { injectSelf } from './injectSelf'

/**
* Returns a hydration previously passed to `ecosystem.hydrate` for this atom's
* id.
*
* This value can be passed directly to `injectSignal` to initialize signal
* state. It will be returned again on subsequent evaluations, though you
* typically ignore it.
*
* The rule of thumb is to always hydrate at the source - top-level
* atoms/signals. While you can hydrate mapped signals or derived atoms, that
* will result in an extra evaluation. Though, since the second evaluation
* happens immediately, before the new atom can be used, it's usually fine.
*
* Scoped atoms can't be hydrated. Hydrate the context instead.
*
* When an atom is destroyed and recreated, it will receive this same hydration
* again. You can prevent this by listening to the `cycle` ecosystem event and
* deleting `ecosystem.hydration[event.source.id]` when `source` becomes Active.
*
* If the atom configured a `hydrate` function, it will be called and the
* resulting, transformed value returned. Pass `{ transform: false }` to prevent
* this.
*
* When `injectHydration` is called in any atom, it prevents Zedux from trying
* to auto-hydrate the atom after initial evaluation. Pass `{ intercept: false
* }` to prevent this.
*/
export const injectHydration = <T = unknown>(config?: {
intercept?: boolean
transform?: boolean
}): T => {
const self = injectSelf()
self.H ||= config?.intercept !== false

const hydration = self.e.hydration?.[self.id]

return typeof hydration === 'undefined'
? hydration
: config?.transform !== false && self.t.hydrate
? self.t.hydrate(hydration)
: hydration
}
1 change: 0 additions & 1 deletion packages/atoms/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export interface AtomConfig<State = any> {
dehydrate?: (state: State) => any
tags?: string[]
hydrate?: (dehydratedState: unknown) => State
manualHydration?: boolean
ttl?: number
}

Expand Down
15 changes: 15 additions & 0 deletions packages/react/test/integrations/plugins.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -472,4 +472,19 @@ describe('plugins', () => {

expect(calls).toEqual(expectedCalls)
})

test('change listeners can detect `mutate` events', () => {
const calls: any[] = []
const signal = ecosystem.signal({ a: 1 })

ecosystem.on('change', (event, eventMap) => {
calls.push(eventMap.mutate)
})

signal.mutate(state => {
state.a = 2
})

expect(calls).toEqual([[{ k: 'a', v: 2 }]])
})
})
82 changes: 82 additions & 0 deletions packages/react/test/integrations/ssr.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { atom, injectHydration, injectSignal } from '@zedux/atoms'
import { ecosystem } from '../utils/ecosystem'

describe('ssr', () => {
test('injectHydration intercepts automatic hydration', () => {
const calls: any[] = []

const atom1 = atom('1', () => {
const hydration = injectHydration()
calls.push(hydration)

// make state something that doesn't match the hydration:
return injectSignal(hydration + 'c')
})

ecosystem.hydrate({ 1: 'ab' })

const node = ecosystem.getNode(atom1)

expect(calls).toEqual(['ab'])
expect(node.get()).toBe('abc')
})

test('injectHydration({ intercept: false }) does not intercept automatic hydration', () => {
const calls: any[] = []

const atom1 = atom('1', () => {
const hydration = injectHydration({ intercept: false })
calls.push(hydration)

// make state something that doesn't match the hydration:
return injectSignal(hydration + 'c')
})

ecosystem.hydrate({ 1: 'ab' })

const node = ecosystem.getNode(atom1)

expect(calls).toEqual(['ab', 'ab'])
expect(node.get()).toBe('ab')
})

test('injectHydration transforms the value by default', () => {
const atom1 = atom(
'1',
() => {
const value = injectHydration<number>()

return value
},
{
hydrate: val => (val as number) + 1,
}
)

ecosystem.hydrate({ 1: 1 })

const node = ecosystem.getNode(atom1)

expect(node.get()).toBe(2)
})

test('injectHydration({ transform: false }) prevents transformation', () => {
const atom1 = atom(
'1',
() => {
const value = injectHydration<number>({ transform: false })

return value
},
{
hydrate: val => (val as number) + 1,
}
)

ecosystem.hydrate({ 1: 1 })

const node = ecosystem.getNode(atom1)

expect(node.get()).toBe(1)
})
})
18 changes: 7 additions & 11 deletions packages/react/test/stores/ssr.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,19 +255,15 @@ describe('ssr', () => {
})

test('all injectors receive the hydration', () => {
const atom1 = atom(
'1',
() => {
const a = injectStore('a', { hydrate: true })
const b = injectStore('b', { hydrate: true })
const atom1 = atom('1', () => {
const a = injectStore('a', { hydrate: true })
const b = injectStore('b', { hydrate: true })

const store = injectStore(() => createStore({ a, b }))
store.use({ a, b })
const store = injectStore(() => createStore({ a, b }))
store.use({ a, b })

return store
},
{ manualHydration: true }
)
return store
})

ecosystem.hydrate({ 1: 'ab' })
const instance = ecosystem.getInstance(atom1)
Expand Down
5 changes: 2 additions & 3 deletions packages/stores/src/injectStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createStore, zeduxTypes, Store } from '@zedux/core'
import {
injectEffect,
injectHydration,
injectRef,
injectSelf,
InjectStoreConfig,
Expand Down Expand Up @@ -108,9 +109,7 @@ export const injectStore: {
: (hydration?: State) =>
createStore<State>(null, hydration ?? storeFactory)

ref.current = getStore(
config?.hydrate ? instance.e.hydration?.[instance.id] : undefined
)
ref.current = getStore(config?.hydrate ? injectHydration() : undefined)
}

injectEffect(
Expand Down