Alvar Lagerlöf's Blog https://alvar.dev Developer and designer living in Stockholm Mon, 05 Jun 2023 01:43:23 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en-us <![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[On 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)