solid-atom
v1.0.3
Published
Simple two way reactive bindings
Downloads
8
Maintainers
Readme
solid-atom
Simple two way reactive bindings
Usage
import { createSignal } from "solid-js";
import { Atom } from "solid-atom";
function Counter(props: { atom?: Atom<number> }) {
const atom = Atom.source(() => props.atom, () => createSignal(12));
return <>
<button onClick={() => atom.value++}>
{atom.value}
</button>
</>
}
function App() {
const atom = Atom.value(1);
return <>
<button onClick={() => atom.value--}>
Decrease
</button>
{/* Controlled from the outside */}
<Counter atom={atom} />
{/* Starts at 12 */}
<Counter />
</>
}Documentation
Atom
Object that wraps something that's both readable and writable
memo(): Creates a newAtomwith the setter of the current one and a memoized version of its getterconvert(): Creates a newAtomthat applies a conversion to the current oneupdate(): Like theSetteroverload of aSignalthat takes a function with the previous valuedefer(): Creates a newAtomthat defers the setter of the current oneselector(): Two way version ofcreateSelector()readOnly()(static): Creates a newAtomthat throws an error when trying to set itunwrap()(static): Allows the use of anAccessorof anAtomwithout having to call theAccessoreach timefrom()(static): Creates anAtombased on aSignalprop()(static): Creates anAtombased on an object propertyvalue()(static): Creates a newAtomthat directly stores a valuesource()(static): Similiar toAtom.unwrap(), but if theAccessordoesn't return anything it automatically creates an internalSignalin which to store the value
Utility
The module also exposes some of its internal utilities
NamesOf: Utility type that powersnameOf()nameOf(): Utility function that powersAtom.prop()NO_OP: Function that does nothingIDENTITY: Function that returns passed value and can be used as a class
Why
Two way bindings are annoying to do in "solid-js", here are some things that I personally tried doing
- ✗ Making props writable: Possible only with one attribute
- ✗ Passing the value and an event to change it: Very annoying when forwarding props, since there are TWO properties to pass for EACH two way binding (And in my project I had a lot on each component)
- ✗ Passing a whole
Signaltuple: We're getting closer, butSignals have NOT been made to be used directly- Not unpackable: If you unpack them they lose one layer of reactivity
const [ get, set ] = props.signal; // What if `props.signal` itself changes? get(); // This could be the `Accessor` of the OLD `Signal` - Indexes: Because of the previous problem, you MUST access the components of a
Signalthrough its indexes, which is both ugly and doesn't make the intention clearconst value = props.signal[0](); // When reading props.signal[1](value); // When writing - Wrapped value: You have always to wrap the value inside of a lambda before passing it to the
Setterif you are not sure that it's not a functionprops.signal[1](() => value); // If value it's a function, it will get called if passed directlyDoing benchmarks, I noticed that it is faster to always wrap with a lambda rather than doing it only if the value is a function
- Ugly to customize: Let's suppose you want to create a custom
Signalto give it a custom behaviour. For example, let's make a function that converts a property into aSignal:
You may be tempted to try something like this, but you'll soon notice that NOT ONLY you have to deal with thefunction prop<T, K extends keyof T>(obj: T, k: K): Signal<T[K]> { return [ () => obj[k], // Getter v => obj[k] = v // Setter (Wrong) ]; }Setteroverload that takes a function when setting, but we also have to explicitly implement that thing ourselves (Since we want our output to behave like aSignal)// ... v => obj[k] = typeof v === "function" ? v(obj[k]) : v; // ... - Requires synchonization: Binding a value requires creating your own
Signaland synchronizing it with the one passed by the user (If present)const [ get, set ] = createSignal(defaultValue); createEffect(on(() => props.signal[0](), x => set(() => x))); createEffect(on(get, x => props.signal[1](() => x)));This is considered bad practice because it breaks the traceability of the "reactivity graph"
- Not unpackable: If you unpack them they lose one layer of reactivity
- ✓ Creating a custom substitute for the
Signal: This is what the library provides- Unpackable: The getter and the setter work indipendently from their
Atomconst { get, set } = Atom.unwrap(() => props.atom); // The `Atom` gets wrapped get(); // This wraps the getter of the original `Atom` - Actual properties: You can interact with the
Atomthrough named propertiesconst atom = Atom.unwrap(() => props.atom); atom.value++; // Actual property for both getting and setting the value const value = atom.get(); // Named getter atom.set(value); // Named setter atom.update(x => value + x); // The infamous setter overload that takes a function atom.update(); // Writes back the same value - No wrapper: You can pass whatever you want to both the property and the setter
props.atom.value = value; // Or props.atom.set(value); - Easy to customize: You just need to create a new
Atomwith your implementationfunction prop<T, K extends keyof T>(obj: T, k: K) { return new Atom<T[K]>( () => obj[k], // Getter v => obj[k] = v // Setter ); }There's already
Atom.prop()for doing this - Only forwarding: Binding a value does NOT require synchronization
const atom = Atom.source(() => props.atom); // (Starts at `undefined` if there's no `Atom` from the user) // Or const atom = Atom.source(() => props.atom, () => createSignal(defaultValue));- If the user doesn't provide
props.atom, aSignalwill be created to store the actual value - If the user provides
props.atom, both its getter and setter will symply be forwarded (No synchronization)
This will also give the end-user control over things like the comparison function of the
Signalthat ultimately stores the value - If the user doesn't provide
- Unpackable: The getter and the setter work indipendently from their
