Filtered by JavaScript

Reset

Adding client-to-server sync to PissueTracker

March 20, 2025
0 comments React, Bun, JavaScript

Last week I started a side project called PissueTracker which is a web app that lets you create issues/bugs. What's cool about it is that it uses an offline database, namely IndexedDB in the browser. That means that every time you create or edit an issue, it's stored in a database in your browser. The problem with this is that inevitably, you might reset your entire browser. Or, when auth is working, you might want to access your data from an entirely different browser. So I'm adding a client-to-server sync. (It'll become two-way sync later)

How it works is demonstrated with this code:


db.issues
  .update(issue.id, { state: newState, syncedAt: null })
  .then((updated) => {
    if (updated) {
      notifications.show(...);
      mutate(issue.id);

The payload sent to the .update() is first the ID, and then it's an object. No matter what the payload changes are, state: newState in this case, there's always syncedAt: null tacked on.
Then, after it has successfully updated the local database, it triggers that mutate function. Let's dig into that next.

The mutate function is based on the useMutation hook in TanStack Query which is a wrapper for making a fetch XHR POST request. It looks like this:


const issue = await db.issues.get({ id });
const response = await upfetch(`/api/sync/issue/${tracker.uuid}`, {
  method: "POST",
  body: { id, issue },
});
if (response.ok) {
  db.issues.update(id, { syncedAt: new Date() });
  return response.json();
}
throw new Error("Failed to sync issue");

Get it?
First, we set syncedAt: null into the local browser database.
Then, we send the issue object to the server. Once that's done, we update the browser database, again, but this time set syncedAt to a valid Date.

Once all of this is done, you have a copy of the same issue payload in the server and in the client. The only thing that is different is that the syncedAt is null on the server.

The server is an Hono server that uses PostgreSQL. All records are stored as JSONB in PostgreSQL. That means we don't need to update the columns in PostgreSQL if the columns change in the Dexie.js definition.

UPDATE

This idea is flawed. The problem is the use of incrementing integer IDs.
Suppose you have 2 distinct clients. I.e. two different people. One on a train (in a tunnel) and one on an airplane. If they both create a new row, they'd be sending:

id: 2
title: "Trains are noisy"
body: ...

and

id: 2
title: "Airplanes are loud"
body: ...

That would override each other's data when synchronized with the central server, later.

The only solution is UUIDs.

Starting a side project: PissueTracker

March 16, 2025
2 comments React, JavaScript

I've started a new side project called PissueTracker. It's an issue tracker, but adding the "P" because my name is "Peter" and it makes the name a bit more unique. Also, the name isn't important. What this project can do is important.

In a sense, it's a competitor to GitHub Issues. An issue can be anything such as a feature or bug report or just something that needs to be addressed. In another sense, it's a competitor to a web product I built 24 years ago called IssueTrackerProduct. That was a huge success and it was so much fun to build.

What's special about this issue tracker is that it's all about offline data, with server sync. All the issues you create and edit are stored in your browser. And if you don't want to lose all your data if/when your browser resets, you can have all that data constantly synchronized to the server which will either store it in PostgreSQL or the file system of a server. Since there's a shared data store, you can also authorize yourself amongst others in the system and this way it can become a collaborative experience.

I'm building it by using a React framework called Mantine, which I'm a huge fan of. The browser storage is done with Dexie.js which uses IndexedDB and allows you to store data in relational ways.

So far, I've only spent 1 weekend day on it but I now have a foundation. It doesn't support comments or auth, yet. And I haven't even started on the server sync. But it supports creating multiple trackers, creating/editing issues, fast filtering, and safe Markdown rendering.

The front end is handled by Vite, React Router, and Bun, I don't know if that's important. Right now, the most important thing is to have fun and build something that feels intuitive and so fast that it feels instantly responsive.

More to come!

Listing issues
List of issues in a tracker

Viewing an issue
Viewing an issue

UPDATE (Mar 20, 2025)

Blogged about the client-to-server sync here: "Adding client-to-server sync to PissueTracker."

Announcing: Spot the Difference

February 23, 2025
0 comments React, Bun, JavaScript

Spot the Difference is a web app where you're shown two snippets of code (programming, config files, style sheets, etc) and you're supposed to find the one difference. If you get it right, you get showered in confetti.

This started as an excuse to try out some relevant patterns in React that I was working on. I was also curious about writing a SPA (Single Page App) using Bun and React Router v7 (as a library). It started as a quick prototype, with no intention to keep it. But then I found it was quite fun to play and I liked the foundation of the prototype code; so I decided to clean it up and keep it. After all, it's free.

There's a goose on the home page because when I first showed my son the prototype, I said: "It's a silly goose game. Wanna try it?". So it stuck.

Game play in light mode

The technology behind it

Truncated! Read the rest by clicking the link below.

get in JavaScript is the same as property in Python

February 13, 2025
0 comments Python, JavaScript

Almost embarrassing that I didn't realize that it's the same thing!


class MyClass {
  get something() {
  }
}

is the same as Python's property decorator:


class MyClass
    @property
    def something:

They both make an attribute on a class automatically "callable".
These two are doing the same functionality:


class Foo {
  get greeting() {
    return "hello";
  }
  place() {
    return "world";
  }
}

const f = new Foo();
console.log(f.greeting, f.place());
// prints 'hello world'

and


class Foo:
    @property
    def greeting(self):
        return "hello"

    def place(self):
        return "world"

f = Foo()
print(f.greeting, f.place())
# prints 'hello word'

How to send custom headers in a loader in react-router v7

February 7, 2025
0 comments React, JavaScript

tl;dr; Use data() in your loader function and make your headers function pass it on to get headers be dependent on what's happening in the loader function.

I recently rewrote the front end of this website from Remix to react-router v7. A route is a page, which can be something like /about or have a parameter in it like /blog/:slug.

The way react-router v7 (the "framework mode") works is that your route looks like this:


import type { Route } from "./+types/post"

export async function loader({ params }: Route.LoaderArgs) {
  const post = await fetchPost(params.slug)
  return { post }
}

export default function Component({loaderData}: Route.ComponentProps) {
  return <h1>{loaderData.post.title}</h1>
}

So good for so far. But suppose you want this page to have a certain header, depending on the value of the post object. To set headers, you have to add an exported function called, surprise surprise; headers. For example:

Truncated! Read the rest by clicking the link below.

TypeScript enums without enums

January 29, 2025
0 comments JavaScript, TypeScript

My colleague @mattcosta7 demonstrated something that feels obvious in hindsight.

Instead of enums, use a regular object. So instead of


enum State {
  SOLID,
  LIQUID,
  GAS,
}

(playground here - note how "complex" the transpiled JavaScript becomes)

...use an object. Objects have the advantage that when the TypeScript is converted to JavaScript, it looks pretty much identical. TypeScript 5.8 makes it possible to disallow "non-erasable syntax" which means you can set up your tsconfig.json to avoid enum.

The alternative is an object. It's a bit more verbose but it has advantages:


const State = {
  SOLID: "solid",
  LIQUID: "liquid",
  GAS: "gas"
} as const

type State = typeof State[keyof typeof State]

(playground here - note how simple the transpiled JavaScript is)

In the above code, if you hover the mouse over State it'll say

'solid' | 'liquid' | 'gas'

Truncated! Read the rest by clicking the link below.

Run TypeScript in Node without extensions

December 10, 2024
0 comments Node, JavaScript

A couple of months ago I wrote about "Node watch mode and TypeScript" which suggests using the @swc-node/register package to be able to run node on .ts file without first converting it to .js using tsc.

In Node 22, you don't even need @swc-node/register. You can use --experimental-strip-types instead. (Note the prefix "experimental"). See release notes here.

Imagine a script like this:


// example.ts

function c2f(c: number) {
  return (c * 9) / 5 + 32;
}
console.log(c2f(123));

you can execute it directly in Node v22 using:


❯ node --experimental-strip-types --no-warnings example.ts
253.4

Truncated! Read the rest by clicking the link below.

An ideal pattern to combine React Router with TanStack Query

November 18, 2024
1 comment React, JavaScript

I'm writing this blog post from an admin interface I built, which is a web app frontend for the backend behind peterbe.com. It's built as a single-page app in Vite, with React.
Vite, unlike frameworks like Remix or Next, doesn't come with its own routing. You have to add that yourself. I added React Router. Another thing you have to do yourself is a way to load remote data into the app for display and for manipulation. This is done as XHR requests happen on the client side. For that, I chose TanStack Query. If you haven't used it but used React, it's sugar for this type of code:


// DON'T DO THIS. USE TANSTACK QUERY

const [stuff, setStuff] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
  fetch('/api/some/thing')
    .then(r => r.json())
    .then(data => setStuff(data)
    .catch(err => setError(err))
}, [])

Truncated! Read the rest by clicking the link below.

Object.keys to return the known strings of an object in TypeScript

October 25, 2024
0 comments JavaScript

This one always confused me but by blogging about it, hopefully, it will stick.

This does NOT work in TypeScript:


const person = {
  firstName: "peter",
  lastName: "bengtsson",
  state: "happy"
}

const keys = Object.keys(person)
const randomKey = keys[Math.floor(Math.random() * keys.length)];
const value = person[randomKey]

It works in JavaScript, but in TypeScript you get an error on the last line (from the person[randomKey]):

Truncated! Read the rest by clicking the link below.

How I make my Vite dev server experience faster

October 22, 2024
0 comments React, Node, JavaScript

I have a web app that operates as a SPA (Single Page App). It's built on regular Vite + React + TypeScript. In production you just host the built static assets, but in development (for hot module reloading and stuff) I use the built-in dev server in Vite. This is what you get when you type vite without any other command line arguments.

But here's what happens, you're in the terminal and you type npm run dev and hit Enter. Then, using your trackpad or keyboard shortcuts you switch over to a browser tab and go to http://localhost:5173/. When you get there to the first request, Vite starts at the entry point, which is src/main.tsx and from there, it looks at its imports and starts transpiling the files needed. You can see what happens with vite --debug transform.

With debug:

Truncated! Read the rest by clicking the link below.

Previous page
Next page