[WIP] Refactor to use the new jotai store exposed internal methods (Attempt 3) #69
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Goals
1. Don't copy scoped atoms.
Currently, we copy scoped atoms because all atoms (scoped and unscoped) use the same base store. When scoped atoms are copied, their
init
property is accessed an extra time (per copy). This causes issues with atoms such as atomWithLazy. The new approach will be to use separate stores for each scope layer and to create a routing function that interprets the atom correctly.2. Don't copy unscoped derived atoms.
Currently, we copy all derived atoms. This allows us shim the getter so derived atoms can read scoped atoms. The issue with doing this is that it causes another mount event for the copy. This causes issues with atoms such as
atomWithObservable
. The plan is to use internal store methods to resolve the correct atom / atomState rather than shimming the getter. We can also create our own readAtomState, but that might be too heavy-handed.3. Remove the writable hack.
Currently, writable atoms may read or write to scoped atoms, so we need to shim their write function's getter and setter. However, we cannot copy writable atoms because they might also hold a meaningful value. So instead we mutate them temporarily. I'd like to avoid this.
Summary of changes
Update ScopeProvider to use the new exposed internals api (pmndrs/jotai#2911).
The new code should address #25 and #36.
TODO
Prior art
#56
#33
Overview of Expected Behavior
FAQ
Types of atoms
atoms type A & B can hold a value. Type B can also reference atoms in the current scope. When a scope inherits a valued atom (A or B), the inherited value is used.
For Type B, we currently:
I'm generally not a fan of mutating configs but I don't see any other way, unless Jotai natively supports creating a cloned copy of an atom whose config references the original atom's value in the store.
Scoping Priority
Existing Implementation
Scope
createScope handles everything dealing with scope and is moved to a separate file.
WeakMaps
explicit
andimplicit
atoms are scopedgetAtom
from the originalAtom, gets the explicit, implicit, inherited, unscoped derived, and unscoped primitive atoms. As necessary, creates scoped copies (one-time) and saves them in their appropriate weakmap.
Inheriting Atoms
Atoms are inherited from the ancestor scopes. The top ancestor is the global scope where the original atom is used.
Inherited Derived Atoms
To inherit atoms that have a custom read, they are copied so that the custom read and write functions can use that scope's getAtom function.
Inheriting Primitive Atoms
To inherit primitive atoms, those atoms are stored directly and not copied. Since the atom itself is the key to its value, we need to store the original in the weakmap.
Inherited Write Only Atoms
Where someAtom could be scoped.
For write-only atoms, since these atoms also hold a value, we also want to preserve the originals. However the write function may access atoms in the current scope. To work around this, we modify the original write method synchronous before baseStore.set and restore the original write method synchronous after in a finally block.
What I'd love is a utility to clone an atom but have the clone also resolve the same value as the original. That way I can copy atoms like below and wrap their write method to resolve atoms in the current scope.
Summary of Changes
TBC
Tests Results
01_basic_spec
❌ 01. ScopeProvider does not provide isolation for unscoped primitive atoms
❌ 02. unscoped derived atoms are unaffected in ScopeProvider
❌ 03. ScopeProvider provides isolation for scoped primitive atoms
❌ 04. unscoped derived can read and write to scoped primitive atoms
❌ 05. unscoped derived can read both scoped and unscoped atoms
❌ 06. dependencies of scoped derived are implicitly scoped
❌ 07. scoped derived atoms can share implicitly scoped dependencies
❌ 08. nested scopes provide isolation for primitive atoms at every level
❌ 09. unscoped derived atoms in nested scoped can read and write to scoped primitive atoms at every level
❌ 10. inherited scoped derived atoms can read and write to scoped primitive atoms at every nested level
02_removeScope
❌ atom get correct value when ScopeProvider is added/removed
03_nested
❌ nested primitive atoms are correctly scoped
04_derived
❌ parent scope's derived atom is prior to nested scope's scoped base
05_derived_self
❌ derived dep scope is preserved in self reference
06_implicit_parent
❌ level 1: "BD" and level 2: "BD"
❌ level 1: "BD" and level 2: "DB"
❌ level 1: "DB" and level 2: "BD"
❌ level 1: "DB" and level 2: "DB"
07_writable
❌ "writableAtom" updates its value in both scoped and unscoped and read scoped atom (27 ms)
❌ "thisWritableAtom" updates its value in both scoped and unscoped and read scoped atom (5 ms)