-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
A completely different approach to defining components #1826
Comments
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 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 <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:
|
A couple of other things that occurred to me on the subway:
God 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 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 |
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:
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 |
If that |
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 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 |
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.
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 <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 |
@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 <script>
let foo = 'default foo value';
let bar = 'default bar value';
...
export foo;
export bar;
</script>
<p>{foo} {bar}</p> |
|
That's what the thunk idea avoids — no compiler magic involved there (except the dependency tracking part used to avoid unnecessary re-rendering).
No — the distinction is between exported public values and non-exported private values —
Yeah, no reason why not — though the syntax would be
This did occur to me. But we're already abusing |
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 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 |
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; ... When I talk about memoization, what I really mean is that 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 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
but didn't express clearly. Glad we're on the same page there! |
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. App.html
main.js
|
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......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>
: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 makebar
reactive: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 ofset({ 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 inuseComputed
(React has useMemo for a similar purpose) and the equivalent ofuseEffect
oncreate
,ondestroy
etc, particularly when there are bindings or immediateset(...)
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).
The text was updated successfully, but these errors were encountered: