sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.93k stars 4.15k forks source link

Ability to select store properties to filter reactivity / derived #8470

Open oodavid opened 1 year ago

oodavid commented 1 year ago

Describe the problem

When a store exposes a complex object, we may want to selectively rebuild based on properties.

For example, a store may expose a User:

interface User {
    name: string;
    email: string;
    dob: Date;
};
const user = writable<User>();

We may calculate our age from the date. If we only want to recalculate if the dob property has changed, we have to put extra checks in place.

let age: number | undefined;
let lastDobTime: number | undefined;
$: {
    // Check that the properties we're interested in haven't changed
    const dobTime = $user.dob.getTime();
    if (dobTime != lastDobTime) {
        lastDobTime = dobTime;
        // The actual logic
        var diff_ms = Date.now() - dobTime;
        var age_dt = new Date(diff_ms);
        age = Math.abs(age_dt.getUTCFullYear() - 1970);
    }
}

...you could see how these checks could bloat with more complex logic

Describe the proposed solution

Have a way to select properties from stores, like so:

let age: number | undefined;
$: dobTime = user.select(($user) => $user.dob.getTime());
$: {
    var diff_ms = Date.now() - dobTime;
    var age_dt = new Date(diff_ms);
    age = Math.abs(age_dt.getUTCFullYear() - 1970);
}

NB, if #8469 was implemented, we could do this with a reactive store:


const age = reactive<number>((set) => {
    const dobTime = user.select(($user) => $user.dob.getTime());
    var diff_ms = Date.now() - dobTime;
    var age_dt = new Date(diff_ms);
    set(Math.abs(age_dt.getUTCFullYear() - 1970));
});

Alternatives considered

Once again, I'm taking inspiration from the Riverpod package for Dart / Flutter. It has a lot of similar goals and behaviours as svelte stores.

https://riverpod.dev/docs/concepts/reading#using-select-to-filter-rebuilds

Importance

would make my life easier

flakolefluk commented 1 year ago

I think this can be done today, without needing to do those checks. I think the issues is that the right side of the reactive assignment is still suggesting that user is a dependency.

<script lang="ts">
    import { writable } from 'svelte/store';
    function nameCheck(name: string) {
        console.log('Called only when name changes');
        return name;
    }

    function dobToAge(dob: number) {
        console.log('Called only when dob changes');
        var diff_ms = Date.now() - new Date(dob).getTime();
        var age_dt = new Date(diff_ms);
        return Math.abs(age_dt.getUTCFullYear() - 1970);
    }

    interface User {
        name: string;
        email: string;
        dob: Date;
    }

    const user = writable<User>({
        name: 'jane',
        email: 'some@email.com',
        dob: new Date('1990-01-01')
    });

    $: dobAsNum = +$user.dob; // evaluates when user changes
    $: age = dobToAge(dobAsNum); // evaluate when dobAsNum changes
    $: nameTracker = $user.name;  // evaluates when user changes
    $: name = nameCheck(nameTracker); // evaluates when nameTracker changes
</script>

<p>
    Name:
    {name}
</p>
<p>
    {age}
</p>

<button
    on:click={() =>
        user.update((u) => {
            let end = new Date('2000-01-01');
            let start = new Date('1990-01-01');

            const timeDiff = end.getTime() - start.getTime();
            const randomTime = Math.random() * timeDiff;
            const randomDate = new Date(start.getTime() + randomTime);
            return { ...u, dob: randomDate };
        })}>Change to random dob</button
>
<button on:click={() => user.update((u) => ({ ...u, name: u.name == 'jane' ? 'john' : 'jane' }))}
    >Toggle name</button
>

note that I'm not tracking dob, as it will always emit when user changes, but it's numeric value.

The same can be achieved with stores

const dobAsNum = derived(user, user => +user.dob); // evaluates when user changes
const age = derived(dobAsNum, dobToAge); // evaluate when dobAsNum changes

if you want to extract some of this into a select fn.

function select<T, U extends Readable<T>>(store: U, prop: keyof T) {
    return derived(store, (s) => s[prop]);
}

const nameTracker = select(user, 'name')  // evaluates when user changes
$: name = nameCheck($nameTracker); // evaluates when nameTracker changes

But it will still have the same issues with derived values where typeof is 'object'

So for Dates you could

function selectDateAsNumber<T, U extends Readable<T>>(store: U, prop: keyof T) {
        let isDate = s[prop] instanceof Date
        if(!isDate) {
             throw new Error("a Date was expected")
        }
    return derived(store, (s) => +s[prop]);
}

const dobAsNum =  selectDateAsNumber(user, 'dob'); // evaluates when user changes
$: age = dobToAge($dobAsNum); // evaluate when dobAsNum changes

note: Only parts of the code have been tested.