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

Feat: Add Box #207

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Feat: Add Box #207

wants to merge 3 commits into from

Conversation

abdel-17
Copy link
Collaborator

@abdel-17 abdel-17 commented Jan 15, 2025

Introduction

This PR solves some of the shortcomings of when it comes to passing state across function boundaries. To preserve reactivity, you have to thunkify the argument () => T, but TypeScript doesn't play well with functions when it comes to type inference.

if (element() !== null) {
  element().focus(); // Type error because `element()` is still `HTMLElement | null`
}

The Angular team and Solid's creator Ryan both commented on a TS issue that may solve this issue, but until then, we're stuck with this.

You can use $derived.by to turn the getter to a simple property access, but you have to do this manually for every property and for every function call. Lots of boilerplate.

function one(props: { one: () => A, two: () => B }) {
  const one = $derived.by(props.one);
  const two = $derived.by(props.two);
}

function two(props: { one: () => A, two: () => B }) {
  const one = $derived.by(props.one);
  const two = $derived.by(props.two);
}

Also, if the getter just simply accesses a prop, it doesn't need to be memoized. It's better to move the decision of memoization to the caller rather than the consumer.

box()

The first Box implementation is box(). It uses $derived under the hood, so it's memoized.

import { box, type Box } from "runed";

let { numbers }: { numbers: number[] } = $props();

function useSum(props: { sum: Box<number> }) {...}

useSum({
  sum: box(() => Math.sumPrecise(numbers)),
});

box() also supports two-way binding by passing a setter as the second argument.

import { box, type WritableBox } from "runed";

let { count = $bindable(0) }: { value: number } = $props();

function useDoubled(props: { doubled: WritableBox<number> }) {...}

useDoubled({
  doubled: box(
    () => count * 2,
    (v) => count = v / 2,
  ),
});

ref()

The second Box implementation is ref(). It's very similar to box(), but doesn't use $derived. This is more suitable for computations that don't benefit from memoization, like passing props around.

import { ref, type Box } from "runed";

let { foo }: { foo: Foo } = $props();

function useFoo(props: { foo: Box<Foo> }) {...}

useFoo({
  foo: ref(() => foo),
});

ref() also supports two-way binding by passing a setter as the second argument.

import { ref, type WritableBox } from "runed";

let { value = $bindable("") }: { value: string } = $props();

function useInput(props: { value: WritableBox<string> }) {...}

useInput({
  value: ref(
    () => value,
    (v) => value = v,
  ),
});

Compatibility

Our existing utilities expect you to pass getters and setters around, which is why Box exposes the get and set properties.

import { box, StateHistory } from "runed";

const count = box(...);
const history = new StateHistory(count.get, count.set);

Copy link

changeset-bot bot commented Jan 15, 2025

⚠️ No Changeset found

Latest commit: f87a5ef

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

Copy link
Contributor

github-actions bot commented Jan 15, 2025

built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
runed ✅ Ready (View Log) Visit Preview f87a5ef

@TGlide
Copy link
Member

TGlide commented Jan 15, 2025

It's better to move the decision of memoization to the caller rather than the consumer.

Not too sure I agree with this. Each function will know how often it access the value, and so how important memoization is. Sure, we can flip it around to the caller knowing how often it changes, but is it worth it if the complexity is shifted to everytime the function is called?

@TGlide
Copy link
Member

TGlide commented Jan 15, 2025

If the name is Derived.by, to mimic Svelte's api, I don't think it should allow a setter, as Svelte's also does not. It is a bit confusing

@TGlide
Copy link
Member

TGlide commented Jan 15, 2025

Derived and Ref implementing a type called Box is also confusing to me, there's little discoverability. Why not call everything Box?

@abdel-17
Copy link
Collaborator Author

I update the PR with the new changes. Dropped the classes. It was indeed a strange API.

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.

2 participants