DEV Community

rohit20001221
rohit20001221

Posted on

Building a Full-Stack Todo App with Esbuild, React & Golang

In this post, we explore how to build a full-stack to-do application using Go, React, Esbuild, and import maps—eliminating the need for traditional Node modules. Our setup enables an efficient workflow with server-side rendering, client-side routing, and seamless state management.


Backend: Go HTTP Server and Page Rendering

Our backend is a simple HTTP server built with Go. It serves static files, handles API requests, and dynamically renders pages using Esbuild.

Initializing the Server

The main.go file defines our HTTP server. It sets up a SQLite database for managing to-do items and builds necessary frontend files:

func init() {
    stdLibFiles, _ := filepath.Glob("ui/lib/*.jsx")
    api.Build(api.BuildOptions{
        EntryPoints:      stdLibFiles,
        Bundle:           true,
        Write:            true,
        Outdir:           ".web/lib",
        JSX:              api.JSXAutomatic,
        External:         []string{"react", "react-dom", "@mui/material", "@emotion/react", "@emotion/styled"},
        Format:           api.FormatESModule,
        MinifyWhitespace: true,
    })

    controller = *controllers.New(db.OpenConnection())

    if _, err := controller.DB.Exec(`
        CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT,
            description TEXT,
            is_completed BOOLEAN
        )
    `); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Serving Pages

We define routes for the home and about pages, leveraging the RenderPage function to dynamically inject data:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    defer controllers.HandleError(w)

    todos := controller.GetTodos()
    isCode := r.URL.Query().Get("isCode") == "true"
    content := controller.RenderPage("index", map[string]any{"todos": todos}, isCode)

    w.WriteHeader(http.StatusOK)
    w.Write(content)
})
Enter fullscreen mode Exit fullscreen mode

The RenderPage function compiles JSX files using Esbuild and injects context data:

func (c *Controller) RenderPage(page string, ctx map[string]any, isCode bool) []byte {
    tmpl, err := template.New("__WEB_ROOT__").Funcs(template.FuncMap{
        "marshal": marshal,
    }).Parse(HTML_PAGE)

    if err != nil {
        panic(err)
    }

    result := api.Build(api.BuildOptions{
        EntryPoints: []string{fmt.Sprintf("ui/pages/%s.jsx", page)},
        Bundle:      true,
        Write:       false,
        JSX:         api.JSXAutomatic,
        External:    []string{"react", "react-dom", "@lib", "@mui/material", "@emotion/react", "@emotion/styled"},
        Format:      api.FormatESModule,
    })

    codeBytes := result.OutputFiles[0].Contents
    code := b64.StdEncoding.EncodeToString(codeBytes)
    code = fmt.Sprintf("data:text/javascript;base64,%s", code)

    if isCode {
        return []byte(code)
    }

    buf := new(bytes.Buffer)
    args := map[string]any{
        "Page":    page,
        "Context": ctx,
        "Code":    code,
    }

    if err := tmpl.Execute(buf, args); err != nil {
        panic(err)
    }

    return buf.Bytes()
}

var HTML_PAGE = `<html>
    <head>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
            rel="stylesheet"
            href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
        />
    </head>
    <body>
        <div id="root"></div>

        <script id="__WEB_ROOT_DATA__" type="json">
            {{ if .Context }}
              {{ .Context | marshal }}
            {{ else }}
                {}
            {{ end }}
        </script>

        <script type="importmap">
            {
                "imports": {
                    "react": "https://esm.sh/react@18.2.0",
                    "react/": "https://esm.sh/react@18.2.0/",
                    "react-dom": "https://esm.sh/react-dom@18.2.0",
                    "react-dom/": "https://esm.sh/react-dom@18.2.0/",
                    "@lib/": "/_lib/",
                    "@mui/material": "https://esm.sh/@mui/material?external=react,react-dom,@emotion/react,@emotion/styles&exports=Box,Typography,TextField,Button,List,ListItem,ListItemText",
                    "@emotion/react": "https://esm.sh/@emotion/react?external=react,react-dom",
                    "@emotion/styled": "https://esm.sh/@emotion/styled?external=react,react-dom"
                }
            }
        </script>

        <script type="module">
            import ui from "{{ .Code }}";
            import { createElement } from "react";
            import { createRoot } from "react-dom/client";

            globalThis.____WEB_ROOT = createRoot(
                document.getElementById("root"),
            );

            globalThis.____WEB_ROOT.render(createElement(ui));

            window.addEventListener("popstate", async (e) => {
                const code = await (
                    await fetch(`${e.target.location.pathname}?isCode=true`)
                ).text();

                const m = await import(code);
                globalThis.____WEB_ROOT.render(createElement(ui));
            });
        </script>
    </body>
</html>
`
Enter fullscreen mode Exit fullscreen mode

Frontend: Import Maps, Client-Side Routing, and Context Management

Our frontend architecture follows a modular approach, using import maps to manage dependencies instead of Node modules.

Client-Side Navigation (link.js)

The Link component provides seamless navigation without full-page reloads:

import { createElement } from "react";

export const Link = ({ to, children }) => {
  return (
    <div
      onClick={async () => {
        const path = `${to}?isCode=true`;
        const code = await fetch(path);
        const m = await import(await code.text());

        history.pushState({}, "", to);
        const root = globalThis.____WEB_ROOT;
        root.render(createElement(m.default));
      }}
    >
      {children}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Managing Global State (context.js)

The context.js file extracts and provides server-injected data:

export const usePageContext = () => {
  return JSON.parse(document.getElementById("__WEB_ROOT_DATA__").text);
};
Enter fullscreen mode Exit fullscreen mode

Home Page (index.jsx)

The home page displays the list of to-do items retrieved from the server:

import { useState } from "react";
import { Link } from "@lib/link.js";
import { usePageContext } from "@lib/context.js";

import {
  Button,
  Typography,
  Box,
  TextField,
  List,
  ListItem,
  ListItemText,
} from "@mui/material";

const HomePage = () => {
  const [count, setCount] = useState(0);

  const data = usePageContext();

  return (
    <Box>
      <Box
        borderBottom={2}
        borderColor={(theme) => theme.palette.divider}
        p={1}
        display={"flex"}
      >
        <Box flex={1}>
          <Typography>Dashboard</Typography>
        </Box>
        <Link to="/about">
          <Button>About Page</Button>
        </Link>
      </Box>
      <Box p={4}>
        <Typography>Todo List</Typography>

        <form method="POST" action={"/createTodo"}>
          <Box display="flex" flexDirection="column" gap={2} p={2}>
            <TextField
              type="text"
              required={true}
              name="title"
              placeholder="Title"
              variant="outlined"
            />
            <TextField
              type="text"
              required={true}
              name="description"
              placeholder="Description"
              variant="outlined"
            />
            <Box>
              <Button type="submit" variant="contained">
                Create
              </Button>
            </Box>
          </Box>
        </form>
        <Box mt={2}>
          <List>
            {data.todos.map((todo) => (
              <ListItem key={todo.id}>
                <ListItemText
                  primary={todo.title}
                  secondary={todo.description}
                />
              </ListItem>
            ))}
          </List>
        </Box>
      </Box>
    </Box>
  );
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

About Page (about.jsx)

A simple about page:

import { Typography, Box, Button } from "@mui/material";
import { Link } from "@lib/link.js";

const AboutPage = () => {
  return (
    <Box>
      <Typography>About Page</Typography>
      <Link to="/">
        <Button>Home</Button>
      </Link>
    </Box>
  );
};

export default AboutPage;

Enter fullscreen mode Exit fullscreen mode

Conclusion

This article demonstrates how we can leverage import maps and esbuild to create a React-based full-stack application without relying on Node modules. By dynamically injecting server-side data into the frontend, we achieve a lightweight and efficient architecture. The approach outlined ensures seamless bundling, efficient routing, and a structured way to manage UI components while keeping dependencies minimal.

For the full code example, visit:
https://github.com/rohit20001221/go-esbuild-react-todo.git

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

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

Okay