Skip to content
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

Add createLens primitive #452

Draft
wants to merge 46 commits into
base: main
Choose a base branch
from

Conversation

nathanbabcock
Copy link

@nathanbabcock nathanbabcock commented May 28, 2023

Utilities for working with nested reactivity in a modular way.

  • createLens - Given a path within a Store object, return a derived or "focused"
    getter and setter pair.

  • createFocusedGetter - The first half of the lens tuple; a derived signal
    using path syntax on an object.

  • createFocusedSetter - The second half of the lens tuple; a Setter
    for a specific path within a Store.

How to use it

// Start with an ordinary SolidJS Store
const storeTuple = createStore([
  { myString: 'first' }
])

// Create a lens to focus on one particular item in the Store.
// Any valid path accepted by `setStore` works here!
const [firstString, setFirstString] = createLens(storeTuple, 0, myString)

// Setters and Getters work just like ordinary Signals
setFirstString("woohoo") // equivalent to `setStore(0, "myString", "woohoo")
console.log(firstString()) // "woohoo"

Motivation

1. Separation of Concerns

Components can receive scoped Setters for only the parts of state they need
access to, rather than needing a top-level setStore function.

2. Type-safety

Essentially, we are just partially applying a setStore
function with an initial path, and returning a function that will apply the
remainder of the path. It is just syntactic sugar, and under the hood
everything is using calls to native Store functionality.

The same approach can already be used by the Setter returned by createStore. However,
Typescript users will find it hard to maintain type-safety for the arguments
passed to a "derived"/partially-applied Setter. The type definitions for SetStoreFunction are...
daunting.

The lenses package alleviates this friction by providing both StorePath<T>
and EvaluatePath<T, P> generic type helpers.

3. Shared path syntax between Getters and Setters

The path syntax defined in Solid Stores is incredibly expressive and powerful.
By introducing createScopedGetter, the same syntax can be also be used to
access Store values as derived Signals. This is particularly relevant to
child components which may both display and modify items from a Store
collection.

Closes #453

@changeset-bot
Copy link

changeset-bot bot commented May 28, 2023

⚠️ No Changeset found

Latest commit: d9a56e3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@nathanbabcock
Copy link
Author

Source code is inside packages/lenses: https://github.com/nathanbabcock/solid-primitives/tree/lens/packages/lenses

@nathanbabcock nathanbabcock reopened this May 29, 2023
@thetarnav
Copy link
Member

This is interesting, but why not rely on the "native" store scoping ability:

const [store, setStore] = createStore([
  {
    inner: {
      innerString: "first",
      innerNumber: 0,
    },
  },
])

const [inner, setInner] = createStore(store[0].inner)

setInner("innerString", "hello") // reflected in both stores as they share the same reference.

With stores you can rely on the references being the same, so any mutations to one will be reflected in all the other proxies that wrap the object.

I've replaced the createLens body with this, and all tests have passed:

const last = path[path.length - 1];
path = path.slice(0, -1);

const storeNode = store[0] instanceof Function ? store[0]() : store[0];
const value = getValueByPath(storeNode as StoreNode, path) as V;

const [get, set] = createStore(value as any);

return [() => get[last], (...args: any[]) => set(last, ...args)];

I don't hate the idea, but I want to know what exactly is the benefit of these primitives, as creating basic lenses is already in reach.

@nathanbabcock
Copy link
Author

nathanbabcock commented May 29, 2023

I had no idea createStore could be used in this way. For some reason I thought it would double-wrap in a Proxy, or the references wouldn't stay consistent over the lifetime of the store.

I'm pretty sure that does everything I need it to do unless I'm missing something.

@clinuxrulz
Copy link

@thetarnav
Thank you so much for that. I found I had a need for lenses on stores again, and had no idea createStore could be used like that. Was thinking using it like that would be invalid (double wrap).

That createStore lenses trick should be documented somewhere, like in the documentation for stores.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add createLens primitive
3 participants