DEV Community

Cover image for Web Frameworks: The Future
João Vitor
João Vitor

Posted on • Edited on

1 1

Web Frameworks: The Future

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  `;
});
Enter fullscreen mode Exit fullscreen mode

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>
  `;
});
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  `;
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

🤯 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)

Collapse
 
ryansolid profile image
Ryan Carniato

I like mental journeys like this. A few thoughts:

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. 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.

Collapse
 
jvzaniolo profile image
João Vitor • Edited

@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.

// Pre-compiled output
async function Counter() {
  const initialValue = await db.getCount() // 0

  return `
    <script>
      let count = $state(${initialValue})
      function increment() { count++ }
    </script>
    <p>Count is {count}</p>
    <button onclick={increment}>Increment</button>
    <style>
      p { font-family: 'Comic Sans MS', cursive; }
    </style>
  `
}

// Compiled Output
async function Counter() {
  const initialValue = await db.getCount() // 0

  return `
    <!-- Initial HTML/CSS -->
    <p class="some-h4$h">Count is ${JSON.stringify(initialValue)}</p>
    <button>Increment</button>
    <style>
      p.some-h4$h { font-family: 'Comic Sans MS', cursive; }
    </style>
    <script>
      // Hydration part once the browser is ready
      const p = document.querySelector('p')
      const button = document.querySelector('button')

      let count = ${JSON.stringify(initialValue)}

      button.onclick = function increment() {
        count++
        p.textContent = 'Count is ' + count
      }  
    </script>
  `
}
Enter fullscreen mode Exit fullscreen mode

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!

Visualizing Promises and Async/Await 🤯

async await

Learn the ins and outs of Promises and Async/Await!

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay