I was watching Ryan Carniato's stream the other day when he said something like "When I imagine building a website in 10 years, React will not be the thing I'll be using...", and that got me thinking "How would the next framework look like?".
So I started playing around with some ideas and decided to write them down.
Syntax
Let's start with a hot one: the syntax.
If you have experience writing HTML, making a real website by sending HTML through the server, playing around CodePen, or tweaking your Tumblr page with some fancy cursor styles (that's how I got into coding), here's how you would write code:
<!DOCTYPE html>
<html>
<!-- ... -->
<body>
<h1>Counter</h1>
<p>Count is 0</p>
<button onclick="increment()">Increment</button>
<style>
/* Inside the style tag is where you make everything look pretty */
h1 {
color: red;
font-family: 'Comic Sans MS', cursive;
}
</style>
<script>
// And here is where you make things work
const p = document.querySelector('p');
let count = 0;
function increment() {
count++;
p.textContent = `Count is ${count}`;
}
</script>
</body>
</html>
HTML based
I like the idea behind Svelte to enhance HTML and if you compare it to the HTML code you'll see that things look pretty similar:
<!-- Component.svelte -->
<script>
let count = $state(0);
function increment() {
count++;
}
</script>
<h1>Counter</h1>
<p>Count is {count}</p>
<button onclick={increment}>Increment</button>
<style>
h1 {
color: red;
font-family: 'Comic Sans MS', cursive;
}
</style>
Everything stays where it's supposed to be, markup in the HTML, CSS in the <style>
tag, and JavaScript in the <script>
tag.
Writing it feels like writing modern HTML with components.
But if you're trying to build a website you'll need more than that. We need servers!
JSX
When you navigate to any website, the browser makes a request to the computer that's hosting your website and expects it to respond with assets (HTML, CSS, and JavaScript) to render the page for you.
The server is where developers connect with databases to fetch or mutate data, check the user’s authentication, permissions, etc., and then return things to the browser.
This is how a web server might look like in reality:
app.get('/', async (req, res) => {
const user = await db.getUser(req.body);
if (!user.isAuthenticated) return res.status(401);
return res.html`
<html lang="en">
<head><title>My website</title></head>
<body>
<h1>Hello ${user.name}</h1>
</body>
</html>
`;
});
When I see it like that, it makes more sense in my head to write pages with JSX.
It looks a lot like a React component!
But now we are at the other side of the fence, where everything is on the server. We still want JavaScript on the client for all sorts of good stuff, like optimistic updates, client-side routing, etc.
I choose both
Let's take a closer look at the API endpoint above and identify which code runs where:
app.get('/', async (req, res) => {
// Up here we are on the server
// We can connect to the database, authenticate the user, etc.
return res.html`
<html>
<!-- Down here, we're on the client -->
<!-- We can send HTML, CSS, and JavaScript for it to render the page -->
<style />
<div />
<script />
</html>
`;
});
As you can see, we can use the <script>
tag to send JavaScript code to the client. And the structure of the returned HTML string looks like the code we wrote for the Svelte component!
Here's what I'm thinking:
export async function ProfilePage() {
// Code here only runs on the server
const user = await getSession();
if (!user) throw redirect('/login');
return `
<>
<div>
<img src="${user.profileUrl}" alt="${user.name}" />
<h1>Hi ${user.name}</h1>
</div>
<style>
h1 {
font-family: 'Comic Sans MS', cursive;
}
</style>
</>
`;
}
Think of it as if React was rendering a Svelte component.
But of course, we don't want to write in template strings anymore, so we'll use JSX!
export async function ProfilePage() {
// Code here only runs on the server
const user = await getSession();
if (!user) throw redirect('/login');
return (
<>
<div>
<img src={user.profileUrl} alt={user.name} />
<h1>Hi {user.name}</h1>
</div>
<style>
h1 {
font-family: 'Comic Sans MS', cursive;
}
</style>
</>
);
}
Reactivity
Since we are talking about a framework for the next decade, I imagine that a lightweight and fast reactivity system will be a must-have.
Signals to the rescue
Today, the only implementation that meets those requirements is Signals.
Svelte runes are my favorite implementation so far, so to illustrate where things would go:
export function Counter() {
// Code up here runs on the server
return (
// Code down here runs on the browser
<>
<script>
let count = $state(0);
$effect(() => console.log(count));
function increment() {
count++;
}
</script>
<div>
<h1>Count is {count}</h1>
<button onclick={increment}>Increment</button>
</div>
<style>
h1 {
font-family: 'Comic Sans MS', cursive;
}
</style>
</>
)
}
Our code is looking awesome in my opinion, but there are a couple more pieces to this puzzle!
Data fetching
For the past few years, I've been experimenting with different React frameworks and the one I enjoyed the most was Remix v2.
Although it didn't have the same features as Next.js, such as caching and middleware, their implementation was closer to how the browser works and easier to understand.
Loaders and Actions
// routes/_index.tsx
export const loader = async () => {
return await db.getCount();
};
export const action = async (req) => {
let data = await req.formData();
await db.updateCount(+data.get('count'));
return null;
};
export default function Root() {
let initialValue = useLoaderData();
let [count, setCount] = useState(initialValue);
function update(event) {
setCount(event.target.value);
}
return (
<form method="POST">
<p>Count is {count}</p>
<input type="range" name="count" value={count} onChange={update} />
<button>Save</button>
</form>
);
}
When the browser makes a request for the route /
, Remix will run the loader
function to get the initial data and send back the rendered JSX to the browser.
React then hydrates that component, binding event listeners with JavaScript to add interactivity, such as the onChange
and the update
function.
When the user submits the form, the browser makes a “POST” request to that same route /
which Remix will run the action
function assigned to that module, invalidating loaders and keeping everything up-to-date.
It behaves similarly to how the browser works with progressive enhancement, AND you can mix and match server and client code on the same file!
React Server Components
Another approach that came into life was React Server Components.
It uses JavaScript directives to tell frameworks which code will execute on the server and which will run on the client.
The main difference is that it sends less JavaScript to the client when compared to loaders and actions.
// page.jsx
import { revalidatePath } from 'next';
async function updateCount(formData) {
'use server';
await db.updateCount(+formData.get('count'));
revalidatePath('/');
}
export default async function Page() {
let count = await db.getCount();
return <Counter initialValue={count} updateAction={updateCount} />;
}
// counter.jsx
'use client';
export function Counter({ initialValue, updateAction }) {
let [count, setCount] = useState(initialValue);
function update(event) {
setCount(event.target.value);
}
return (
<form action={updateCount}>
<p>Count is {count}</p>
<input type="range" name="count" value={count} onChange={update} />
<button>Save</button>
</form>
);
}
Notice that with RSCs, I had to move the client-side logic to another file marked as "use client"
and mark the updateCount()
function with "use server"
directive.
Functions marked with "use server"
are called Server Functions. They are exposed to the network as a Remote Procedure Call (RPC).
I pick none
For my personal preference, I like the ergonomics that RSC allows, but I wish we didn't have to write directives or even separate client and server code on different files.
What would it look like to fetch and mutate data from our example of React + Svelte code?
Well, we already know where we can run code on the server!
export async function FullStack() {
// Fetch data up here
let initialValue = await db.getCount();
async function updateCount(formData: FormData) {
let count = +formData.get('count');
await db.updateCount(count);
}
return (
<>
<script>
let count = $state(initialValue);
$effect(() => console.log(count));
function update(e) {
count = e.target.value;
}
</script>
<form action={updateCount}>
<p>Count is {count}</p>
<input type="range" name="count" value={count} oninput={update} />
<button>Save</button>
</form>
<style>
p {
font-family: 'Comic Sans MS', cursive;
}
</style>
</>
)
}
Now you're probably wondering if this guy is insane or if he's up to something.
How would the framework know that updateCount
needs to be transformed into an RPC call, like the Server Function?
I thought that looking at the <form action={} />
would've been enough but maybe not, and that's where the last piece comes in place.
The language
When thinking about how to make the updateCount
function special without using directives, I remembered something about React, the static type checker for JavaScript used by Meta, Flow, and it gave me some ideas.
In 2024, Flow introduced a Component syntax feature to help developers write simpler React code. They added two keywords to the language component
and hook
to create components and hooks with some pre-configured behavior.
If we mark an async function with an action
keyword we would know that it needs to be transformed into an RPC call:
export async component Counter() {
let initialValue = await db.getCount();
async action updateCount(formData: FormData) {
let count = +formData.get('count');
await db.updateCount(count);
}
return (
<>
<script>
let count = $state(initialValue);
function update(e) {
count = e.target.value;
}
</script>
<form action={updateCount}>
<p>Count is {count}</p>
<input type="range" name="count" value={count} oninput={update} />
<button>Save</button>
</form>
<style>
p {
font-family: 'Comic Sans MS', cursive;
}
</style>
</>
)
}
🤯 Wow!! Now I think we've done it! We just envisioned the future of the web 🥳
We're combining all the benefits of Svelte and React libraries. For example:
When compared to React
- Faster and lightweight reactivity with Signals
- Smaller JS footprint on the client with compilers
- Scoped CSS and no CSS-in-JS syntax needed
When compared to Svelte
- Named exports
export function MyComponent() {}
- Better TypeScript support with no type generation
- Better template syntax with
Array.map()
in favor of{#each}{/each}
and others (arguably)
Conclusion
Please don't take this post seriously, I was playing around with my thoughts 😅
But it's always best to take a step back, consider everything, and ask questions like "How would I like to write this code?"
What about you? Have you considered what frameworks would look like in ten years? Please write it down!
*Cover image by Yolanda Suen on Unsplash.
Top comments (2)
I like mental journeys like this. A few thoughts:
JSX presented here isn't quite JSX. Like Style/Script Tags can't be authored that way. Their content would need to be escaped as strings. This could be solved with a custom parser. So I think its best to think of this as JSX-like.
Style tags working this way in a JSX-like framework has consequences. Vercel built a library like this called styled-jsx but comparatively it hasn't really come out as the winner. Part of it is when components become easier to break apart where styles apply and don't gets trickier. I do think having a
component
decorator like proposed does help from analysis standpoint to do something more sensible.The different between
action
and"use server"
is more or less a syntax decision and doesn't necessarily need to define different behavior. Other options include function decorators, comments, function wrappers. Downside of special keywords is need for a special parser. There is a different between hijacking valid JS and adding syntax which it chokes on. Just something to consider in terms of maintenance of language level tools (TypeScript, Linters, Code Highlighters, Code Completion).Composition is a question that needs to be answered. How to manage server components/client components within each other. Is it safe to assume all JSX is client components? Can you compose on the server? Can component logic be either or depending where it is used? If one was to take this idea forward this is where I'd put my focus on coming up for clear examples of intending how this could work.
None of this suggests you can use
.map
instead of{#each}
. Lack of JSX isn't what is limiting these Signal solutions to special control flow. It's the desire that when one part of an array updates you don't recreate the whole thing. When you run.map
you re-run every row. If it's VDOM it's a bit of who cares, but that isn't Svelte 5. If that is what you are doing Signals are an additional overhead. If you want to leverage Signals you will need to replace{#each}
with<For>
(see SolidJS) or equivalent.Overall I think this is an interesting thought experiment. Syntax details aside you are proposing a split execution model similar to RSCs/Islands, but where the code is co-located in the same file. Co-locating server function RPCs is pretty trivial because they don't compose/weave but for the UI itself it can be a bit more involved.
@ryansolid First of all, I can't believe this got your attention! I'm a huge fan, and I appreciate your taking the time to read and respond.
My idea was that each component would return its templating, styles, and interactivity just like a server endpoint but more granularly.
Of course, no one would like to write template strings again, that's why I thought something like JSX could do the trick, but I guess a
tagFunction()
could serialize the arguments and send that to a compiler. Syntax highlighting and intelli-sense would be a matter of a VSCode extension of some sort.After that, everything became a mess and I did not think about composability or control flow at all!
Anyway, thanks again for the chat!