Alvar Lagerlöf's Blog https://alvar.dev Developer and designer living in Stockholm Fri, 11 Oct 2024 09:20:35 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en-us <![CDATA[Safari's disadvantage is OS updates]]> https://alvar.dev/blog/safari-disadvantage-os-updates https://alvar.dev/blog/safari-disadvantage-os-updates Sun, 21 Apr 2024 00:00:00 GMT Sometimes I hear "Safari is the new IE" in discussions about support for web platform features. Often, this is said because Safari happens to be the one browser where feature x can't be used. In a practical sense, the feature may indeed even be "available" (and more and more features are). But that doesn't matter if support for it isn't good enough.

If we separate "available" and "widely supported", we can see more clearly see what's going on here.

Browser updates

Browsers tend to update in one of two ways:

- On their own, independently of things around them

- With the OS

Safari is the odd one out here. It updates with the OS. This means that new web platform features only come when update your OS. While Apple users tend to be quite fast upgrading to the latest version, they'll never be as fast as someone who doesn't even know that they've been upgraded to the latest version automatically.

The long tail of old iOS versions stretches on for a far longer time than that of any other browser. If often becomes the sigular thing we have to wait for when looking at caniuse.com. Upgrading your OS can be kind of tedious and time-consuming, so it makes perfect sense that some 5% will still be one a version that doesn't support that CSS feature you'd like to use. The percentage tends to be way too large to argue for not supporting. It seems like this is shifting the "widely supported" milestone by 1-2 years for every feature that comes out.

When the EU forces Apple to allow third-party browser engines, the biggest change will arguably be that it will now be possible to have an auto-updating browser that isn't tied to the OS version on iPhones.

Safari is only "The new IE" in the sense that the Safari that web developers can target is always significantly older than the latest available one.


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[Devtools for React Server Components]]> https://alvar.dev/blog/creating-devtools-for-react-server-components https://alvar.dev/blog/creating-devtools-for-react-server-components Tue, 08 Aug 2023 00:00:00 GMT Update: The extension has now been published and is available here. I recommend continuing to read if you're interested in how React Server components work.

I've been interested in the potential of React Server Components (often referred to as RSC) ever since the concept was first presented in December of 2020.

There's much you can speak to about benefits and drawbacks, but the core capability that interests me is the ability for the server stream content as it loads and renders on the server.

Now, developers can define areas in their sites where content may take longer to load. While its loading, a fallback can be shown. These areas are called <Suspense> boundaries. They allow you to unblock the rest of the page while they're still loading. And the loading part can now happen server-side. Previously, everything we've rendered on the server was blocking the initial page load. But now, the server can stream rendered content to client as it becomes ready.

Recently, this is all starting to come together and be useable in production. In this article we'll first explore the how React Server Components are represented over the network and then how I'm using this to create devtools.

The lack of devtools for RSC

Unsurprisingly, there isn't much tooling for React Server Components yet. Everyone is working hard on creating frameworks, creating libraries with new capabilities, and thinking about wether or how they could use RSC in their sites and apps.

The good thing though is that there is quite a bit of data to work with. Arguably much more than we've ever had before.


There's this format that RSC uses when streaming content to a page. It's been referred to as the "wire format" sometimes. It's changed since 2020, and will likely change again, but this is an internal spec managed by React, so you don't need to care about it while developing apps. However, I've found the data inside of it quite interesting.

This is an example of the format, as seen from the /blog route on this site. This type of response will only be seen on subsequent navigations. If you were to load /blog as your initial load, you'd only get a html response.

You may notice that the scroll bars at the bottom of the screenshot. Some of the lines are really long. This is problematic if you're trying to make sense of the response. You may also notice that this response kind of looks like json. That's because it mostly is. Let's unpack what we're seeing.

Parsing the response

You'll notice that each line in the response starts with a number and a :. After that, we sometimes get a letter. In this case, all we can see is an I on some of the lines. The rest is JSON.

That means that we can easily turn a line like:

3:I{"id":47767,"chunks":["2272:static/chunks/webpack-7dc9770b4e094816.js","2971:static/chunks/fd9d1056-ea8ad81a8bf99663.js","596:static/chunks/596-5ca25ac509d95d33.js"],"name":"","async":false}

Into a JSON object like:

{
   "id":47767,
   "chunks":[
      "2272:static/chunks/webpack-7dc9770b4e094816.js",
      "2971:static/chunks/fd9d1056-ea8ad81a8bf99663.js",
      "596:static/chunks/596-5ca25ac509d95d33.js"
   ],
   "name":"",
   "async":false
}

Suddenly a lot more readable. Initially when wanting to understand this, this is what I did. Copy, paste, edit the beginning slightly, format as JSON. But it got really tedious, really quick.

So I thought, why not, I write a little parser. We can start like this:

  1. Split the response by newlines
  2. Look for a number in the beginning, let's call that identifier.
  3. Then look for a :, and after that, there should either be a special letter or the beginning of JSON content. If there's a letter, like I, let's save that and call it type. If there's no letter, let's just let the type be null for now.
  4. After that, there should be JSON data. Let's save that as data.

Repeated over each line, an array looking somewhat like this.

[
  {
    "identifier":0,
    "type":null,
    "data":[
      "OaPKRBs0KvtlR-v4ORTOM",
      [
        [
          "children",
          "(main)",
          "children",
          "blog",
          "... (more content)"
        ]
      ]
    ]
  },
  {
    "identifier":3,
    "type":"I",
    "data":{
      "id":47767,
      "chunks":[
        "2272:static/chunks/webpack-7dc9770b4e094816.js",
        "2971:static/chunks/fd9d1056-ea8ad81a8bf99663.js",
        "596:static/chunks/596-5ca25ac509d95d33.js"
      ],
      "name":"",
      "async":false
    }
  },
  {
    "identifier":4,
    "type":"I",
    "data":{
      "id":57920,
      "chunks":[
        "2272:static/chunks/webpack-7dc9770b4e094816.js",
        "2971:static/chunks/fd9d1056-ea8ad81a8bf99663.js",
        "596:static/chunks/596-5ca25ac509d95d33.js"
      ],
      "name":"",
      "async":false
    }
  },
  {
    "identifier":5,
    "type":"I",
    "data":{
      "id":46685,
      "chunks":[
        "6685:static/chunks/6685-a4c378aab6a445df.js",
        "3517:static/chunks/app/(main)/blog/page-5ef(...).js"
      ],
      "name":"",
      "async":false
    }
  },
  {
    "identifier":6,
    "type":null,
    "data":"$Sreact.suspense"
  },
  {
    "identifier":1,
    "type":"",
    "data":[
      "$",
      "$L3",
      null,
      {
        "parallelRouterKey":"children",
        "segmentPath":[
          "children",
          "... (more content)"
        ]
      }
    ]
  },
  {
    "identifier":2,
    "type":null,
    "data":[
      [
        "$",
        "meta",
        "0",
        {
          "charSet":"utf-8"
        }
      ],
      [
        "$",
        "title",
        "1",
        {
          "children":"Blog"
        }
      ],
      "... (more content)"
    ]
  },
  {
    "identifier":7,
    "type":null,
    "data":[
      "$",
      "div",
      null,
      {
        "className":"inflate-y-8 ...",
        "children":[
          [
            "$",
            "section",
            "2022",
            {
              "className":"flex flex-col items-start",
              "children":[
                [
                  "$",
                  "h3",
                  null,
                  {
                    "className":"font-heading text-3xl ...",
                    "children":"2022"
                  }
                ],
                "... (more content)"
              ]
            }
          ]
        ]
      }
    ]
  }
]

I've cut out a lot of the data to keep the length reasonable, but it's enough to get an idea the structure.

Now we've got something much more readable. You can start to see there are different types of data.

  • Looking at the I types, it looks like they're referencing to some javascript files. These are files that need to be loaded to render content that is on the page.
  • Looking at the null types, some of them look a lot like React elements. There's html tags, classNames and children. These are indeed used for representing the tree of the page.

As you might imagine, there's a lot more nuance and details to this format. Please see the above as only a simplified example.

Visualising the data

When I had this parsed structure in place, I began thinking about ways to present it in a nicer way. Chrome DevTools had really long horizontal lines, but what I got myself instead a really long (vertical) formatted json file.

My first idea was to split each line into some tabs. Each tab gets the identifier , a remapped type as well as a size indicator that shows the % of the total response length for each line.


With different types for each line, we can render tab content differently for each of them.

For the client ref type, you see a list of files that need to be loaded.

Sometimes you can see the name of a client component as well.

Then for the tree type, I render code that closely matches the JSX you write.

Producing this tree was quite a fun challenge. The gist of it is a recursive component called Node. That walks the component tree and outputs styled React elements as it progresses.

A basic tag is expressed like this by React:

["$", "p", "0", {"children": "Hello world"}]

I render this like this:

You can also collapse tags:

The Node component can also take an array of elements:

[
  ["$", "p", "0", {"children": "Hello world"}],
  ["$", "p", "0", {"children": "Hello world"}],
  ["$", "p", "0", {"children": "Hello world"}]
]


And the output looks like this:

I could probably write a whole article about just this component, but you can explore some examples I've set up in a Storybook. The goal is valid JSX output that you can copy into your editor.

If you look around the trees, you may also find some strings like $L6. This is a reference to another line. It could for example be a client component or another tree.

The first tool

Now I had some building blocks, I made rsc-parser.vercel.app.

This is a page where you can copy paste network response sites using RSC, and get a nicer experience exploring the data. You can explore the trees and imports and get a feel for the order that things are streaming in.

This was already useful, and let me find some curious things on my site. For example, I noticed the object of data that I send to my NextSanityImage client component was excessively large:

It turns out that Sanity has all this useful metadata about images. Great if you need it, unnecessary if you're not using it. A simple tweak to my query to only select the fields that I needed reduced the size of the page by 34%.

The image={} prop now looks like this:

A similar but even more embarrassing mistake on the /blog route reduced the size by -86%. Issues like this can be caught in other ways, but this tool made it really obvious what what actually being sent over the wire.

Creating a browser extension

The website that you can copy-paste into is useful, but I realised quite quickly that it would be even better if you could just record the responses as they came in and as you browsed. That lead me to to create an extension that runs next to your side, and presents information as it comes in.


What you're seeing above is the extension running next to the production version of this website, with a scrubbable timeline at the top, various RSC responses in a vertical tab list, and their content of each response parsed and rendered.

Creating an extension not only makes it it a lot easier to quickly explore what's being sent, but it also allows you to time-travel the data. The extension stores timestamps for each streaming chunk, letting you granularly how the lines for a page are loaded. There are lots of opportunities for interesting visualisation here.

Next steps

This extension is currently still in very early stages, and a few key things need to be resolved:

  • fetch() is patched globally on all sites to clone bodies for RSC responses. This is useful for RSC fetches, but probably not great for everything else. Either another way needs to be found or it should only happen temporarily (like when pressing a record button).
  • The DevTools panel s not aware of the tabs, so all of them will send data to it.
  • Firefox support is missing.
  • Distribution to extension stores.

Furthermore, my reverse-engineering of the RSC wire format is not sustainable longer-term and likely to break soon. So I'm looking at ways to make use of some of the internal React code for parsing responses.

Usage

But if you're feeling adventurous, you can still try it by downloading build artefacts from GitHub and loading them into a chromium-based browser. To minimize risk of breakage on other sites, you could load it into a different browser profile.

If you still want to try the parsing and visualisation, then copy-pasting into rsc-parser.vercel.app is a risk-free alternative. They're using the same code, but the website lacks all time-based functionality.


If you're interested in reading more about RSC I recommend the following:


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[Skeleton Loading with Suspense in Next.js 13]]> https://alvar.dev/blog/skeleton-loading-with-suspense-in-next-js-13 https://alvar.dev/blog/skeleton-loading-with-suspense-in-next-js-13 Thu, 29 Dec 2022 00:00:00 GMT With Next.js 13 released recently, I've started thinking a lot more about how loading happens and is communicated to the user. I ended up with something like this:



Suddenly, with Suspense, it is way more scaleable and convenient to do something pretty yet simple. However, in my adventures, I ran into some gotchas along the way and found some strategies to keep it all tidy. Let's dive in!

Hold up, Suspense?

No worries. Suspense is a special new component that handles something loading. While that is happening, it renders a fallback. Let's look at a minimal example:


function BlogPosts() {
  return (
    <section>
      <h2>Blog posts</h2>
      
      <Suspense fallback={<p>Loading...</p>}>
        <Posts />
      </Suspense>
      
    </section>
  )
}

This example is not complete, but we can start to see a structure. While <Posts /> are loading, show <p>Loading...</p>.

Let's take a look at <Posts />. There are two ways to do it, depending on if it's rendered on the server or the client.

// If rendered on a server
async function Posts() {
  const posts = await fetch("http://example.com");
  
  return (
    <ul className="space-y-6">
      {posts.map(({title, description}) => {
        return (
          <article className="space-y-2">
            <h3>{title}</h3>
            <p>{description}</p>
          </article>
        )
      })}
    </ul>
  )
}

// If rendered on a client
function Posts() {
  const posts = use(fetch("http://example.com"))
  
  return (
    <ul className="space-y-6">
      {posts.map({title, description} => {
        return (
          <article className="space-y-2">
            <h3>{title}</h3>
            <p>{description}</p>
          </article>
        )
      })}
    </ul>
  )
}

These both are very similar, and also behave similarly in terms of Suspense. There are big differences underneath, but we don't need to consider them for our skeletons.

Let's make some Skeletons!

I use Tailwind, and there are lots of useful utilities to make making skeletons simpler. Even so, I found making a <Skeleton /> component to be useful. Mine looks something like this:

export default function Skeleton({ className }: { className: string }) {
  return <div className={`bg-slate-200 motion-safe:animate-pulse rounded ${className}`} />;
}

You get a few nice things from doing this:

  • The skeleton has a slight pulsating animation, to indicate that there is activity happening.
  • The animation does not show if the user prefers reduced motion (via motion-safe).
  • There's a default design with slightly rounded corners and a background color.

Now let's take this, and try to make a component indicating loading for our <Posts />.

function Loading() {
  return (
    <div className="space-y-6">
      <div className="space-y-2">
        <Skeleton className="w-[30ch] h-[1.25rem]"/>
        <Skeleton className="w-[45ch] h-[1rem]"/>
      </div>
      <div className="space-y-2">
        <Skeleton className="w-[30ch] h-[1.25rem]"/>
        <Skeleton className="w-[45ch] h-[1rem]"/>
      </div>
      <div className="space-y-2">
        <Skeleton className="w-[30ch] h-[1.25rem]"/>
        <Skeleton className="w-[45ch] h-[1rem]"/>
      </div>
    </div
  )
}

This gives us something like this:

We can now use this component in our <Suspense> like this.

function BlogPosts() {
  return (
    <section>
      <h2>Blog posts</h2>
      
      <Suspense fallback={<Loading/>}>
        <Posts />
      </Suspense>
      
    </section>
  )
}

That works! We can stop here, but there is an opportunity for a different structure.

What if each component provides their own skeleton

What if each Post in our <Posts /> list provided its own Skeleton? Let's take a look at how that plays out:

// Post.tsx

import Skeleton from "../components/Skeleton"

export function Post({title, description}) {
  return (
    <article className="space-y-2">
      <h3>{title}</h3>
      <p>{description}</p>
    </article>
  )
}

export function PostLoading() {
  return (
    <div className="space-y-2">
      <Skeleton className="w-[30ch] h-[1.25rem]"/>
      <Skeleton className="w-[45ch] h-[1rem]"/>
    </div>
  )
}

// RecentPosts.tsx

import { Post, PostLoading } from "./Post"

function RecentPosts() {
  return (
    <section>
      <h2>Blog posts</h2>
      
      <ul className="space-y-6">
        <Suspense fallback={<PostsLoading/>}>
          <Posts />
        </Suspense>
      </ul>
      
    </section>
  )
}

async function Posts() {
  const posts = await fetch("http://example.com");
  
  return (
    <>
      {posts.map((post) => {
        return <Post {...post} />
      })}
    </>
  )
}

function PostsLoading() {
  return (
    <>
      <PostLoading />
      <PostLoading />
      <PostLoading />
    </>
  )
}

This is pretty much what I landed in. How much to separate the components into different files is up to you, but the idea of a component exporting both its complete and loading state was a powerful idea for composing skeleton UIs on my site.

If you've tried it yourself, I'd love to hear your take on skeleton UIs with Suspense.


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[TailwindCSS with @next/font]]> https://alvar.dev/blog/tailwindcss-with-next-font https://alvar.dev/blog/tailwindcss-with-next-font Sun, 30 Oct 2022 00:00:00 GMT If you're looking into and trying out the new Next.js 13, you might have noticed @next/font. If you haven't, it's a way to optimize local and external fonts in Next via various advanced techniques. The good thing is they did it for us, and it works!

But as I started thinking about trying it out, I noticed that the docs didn't provide any guidance on how to integrate with Tailwind yet, which I use for my site. I realised that I needed to do some tweaking to make it works smoothly. Without further ado, here's what I did.

Edit: Since writing this, I found out that the same setup also works well with the ./pages directory. I have added a section on how to use that below.

Previous setup

My previous setup defined fonts like this:

// tailwind.config.js

module.exports = {
  ...
  theme: {
    ...
    extend: {
      fontFamily: {
        heading: ["MADE Dillan", "sans-serif"],
        subheading: ["Space Text", "sans-serif"],
      },
    }
  }
}
/* ./style/global.css */

@tailwind base;
@tailwind components;

@font-face {
  font-family: "MADE Dillan";
  src: url(/fonts/made-dillan.woff) format("woff");
  font-display: swap;
}

@font-face {
  font-family: "Space Text";
  src: url(/fonts/space-text-medium.woff) format("woff");
  font-display: swap;
}

@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');

html {
  font-family: "Inter", sans-serif;
}


@tailwind utilities;


This way, my base font was Inter, and I used font-heading and font-subheading in my classNames wherever I wanted something else. The setup was likely far from optimal, but it worked.


Enter @next/font

The first step I did was to install the package:

npm i @next/font
or
yarn add @next/font


Since I was using both local font files and Inter Google Fonts, I imported @next/font/google and @next/font/local in my root layout.tsx file in the ./app folder.

// ./app/layout.tsx

import { Inter } from "@next/font/google";
import localFont from "@next/font/local";


Setting up layout.tsx

Proceeding in the same file, I defined my three fonts. Note here, that the two local fonts have a variable defined, while my base font Inter does not. This is important for the next steps.

Side note: If you're wondering why the Inter font has subsets: ["latin"] defined, it's to only load a smaller part of the glyphs in the font when all of them are not used. The Next.js docs provide more information here.

// ./app/layout.tsx

import { Inter } from "@next/font/google";
import localFont from "@next/font/local";

const inter = Inter({
  subsets: ["latin"],
});

const madeDillan = localFont({
  src: "./assets/fonts/made-dillan.woff",
  variable: "--font-made-dillan",
});

const spaceText = localFont({
  src: "./assets/fonts/space-text-medium.woff",
  variable: "--font-space-text",
});


Moving on to the layout definition, here's where things get interesting. I have taken many classNames and combined them into one.

I already mentioned that my base font is Inter. That one I apply using inter.className. This makes all of the text on the page default to Inter.

Continuing, the other ones I add using madeDillan.variable and spaceText.variable. That way, I can use them in the next step.

// ./app/layout.tsx

export default function RootLayout({ children }: React.PropsWithChildren) {
  return (
    <html lang="en" className={`${inter.className} ${madeDillan.variable} ${spaceText.variable}`}>
      ...

Tweaking the Tailwind config

You can now use the CSS variable defined from layout.tsx in your tailwind.config.js like this:

// tailwind.config.js

module.exports = {
  ...
  theme: {
    ...
    extend: {
      fontFamily: {
        heading: ["var(--font-made-dillan)"],
        subheading: ["var(--font-space-text)"],
      },
    }
  }
}

Now in my case, I could remove most things from my global.css.

/* ./style/global.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

That's it! This setup lets me keep all of my components and their styling exactly as they were, while still benefiting from the features from @next/font.

Bonus: usage with the ./pages directory.


To be honest, I did not know this was possible, but I found out through Twitter. However, setup seems to be unclear and undocumented, so I figured out how to do the same setup in an app on Next 13 without the ./app directory.

First, I tried importing Inter from @next/font/google in _document.tsx. I thought that was resonable, since I have previously had <link> tags for loading fonts there. However, that cause this error.

Then I tried implementing something similar to what I did with above but in _app.tsx. It worked perfectly. Looks something like this:

// ./pages/_app.tsx

...

import { Inter } from "@next/font/google";
import localFont from "@next/font/local";

const inter = Inter({
  subsets: ["latin"],
});

const madeDillan = localFont({
  src: "./assets/fonts/made-dillan.woff",
  variable: "--font-made-dillan",
});

const spaceText = localFont({
  src: "./assets/fonts/space-text-medium.woff",
  variable: "--font-space-text",
});

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <div className={`${inter.className} ${madeDillan.variable} ${spaceText.variable}`}>
      <Component {...pageProps} />
    </div>
  )
}



Hope this helps you try it out!
I'd love to hear your thoughts and feedback here.


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[Thoughts on Photography Tools]]> https://alvar.dev/blog/thoughts-on-photography-tools https://alvar.dev/blog/thoughts-on-photography-tools Fri, 15 Jul 2022 00:00:00 GMT I've taken photos ever since I was a kid. I love framing up something I find beautiful, and freezing that moment in time. Over the years, I have taken photos with a few compact cameras, a DSLR, and lately my phone, due to my negative experience with Instagram.


When I started, phone cameras were terrible. But they have gradually gotten really good. My Pixel 6 takes nice photos. Previously, I stored photos in Google Drive, but recently set up a file server for myself. Google Drive worked well for photos from cameras, but has never been a good experience for uploading from a phone.


So, I use Google Photos. Very convenient, but with one big issue. Editing. In the app, it's quick, but the tools are lacking. Using other apps/programs is possible, but you need to manually download each photo, edit, and then upload again. It gets messy. Where Google Photos shines in quickly browsing through photos. With that you lose a lot of flexibility and freedom. It's perfect if you only want to take pictures with your phone, and only want to make simple edits.

There are of course other tools, like Adobe Lightroom, which seem nice at the surface, but have a monthly cost that can get quite high. All data is also cloud-based, in a structure not exposed in the file system. And lastly, they have no Linux clients at all.

There are numerous other tools, some being file-system-based, but they usually fail on some points, like having an interface that is clunky, or no mobile upload. This leaves me in a place where I don't have one source for all photos, and therefore multiple places to edit and view. Not convenient at all.

So, with that context in mind, I've been enticed to make my own thing. Here are the goals of what I want to create:

  • Open-source
  • Apps for all platforms on desktop/laptop and mobile
  • Operates on your own data store (disk, nas, service) and you can always see the data folder.
  • Keeps a folder structure that makes sense out of the box even if you stop using the app.
  • Keeps edits as separate files next to the originals, even multiple ones. So you know everything is there, but the app can show you only the most recent edit by default.
  • The main view is an endless scrolling grid automatically sectioned off by dates. Smaller versions of images are cached, so scrolling is always fast.
  • You can create collections for events/trips/etc. If you move a file to a collection, that is clearly represented in the data structure. No hidden databases or indexes. The app does the magic based on files in folders.
  • A map view based on GPS coordinates.
  • The app can create its own data as long as it can be destroyed. Saving caches or thumbnails is okay, for example.
  • Each platform can upload/import.
  • It should be possible to silo data at the root level. Google Photos has Archive, which is useful for receipts, memes and screenshots. Let that be folders at the top of the tree, with custom names but the same file structure as the main view.

As far as I know, this app does not exist. I've been thinking about making it for a while. I don't have experience coding photo editors or viewers, other than knowing what I want. If any of this sounds interesting, contact me and let's discuss.


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[Always add "name” to type="radio"]]> https://alvar.dev/blog/always-add-name-to-type-radio https://alvar.dev/blog/always-add-name-to-type-radio Wed, 06 Apr 2022 00:00:00 GMT A while ago, I was building a form in React. It wasn’t big or complicated, so I decided not to use a form library. I just listened for onChange and have a useState for each input. At first, this seemed to work file. I was building my form, and my state was updating as expected.

Then I added an <input type=”radio”/> and the tab order started getting ugly.  For example, I could only focus the first and last radios when using tab and shift+tab. And using the arrow keys, I could select all of the radios at the same time.

So this turns out to be because the “name” attribute was missing on my input. As soon as I added that, it started working again. I had no need or use for it in my JavasSript, so that’s why I didn’t add it, but it turned out to be needed for other reasons.


Hope this little article will save you from the bafflement I felt when I encountered this.


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[TypeScript things I wish I knew earlier]]> https://alvar.dev/blog/typescript-things-i-wish-i-knew-earlier https://alvar.dev/blog/typescript-things-i-wish-i-knew-earlier Mon, 04 Apr 2022 00:00:00 GMT About a year ago, I decided to try TypeScript. It seemed in demand, and I had previous positive experiences writing in typed languages. It turned out to be a good choice, but there are some things I wish someone had told me before I started.

If you're unfamiliar with TypeScript, it is a layer on top of JavaScript that specifies the type of things. You tell if a variable must be a string, or if an object must have the firstName and lastName keys inside.

No type checking at runtime

Before TypeScript, I had been using types with things like Java and Swift, which both have type checking built into the language. I knew TypeScript converted to JavaScript in the end, but I expected it would leave some type checking things behind. It doesn’t! There is no run-time type checking.

Upon first getting to know, I was confused. What was the point, then? Then I learned about compile-time type checking. Basically, the TypeScript compiler looks at your code before it runs in the browser or in Node, and checks to make sure everything lines up. There are other tools that do this, but with TypeScript and some of your own types to guide can become much smarter. You’ll often find errors before your code even runs!

Refactoring feels much safer

Due to the previous point, refactoring is easier. TypesScript keeps track of how code is being used and imported in multiple places. If you introduce a change in one part, it can tell you how other parts need to be changed for the code to work. As someone who easily gets distracted, this helps me remember to complete my refactoring adventures.

Prefer interfaces over types

In TypeScript, you can type JavaScript objects using type or interface. At first, I was confused about when to use each of these. The documentation was unclear. What I’ve landed on though is to use interface primarily. Interfaces do not have as many features as a type, so use them until it isn’t possible anymore. This seems to be the unwritten convention in TypeScript code.

You don’t need to use TypeScript everywhere

Unless you’re on strict settings, all JavaScript is valid TypeScript code. TypeScript can be sprinkled on top. Use this to your advantage. When converting code, everything does not have to be typed. You can do a bit at a time, or even completely leave parts out. This makes the process much easier and less time-consuming to switch.

These are my four things I wish I’d known before I started. What’s yours? I’d love to hear your tips here.



(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[How to make Swedish mud cake]]> https://alvar.dev/blog/how-to-make-swedish-mud-cake https://alvar.dev/blog/how-to-make-swedish-mud-cake Wed, 24 Nov 2021 00:00:00 GMT It's recently come to my attention that many pastries and cakes I take for granted are in fact local to my country. One of them happens to be "kladdkaka", translated to about "sticky cake".

My favorite recipe for this so far is too good not to share, so here you go. If you're looking for something healthy, this is definitely not it. All credit goes to Ingrid for this recipe.

Ingredients (Metric):

  • 2 eggs
  • 3 dl sugar
  • 60 ml cocoa powder
  • 1.5 dl flour
  • 7.5 ml vanilla sugar
  • 100 g melted butter
  • 1 ml salt

Ingredients (US):

  • 2 eggs
  • 1 ⅓ cups sugar
  • 4 tbsp cocoa powder
  • cups flour
  • ½ tbsp vanilla sugar
  • 3,5 oz melted butter
  • ⅕ tsp salt

Instructions:

  1. Turn on the oven at 175°C (347F).
  2. Mix the eggs and the sugar in a bowl.
  3. Pour in the rest of the ingredients and mix.
  4. Butter a springform pan and cover with bread crumbs.
  5. Pour in the ingredients.
  6. Bake for 16-18min.

Enyoy!


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[It's not smartness]]> https://alvar.dev/blog/on-smartness https://alvar.dev/blog/on-smartness Sat, 24 Jul 2021 00:00:00 GMT For as long as I can remember, I’ve been a geek—always interested in technical things. Meanwhile, my peers played different kinds of video games, Pokémon, or football. They followed the performances on Eurovision, talking about their bets and scores of the different countries. No one seemed to do what I did.

When I was in primary school, I would build all kinds of mechanical contraptions. Cableways between beds. Tall Lego towers. Robots. By no means was I good at it, but I kept trying until I at least partially succeeded. My tall constructions would quickly look like the leaning tower of Pisa.

Then one day, we had an electronics workshop. We were supposed to turn on a light bulb using three components: a cable, battery, and a bulb. The key here was to create a closed-circuit that lets electrons flow through each component.

I completed the task in seconds. Meanwhile, my peers struggled with the task for several minutes. To be explicit, I don’t blame them. You see, I had spent a similar amount of time solving the same problem at home, long before. I was already familiar with it and comfortable with electronics.

That day changed how many of my classmates saw me. Suddenly, I was the "smart" kid. This attention was delightful at first, because who doesn't want to be called smart, right? Since I was “smart”, I should also know what 9×13 is, right? Spoiler: I didn't, I was terrible at math. And how was it possible that I also didn't know all the winners in Eurovision? The list goes on and on—so many things the “smart” kid didn’t actually know. The label proved inaccurate, and everyone moved on.

But this situation puzzled me. In my own mind, I was hardly smart at all. I had simply spent all the time they spent on what they liked (like football) on things I liked, like electronics.

So if I’m not “smart”, then what? I prefer “experienced”. Being interested in a niche problem space all my life made me more experienced, not smart. I consider someone smart if they can solve a novel problem in some area they are unfamiliar with.

Still, there are situations where I believe someone did something astonishing. But even there, it’s worthwhile to consider that you don’t know what they’re experienced in. It reminds me of the word “Sonder,” described by The Dictionary of Obscure Sorrows as the following:

n. The realization that each random passerby is living a life as vivid and complex as your own—populated with their own ambitions, friends, routines, worries and inherited craziness—an epic story that continues invisibly around you like an anthill sprawling deep underground, with elaborate passageways to thousands of other lives that you’ll never know existed …

The next time you're impressed by the abilities of someone, you might be even more amazed to consider what it took to get there. There is often much more to what they know than you think.


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)
<![CDATA[Instagram Ruined Photography for Me]]> https://alvar.dev/blog/instagram-ruined-photography https://alvar.dev/blog/instagram-ruined-photography Sat, 17 Jul 2021 00:00:00 GMT The following is a text I've been thinking about writing for years. Both as a cautionary tale to anyone getting into photography, and as an explanation for why I stopped. I've been reluctant to share this, but here's what happened.

Beginnings

Starting from when I was a kid, I've had a fascination with photography. I used to loan my mom's camera and capture everything that caught my eye. I wasn't any good at all at the start, but after getting my own camera as a birthday present, I improved.

Sometime after that, I started an Instagram account. At first, it was private, and only a few friends viewed what I posted. Limited reach. Getting encouragement and feedback kept me going.

Going public

Getting older, I made my account public. The motivation for this was to get more feedback. I’d always lacked any significant mentorship in my photography, and I wanted that from people who were better than me.

The discovery mechanism on Instagram was mostly based on hashtags. To get your posts noticed on a hashtag page, you need to get it popular. It wasn't obvious then, but that was the start of significant pain.

So I started using the hashtags that all the other posts used. Now I started getting likes and followers from strangers. At first, it was delightful. This many people like what I do? Wow.


What I didn’t know was that for each new like and follow, a little dopamine was sent out into my brain. Every little bit of attention was a hit of a drug I didn't know about. After a while, you get used to it—wanting more and setting ever higher standards for yourself. Did the latest post get fewer likes than the last few? The photo must be worse.

Soon, I started thinking about how my photos would be perceived even before taking them. I'd reject compositions that I liked before even clicking the shutter. All based on a vague grasp of what appeared to perform well.

Gaming the algorithm

Set on getting more: likes, followers, comments, I soon found ways to improve my metrics:

  1. Optimizing hashtags. Post with tags like #nature also used other tags. I'd locate these and fill my own posts with them. There are even websites made for this.
  2. Posting at specific times. I would look at the dominant geographic regions of my followers, and map out optimal times to post. For example, lunchtime on the US west coast.
  3. Faking interest. I'd view posts on a hashtag, open the profiles of the posters, and like a bunch of their posts. This led people to my profile.
  4. Deleting posts. If any of my posts performed badly, I'd delete them.

Result

Looking back at these tactics, they seem desperate. In a way, they were. I found myself in a highly competitive environment—too young to reflect on what I was doing: chasing metrics. I had completely lost my original goal in the process.

Using the tactics above, I reached 700 followers. All at the expense of mental health, which had gotten worse the further I got. By then, even picking up my camera gave me anxiety. So in March 2017, I stopped. My mental health quickly improved.

I've been thinking a lot about who to blame here. There seem to be no simple answers. At its core, Instagram is simply a photo-sharing platform. Maybe the situation could have been prevented from my side. Still, I know that many others use it in similar ways, whether they want to or not. With what’s coming out now about Facebook’s internal research, I suspect this experience is not uncommon.

Solution (in progress)

I haven't posted anything on Instagram since then. I'd still like to get back to taking and showing some people photos, but I'm still working on the best way to do this.

For now, using my phone reduces friction a lot. Some of these go on my Unsplash, but I'm still looking for the right place. Twitter appears to have a more natural and personal discovery mechanism due to interactions from people you follow being exposed on the feed. But I don’t know. If you know of a better platform, please contact me!


(This is an article posted to my blog at alvar.dev. You can read it online by clicking here.)]]>
hello@alvar.dev (Alvar Lagerlöf)