DEV Community

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

Posted on

Web Frameworks: The Future

I was watching a stream the other day when the host 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 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 from 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

I hope that you understand that this is not a serious post and that I'm just joking around with my thoughts, but I do believe that it's always a best practice to take a step back, think about everything as a whole, and ask questions like "How would I like to write this code?".

What about you? Have you thought about how frameworks would look like in ten years from now? Please write it down!

*Cover image by Yolanda Suen on Unsplash.

Top comments (0)