Skip to content

Commit

Permalink
Revert "breaking: remove $state.link callback (#12942)"
Browse files Browse the repository at this point in the history
This reverts commit 0b51ff0.
  • Loading branch information
Rich-Harris committed Aug 21, 2024
1 parent 0b51ff0 commit 85871d7
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 10 deletions.
5 changes: 0 additions & 5 deletions .changeset/sharp-foxes-whisper.md

This file was deleted.

18 changes: 18 additions & 0 deletions documentation/docs/03-runes/01-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ console.log(a, b); // 3, 3

As with `$state`, if `$state.link` is passed a plain object or array it will be made deeply reactive. If passed an existing state proxy it will be reused, meaning that mutating the linked state will mutate the original. To clone a state proxy, you can use [`$state.snapshot`](#$state-snapshot).

If you pass a callback to `$state.link`, changes to the input value will invoke the callback rather than updating the linked state, allowing you to choose whether to (for example) preserve or discard local changes, or merge incoming changes with local ones:

```js
let { stuff } = $props();

let incoming = $state();
let hasUnsavedChanges = $state(false);

let current = $state.link({ ...stuff }, (stuff) => {
if (hasUnsavedChanges) {
incoming = stuff;
} else {
incoming = null;
current = stuff;
}
});
```

## `$state.raw`

State declared with `$state.raw` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it:
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ declare namespace $state {
*
* @param value The linked value
*/
export function link<T>(value: T): T;
export function link<T>(value: T, callback?: (value: T) => void): T;

export function raw<T>(initial: T): T;
export function raw<T>(): T | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ export function VariableDeclaration(node, context) {
}

if (rune === '$state.link') {
value = b.call('$.source_link', b.thunk(value));
value = b.call(
'$.source_link',
b.thunk(value),
args.length === 2 && /** @type {Expression} */ (context.visit(args[1]))
);
} else if (is_state_source(binding, context.state.analysis)) {
value = b.call('$.source', value);
}
Expand Down
19 changes: 17 additions & 2 deletions packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ export function source(v) {
/**
* @template V
* @param {() => V} get_value
* @param {(value: V) => void} [callback]
* @returns {(value?: V) => V}
*/
export function source_link(get_value) {
export function source_link(get_value, callback) {
var was_local = false;
var init = false;
var local_source = source(/** @type {V} */ (undefined));

var linked_derived = derived(() => {
Expand All @@ -75,7 +77,20 @@ export function source_link(get_value) {
return value;
}

return (local_source.v = get(linked_derived));
var linked_value = get(linked_derived);

if (init) {
if (callback !== undefined) {
untrack(() => callback(linked_value));
return local_source.v;
}
} else {
init = true;
}

local_source.v = linked_value;

return linked_value;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
html: `<button>0</button><button>0</button><button>false</button>`,

test({ assert, target }) {
const [btn1, btn2, btn3] = target.querySelectorAll('button');

flushSync(() => btn1.click());
assert.htmlEqual(
target.innerHTML,
`<button>1</button><button>1</button><button>false</button>`
);

flushSync(() => btn2.click());
assert.htmlEqual(
target.innerHTML,
`<button>1</button><button>2</button><button>false</button>`
);

flushSync(() => btn3.click());
assert.htmlEqual(target.innerHTML, `<button>1</button><button>2</button><button>true</button>`);

flushSync(() => btn1.click());
assert.htmlEqual(target.innerHTML, `<button>2</button><button>2</button><button>true</button>`);

flushSync(() => btn1.click());
assert.htmlEqual(target.innerHTML, `<button>3</button><button>2</button><button>true</button>`);

flushSync(() => btn1.click());
flushSync(() => btn3.click());
assert.htmlEqual(
target.innerHTML,
`<button>4</button><button>4</button><button>false</button>`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
let a = $state(0);
let b = $state.link(a, (value) => {
if (c) return;
b = value;
});
let c = $state(false);
</script>

<button onclick={() => a++}>{a}</button>
<button onclick={() => b++}>{b}</button>
<button onclick={() => c = !c}>{c}</button>
2 changes: 1 addition & 1 deletion packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2937,7 +2937,7 @@ declare namespace $state {
*
* @param value The linked value
*/
export function link<T>(value: T): T;
export function link<T>(value: T, callback?: (value: T) => void): T;

export function raw<T>(initial: T): T;
export function raw<T>(): T | undefined;
Expand Down
18 changes: 18 additions & 0 deletions sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ console.log(a, b); // 3, 3

As with `$state`, if `$state.link` is passed a plain object or array it will be made deeply reactive. If passed an existing state proxy it will be reused, meaning that mutating the linked state will mutate the original. To clone a state proxy, you can use [`$state.snapshot`](#$state-snapshot).

If you pass a callback to `$state.link`, changes to the input value will invoke the callback rather than updating the linked state, allowing you to choose whether to (for example) preserve or discard local changes, or merge incoming changes with local ones:

```js
let { stuff } = $props();

let incoming = $state();
let hasUnsavedChanges = $state(false);

let current = $state.link({ ...stuff }, (stuff) => {
if (hasUnsavedChanges) {
incoming = stuff;
} else {
incoming = null;
current = stuff;
}
});
```

## `$state.raw`

State declared with `$state.raw` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it:
Expand Down

0 comments on commit 85871d7

Please sign in to comment.