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.

Use 'key' in React components to reset them

February 12, 2025
0 comments React

I'm sure you've seen this React code:


<ul>
  {users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>

The key prop is necessary so each element is identifiable.

But did you know you can pass key to a component even though there's no .map(...) or other array of React elements.

Consider this simplified app:

Truncated! Read the rest by clicking the link below.

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.

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.

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.

The performance benefits of code-split an SPA

October 12, 2024
0 comments React

This isn't a comprehensive in-depth analysis but I have this SPA which is built with Vite + React.
When you run npm run build it produces:

vite v5.4.8 building for production...
✓ 8210 modules transformed.
dist/index.html                             0.76 kB │ gzip:   0.43 kB
dist/assets/images-xTxpPavl.css             2.02 kB │ gzip:   0.55 kB
dist/assets/index-IHK6QBxo.css            200.12 kB │ gzip:  29.85 kB
dist/assets/index-jgmGYYS9.js               0.79 kB │ gzip:   0.51 kB
dist/assets/open-graph-image-Ca6hLYnz.js    1.47 kB │ gzip:   0.82 kB
dist/assets/images-CwbhV2EW.js             28.75 kB │ gzip:  10.37 kB
dist/assets/pageviews-C6NSq649.js         378.67 kB │ gzip: 106.42 kB
dist/assets/index-HpyQl1NK.js             490.15 kB │ gzip: 154.11 kB
✓ built in 4.46s

Truncated! Read the rest by clicking the link below.

How to handle success and failure in @tanstack/react-query useQuery hook

September 16, 2024
0 comments React, JavaScript

What @tanstack/react-query is is a fancy way of fetching data, on the client, in a React app.

Simplified primer by example; instead of...


function MyComponent() {
  const [userInfo, setUserInfo] = useState(null)
  useEffect(() => {
    fetch('/api/user/info')
    .then(response => response.json())
    .then(data => {
      setUserInfo(data)
    })
  }, [])

  return <div>Username: {userInfo ? userInfo.user_name : <em>not yet known</em>}</div>
}

Truncated! Read the rest by clicking the link below.

Wouter + Vite is the new create-react-app, and I love it

August 16, 2024
0 comments React, Node, Bun

If you've done React for a while, you most likely remember Create React App. It was/is a prepared config that combines React with webpack, and eslint. Essentially, you get immediate access to making apps with React in a local dev server and it produces a complete build artefact that you can upload to a web server and host your SPA (Single Page App). I loved it and blogged much about it in distant past.

The create-react-app project died, and what came onto the scene was tools that solved React rendering configs with SSR (Server Side Rendering). In particular, we now have great frameworks like Gatsby, Next.js, Remix, and Astro. They're great, especially if you want to use server-side rendering with code-splitting by route and that sweet TypeScript integration between your server (fs, databases, secrets) and your rendering components.

However, I still think there is a place for a super light and simple SPA tool that only adds routing, hot module reloading, and build artefacts. For that, I love Vite + Wouter. At least for now :)
What's so great about it? Speed

Truncated! Read the rest by clicking the link below.

Previous page
Next page