Ok, brain dump time.
We often get questions in the chatroom about why you can't use methods in templates, and that sort of thing. And React people often look at code like this...
<Nested foo={bar}/>
<script>
import Nested from './Nested.html';
export default {
components: { Nested },
data: () => ({
bar: 1
})
};
</script>
...and ask 'why do you need to register the component? We don't have that problem because our apps are Just JavaScriptβ’οΈ'. And while there are good technical reasons for both of those things, in the context of the current design, the truth is they have a point.
I've occasionally wondered if there's a way to have the best of both worlds. It boils down to the template being able to access variables that are in scope in the <script>:
<script>
import Nested from './Nested.html';
const bar = 1;
</script>
<Nested foo={bar}/>
The issue, of course, is that bar is no longer reactive β there's no way to say 'this value has changed, please update the child component'.
Last week, the React team introduced hooks. If you haven't watched Sophie and Dan's talk, I do recommend it.
I've been slightly ambivalent about hooks β for the most part, they solve problems that are specific to React. But they do make me wonder if there's a way to solve the worst ergonomic drawbacks of Svelte's approach.
Here's a version of the example above using the useState hook to make bar reactive:
<script>
import { useState } from 'svelte/hooks';
import Nested from './Nested.html';
const [bar, setBar] = useState(1);
</script>
<Nested foo={bar}/>
<button on:click="setBar(bar + 1)">increment</button>
I had a go at rewriting a few more examples in this style to get a sense of what it would entail. I've been fairly pleasantly surprised; in many cases no changes are necessary (because the components only contain markup and possibly styles), and in the cases where changes are necessary, the code gets cleaner and more concise.
What I haven't done is figure out exactly what this would mean for the compiler. Svelte's whole deal is that it avoids doing as much work as possible, and there are two main components to that β avoiding touching the DOM, and not giving the user a place to accidentally do unnecessary and expensive computation. This means a) knowing which values could have changed, and b) not having a big ol' render function. This approach clearly makes that harder. Impossible? Not sure.
Things I'm thinking about, in no particular order:
setFoo(...); setBar(...) instead of set({ foo: 1, bar: 2 }) means you really need to batch the work up, which sort of forces us into an async paradigm. Maybe that's ok? Not sure<script> β at least in useComputed (React has useMemo for a similar purpose) and the equivalent of useEffectoncreate, ondestroy etc, particularly when there are bindings or immediate set(...) calls. This feels like it could be a way to side-step those entirelyexport { thingMyParentWants }?This issue might alarm some of you; rest assured I'm not going to immediately rewrite Svelte just because Sunil goaded me on Twitter, and this is just the very beginning of this process of exploration. It may go nowhere. If we do end up using some of these ideas, we could probably leverage svelte-upgrade.
Key point to note: even if we do end up creating slightly more work for Svelte than it currently has to do (by making it less clear which values could have changed, etc) we'd still be in a better place than React, since a) we don't have the overhead of a virtual DOM, b) we have all the affordance of HTMLx, c) ...including <style>.
I think it's at the very least worth considering β keeping Svelte feeling modern, and in line with evolving developer tastes, is an important consideration alongside the mission of making it easy to write small, fast apps (or making it difficult to write large slow ones).
Let me quickly vomit another half-formed thought onto the screen:
We're a compiler. That means we don't need to play by everyone else's rules. So we could do something like this...
<script>
import Nested from './Nested.html';
let bar = 1;
</script>
<Nested foo={bar}/>
<button on:click="bar += 1">increment</button>
That bar += 1 could be augmented thusly:
button.addEventListener('click', () => {
bar += 1;
scheduleRerender(['bar']);
});
Every time a variable is assigned to, the compiler could trigger a re-render with knowledge of what changed. This would also work inside the <script> tag:
<p>the time is {time.toISOString()}</p>
<script>
import { oncreate } from 'svelte/lifecycle';
let time = Date.now();
oncreate(() => {
let interval = setInterval(() => {
time = Date.now();
}, 1000);
return () => clearInterval(interval);
});
</script>
oncreate(() => {
let interval = setInterval(() => {
time = Date.now();
scheduleRerender(['time']);
}, 1000);
return () => clearInterval(interval);
});
This is composable β you just need to use a closure:
// React-style
const [time, setTime] = useState(new Date());
useCustomHook(setTime);
// Svelte-style
let time = new Date();
const setTime = () => time = new Date();
useCustomHook(setTime);
@Rich-Harris First example in the last comment is absolutely awesome!
Most important idea:
We're a compiler. That means we don't need to play by everyone else's rules.
A couple of other things that occurred to me on the subway:
<script> tag only runs once, as it currently does, rather than on every re-render (which is what is implied by hooks). In that last example, where we import a lifecycle function (I have to get out of the habit of calling them 'lifecycle hooks', that word is now loaded), we don't need to rely on any funky order-dependence tricks. We might be able to completely bypass both the weirdness and the performance gotchas of hooks, and be in a better position, ergonomically speakingGod damn, I'm excited
My initial reaction to this is: I have ideas for wacky alternate ways I'd like to interact with component state, but I'm waiting for the store contract to become formalized before I start getting crazy.
Could we enable that and play around with alternate state management in userland a while? It wouldn't give us the "the script tag is the scope", which is cool, but it might let us play around with a hooks-like setWhatever scheme, or whatever other interface people dream up.
I do want that, yeah. Though I do also wonder if our ideas about state management would be different if we adopted an alternative approach to defining components. I think we should proceed, gingerly, on both fronts until we have some more clarity, but treating the store stuff as a more near-term tangible goal.
A question from the chatroom, from @mrkishi:
did you have any ideas on syntax for receiving attributes?
In the markup, that's not a problem:
<!-- these are both (hopefully!) easy to understand -->
<div>{propThatWasPassedIn} / {localState}</p>
It gets trickier with computed properties, and with effects, to adopt React's terminology.
Those could both be handled with functions β for example (alpha footage, not representative of final product):
<script>
import { compute, onupdate } from 'svelte/whatever';
let filter = null;
let filtered = compute(props => {
return filter
? props.items.filter(filter)
: props.items;
});
onupdate(props => {
console.log(props.items, filtered);
});
</script>
{#each filtered as item}
<p>{item.description}</p>
{/each}
It becomes slightly clearer there that those functions will re-run when props changes, perhaps.
If that computed contract/API is well-defined, the Functional Reactive crowd will be falling over themselves to bring their own favored implementations to use with Svelte.
Something I noted in chat β it'd be nice (from both implementation and documentation perspectives) if the only magic was around assignments to variables and properties. So perhaps computed values would be better attacked in this fashion:
import { observe } from 'svelte/whatever';
let value = 1;
let someComputedThing;
observe(['value'], () => {
someComputedThing = value * 2;
});
That way, setting value triggers a round of observers, which triggers the assignment to someComputedThing, which gets included with value in the list of changed properties that need to be considered during the subsequent render.
I don't love the string array argument, just throwing it out there for now.
I don't love the string array argument, just throwing it out there for now.
Since svelte doesn't have to be normal JS, could it be:
observe(value, () => {
someComputedThing = value * 2;
});
instead? The compiler could turn that into an array of strings or whatever it needs to and users could get a super-easy-to-follow API.
It's not impossible, and in fact I just took this screenshot of the React docs which talks about a similar idea:

Having said that I think it's preferable to contain the magic as far as possible, not least because it results in more opportunities for composition (e.g. you could call observe from a helper that was shared between modules, if the compiler didn't need to muck about with it).
I also think about how could we represent the public interface of a single component. Right now, Svelte's output is just a JS constructor. But seems it's not adaptive to the new approach.
Perhaps, we can just allow a component's script to be freer what and how to export outside:
<script>
let count = 0;
export function getCount() {
return count;
}
export function setCount(val) {
count = val;
}
</script>
<button on:click="--count">-</button>
<input bind:value="count">
<button on:click="++count">+</button>
import { getCount, setCount } from './Counter.html';
getCount(); // 0
setCount(1); // 1
I really like this syntax
````
````
The syntax for computed value/property is so simple and elegant. I don't know why people are confused, Its very clear that double is dependent on count.
I don't love the string array argument, just throwing it out there for now.
We can have both string array argument and variable syntax for observe.
Exporting functions that way is definitely a possibility. I was thinking, though, that maybe in this new world components wouldn't have methods, which are intrinsically tied to a concept of 'classes' which doesn't quite gel with the approach. (With my implementer's hat on, it's also hard to imagine what the compiler would transform that to β the scoping gets a little tricky.)
Someone (I think @mrkishi) made a really neat suggestion yesterday, which is that the export keyword could be used to indicate which values could be set from outside β i.e. 'props'. I really like the look of this:
<script>
export let foo = 'default foo value';
export let bar = 'default bar value';
</script>
<p>{foo} {bar}</p>
Under the hood it might compile to something like this:
function create_main_fragment(component, ctx) {
// ...
}
const Component = createComponent((__update, __props) => {
let foo = 'default foo value';
let bar = 'default bar value';
__props(({ changed, current }) => {
foo = current.foo;
bar = current.bar;
__update(changed, { foo, bar });
});
}, create_main_fragment);
Private state that was derived from props could be done in the same way we were talking about handling computed properties in Discord yesterday β as thunks:
<script>
export let foo = 'default foo value';
export let bar = 'default bar value';
let baz = () => foo + bar;
</script>
<!-- baz() will never be rendered with the default values -->
<p>{foo} {bar} {baz()}</p>
(If necessary the compiler could memoize these thunks as appropriate, by statically analysing the dependency graph.)
Using exports as data props seems fine to me, I do like the explicitness of saying "these are my template vars".
I'm really not sold on computed coming from assignment though, or anything where a simple assignment is transformed into a function. I don't mind the compiler being smart and doing smart things but when it fundamentally changes behavior I start to get a little suspicious/spooked.
@rich-harris should baz also be exported in the example? It seems weird to export data props but have computed values implicitly available to the template.
@tivac Seems, only things what we want to be available outside of the component should be exported. All variables and functions defined in the script always available in the template from the beginning.
@Rich-Harris
Are we'll be able to export 'props' values already after declaration?
<script>
let foo = 'default foo value';
let bar = 'default bar value';
...
export foo;
export bar;
</script>
<p>{foo} {bar}</p>
exported things being changeable from the outside would seem weird - with ES modules, those exported things aren't re-assignable.
I'm really not sold on computed coming from assignment though
That's what the thunk idea avoids β no compiler magic involved there (except the dependency tracking part used to avoid unnecessary re-rendering).
should
bazalso be exported in the example?
No β the distinction is between exported public values and non-exported private values β foo, bar and baz are all available to the template, but only foo and bar are part of the component's contract with the outside world
Are we'll be able to export 'props' values already after declaration?
Yeah, no reason why not β though the syntax would be export { foo, bar }.
exported things being changeable from the outside would seem weird - with ES modules, those exported things aren't re-assignable.
This did occur to me. But we're already abusing export default as a way to make it unambiguous which object is the component definition, rather than to export the things on that definition β the actual default export is something completely different, and no-one really objects to it. I think this is the same thing β it's an abuse of syntax, but for a reason. As long as people have some idea of what the compiled code looks like (and I think we can do a better job of communicating that), I reckon it'd be accepted.
I mentioned this in chat but I want to have it here too:
I'm really not sure how user-defined, publicly-accessible methods fit in to all this. Would they just be const exports that happen to be functions? (This seems like it would clash with thunks-as-computeds.) Or maybe they could be old-fashioned function exports? Actually, that probably makes more sense, as arrow functions as methods won't really work, because of the this stuff.
edit: Okay, I missed that above it was suggested that methods be done away with, but I am really not comfortable with that.
edit again: There isn't really a this to worry about though anyway, maybe. As everything is just local variables. I dunno.
No β the distinction is between exported public values and non-exported private values β
foo,barandbazare all available to the template, but onlyfooandbarare part of the component's contract with the outside world
Just to make sure I'm following;
<script>
// Any declared variable is accessible in the template function,
// by default they are not able to be set from outside the
// component's <script> block
let foo = 'default foo value';
var bar = 'default bar value';
const faz = 'default faz value';
// Exporting variables makes them settable from outside a component
export { foo, bar };
// Computed functions are denoted by... ?
// The fact that they depend on a tracked variable?
let baz = () => foo + bar;
</script>
<!-- baz() will never be rendered with the default values -->
<!--
baz() is memoized somehow, maybe even rewritten so it's not a
function invocation if the dependencies haven't changed?)
-->
<p>{foo} {bar} {baz()} {faz}</p>
An idea from @mrkishi - Does it really matter whether we can distinguish computed-property-thunks from methods?
Maybe not?
My thinking is that 'computed properties' no longer exist as a separate Svelte-specific concept β instead you Just Write JavaScript and the compiler wires up the annoying bits.
In other words, with...
const baz = () => foo + bar;
...foo and bar could be private state, they could be props, they could even be imports. So nothing 'denotes' computed functions, they just... are.
When I talk about memoization, what I really mean is that <p>{foo} {bar} {baz()}</p> could result in the following update code, without mucking around with the definition of baz at all:
function update(changed, ctx) {
if (changed.foo) text1.data = ctx.foo;
if (changed.bar) text2.data = ctx.bar;
if (changed.foo || changed.bar) text3.data = ctx.baz();
}
The if statement 'is' the memoization. (It's not, of course β we're not memoizing anything, and nor should we since memoization isn't free. It's just to say that baz won't get called unless foo or bar changed since the last render. You could do your own memoization if that was appropriate, e.g. if baz was called multiple times in a single render.)
I agree that we probably do want some way to attach methods to a component for the sake of standalone widgets β not sure what that would be yet β I just don't think that's necessarily the thing to optimise for.
The if()-protected version of the re-render for the function is what I was getting at with
baz() is memoized somehow, maybe even rewritten so it's not a function invocation if the dependencies haven't changed?)
but didn't express clearly. Glad we're on the same page there!
exported things being changeable from the outside would seem weird - with ES modules, those exported things aren't re-assignable.
I mentioned this in the #future forum, but if we can make the JS aspect of this the least magical, I see a long play in this all with trying to motivate other frameworks to adopt ECMAx (not JSX) standard, i.e., something so similar to JS, but adding just enough value to standardize across frameworks, kind of like the play toward HTMLx. Not to derail the above, but it's a thought...
Methods/actions and refs both have a place. If we decide to use expressions for computed values then we can use arrow functions for private/public methods/actions. The compiler should insert refs into the component scope. Here is my component setup.
````
````
````
import { Store } from 'svelte';
import router from 'somewhere'
const app = new App({
target: document.querySelector('main'),
props: {count : 3 },
context: {
store: new Store({pageTitle: 'Home'}),
router: router
}
});
````
Most helpful comment
Let me quickly vomit another half-formed thought onto the screen:
We're a compiler. That means we don't need to play by everyone else's rules. So we could do something like this...
That
bar += 1could be augmented thusly:Every time a variable is assigned to, the compiler could trigger a re-render with knowledge of what changed. This would also work inside the
<script>tag:This is composable β you just need to use a closure: