SolidJS is a Javascript framework for building fast, declarative UIs on the web. It shares many ideas with React, but does not use the virtual DOM to deliver a more performant and pragmatic developer experience.
In the playground, you can view the compiled output.
Also, you can change the compile mode, between “Client side rendering”, “Server side rendering”, and “Client side rendering with hydration”
Any code that you write in the playground can be exported to a coding sandbox, like Codesandbox. So helpful!
Philosophy - Think Solid
Declarative Data
Vanishing Components
Solid updates are completely independent of the components. Component functions are called once and then cease to exist.
Read/Write segregation
We don’t need true immutability to enforce unidirectional flow, just the ability to make the conscious decision which consumers may write and which may not.
Simple is better than easy
Compilation
Solid’s JSX compiler doesn’t just compile JSX to JavaScript; it also extracts reactive values (which we’ll get to later in the tutorial) and makes things more efficient along the way.
This is more involved than React’s JSX compiler, but much less involved than something like Svelte’s compiler. Solid’s compiler doesn’t touch your JavaScript, only your JSX.
Destructuring props is usually a bad idea in Solid. Under the hood, Solid uses proxies to hook into props objects to know when a prop is accessed. When we destructure our props object in the function signature, we immediately access the object’s properties and lose reactivity.
So in general, avoid the following:
function Bookshelf({ name }) { return ( <div> <h1>{name}'s Bookshelf</h1> <Books /> <AddBook /> </div> );}
And replace with props instead.
Dependency Arrays
In React, you’d declare the dependencies explicitly using the dependency array. If you didn’t, the effect would rerun whenever any state in the component changes. In Solid, dependencies are tracked automatically, and you don’t have to worry about extra reruns.
Looping with array.map
If we used array.map in Solid, every element inside the book would have to rerender whenever the books signal changes. The For component checks the array when it changes, and only updates the necessary element. It’s the same kind of checking that React’s VDOM rendering system does for us when we use .map.
Conditional if statements on re-rendering
In the Building UI with Components section of this tutorial, we noted that component functions run only once in Solid. This means the JSX returned from that initial function return is the only JSX that will ever be returned from the function.
In Solid, if we want to conditionally display JSX in a component, we need that condition to reside within the returned JSX. While this takes some adjustment when coming from React, we have found that the fine-grained control afforded by Solid’s reactive system is worth the trade-off.
Reactivity and proxy objects
In Solid, props and stores are proxy objects that rely on property access for tracking and reactive updates. Watch out for destructuring or early property access, which can cause these properties to lose reactivity or trigger at the wrong time.
onChange vs. onInput
In React, onChange fires whenever an input field is modified, but this isn’t how onChangeworks natively. In Solid, use onInput to subscribe to each value change.
No VDOM or VDOM APIs
Finally, there is no VDOM so imperative VDOM APIs like React.Children and React.cloneElement have no equivalent in Solid. Instead of creating or modifying DOM elements directly, express your intentions declaratively.
Solid Primitives
Signals - The basic way to manage state in the application
Similar to useState in React, but as a reactive value
Derived state - you can track a computed state from a signal, which is also reactive
You can pass the signal as a prop like this: <BookList books={books()} />. It’s not a typo to use the function as it’s passing an accessor, which is important for reactivity.
Effects - ability to react to signal changes
A driving philosophy of Solid is that, by treating everything as a signal or an effect, we can better reason about our application.
Looping
Solid has a component called <For /> with an each prop. You can pass in a signal that will make this reactive.
As you can see, onInput is the event handler that takes in the event. In this case, we are setting the new book for each input (the title and author).
The onClick handler for the button uses the addBook function where it can prevent the form from submitting, set the books using a new array, then resetting the new book. It should be noted that setBooks is using a callback function where you access the current state. Also, it should be noted not to mutate state by creating that new array (much like in Redux practice).
The primitive for any external data source is createResource. The function returns a deconstructed array with the data. It takes two arguments: the signal and the data fetching function.
Putting it all together, query is the signal. searchBooks is the data fetching function. Once the data is returned, we can loop over it, and for each item, we can set the books if selected.
The following is a code example introducing how Reactivity or Reactive Programming works.
import { createSignal, createEffect } from "solid-js";const [count, setCount] = createSignal(2);const [multipler, setMultiplier] = createSignal(2);const product = () => count() * multipler();// Change the count every secondsetInterval(() => { setCount(count() + 1);}, 1000);// Change the multiplier every 2.5 secondssetInterval(() => { setCount(multipler() + 1);}, 2500);// Effect automatically detects when a signal has changed// So you don't have to add a dependency array.// This is defined as "reactivity"createEffect(() => { console.log(`${count()} * ${multiplier()} = ${product()}`);});
createSignal works by creating a data structure that can read and write. To each of these functions, subscribers are added and updated
CreateEffect works by executing on a context queue (JS array). It takes its queue, executes it, then pops it (at least in pseudocode)
SolidJS uses “granular updates” so only the variables that change only update the DOM, and not entire components.
In this example, we extracted Multipler into its own component, added props, like React, and called it multiple times in App. As you will also notice, signals do not have to be in the function scope! This is counter to what you do in React. Of course, if you don’t want to share the signal across other components, you can keep it in the functional lexical scope.
I started this journey into Typescript by taking a ton of notes with Github Copilot X, “Typescript in 50 Lessons”, and the official documentation. I was able to tailor my experience in understanding how to create types through this method. Without further ado, here’s some notes that I generated (and modified).
What is Typescript?
TypeScript is a superset of JavaScript that adds optional static typing and other features to the language. It is designed to make it easier to write and maintain large-scale JavaScript applications. TypeScript code is compiled into JavaScript code that can run in any browser or JavaScript runtime.
TypeScript provides features such as classes, interfaces, enums, and modules that are not available in standard JavaScript. It also includes support for modern JavaScript features such as async/await and decorators.
TypeScript is developed and maintained by Microsoft, and it is open source and free to use. It is widely used in web development, and many popular frameworks and libraries such as Angular, React, and Vue have TypeScript support.
Static Typing
Static typing in TypeScript allows you to specify the types of variables, function parameters, and return values. This means that you can catch type-related errors at compile-time rather than at runtime.
For example, you can specify that a variable is of type string, and TypeScript will give you an error if you try to assign a number to that variable. Similarly, you can specify that a function takes a parameter of type number, and TypeScript will give you an error if you try to call that function with a string.
Here’s an example of a function that takes two numbers and returns their sum, with static typing:
function addNumbers(x: number, y: number): number { return x + y;}
In this example, the x and y parameters are of type number, and the function returns a value of type number. If you try to call this function with non-numeric arguments, TypeScript will give you an error.
In addition, you can declare types for functions separately to the function implementation. This allows you to define a function type once and reuse it in multiple places.
The void type is used to indicate that a function does not return a value. You can use void as the return type of a function to explicitly indicate that the function does not return anything.
Here’s an example of a function that returns void:
function logMessage(message: string): void { console.log(message);}
In this example, the logMessage function takes a parameter of type string and logs it to the console. The function returns void, which means it does not return a value.
Type Assertions
A type assertion is a way to tell the compiler that you know more about the type of a value than it does. Type assertions are sometimes necessary when working with values that have a more general type than you need.
You can use a type assertion by adding the type you want to assert to the end of an expression, preceded by the as keyword. Here’s an example:
let myValue: any = "hello world";let myLength: number = (myValue as string).length;
In this example, the myValue variable is declared as type any, which means it can hold any value. We then use a type assertion to tell the compiler that we know myValue is actually a string, so we can access its length property.
You can also use angle bracket syntax (< >) to perform a type assertion:
let myValue: any = "hello world";let myLength: number = (<string>myValue).length;
In this example, the type assertion is performed using angle bracket syntax instead of the as keyword.
It’s important to note that type assertions do not change the runtime behavior of your code. They only affect the type checking performed by the TypeScript compiler.
You can use rest parameters to represent an indefinite number of arguments as an array. Rest parameters are denoted by an ellipsis (…) followed by the parameter name.
Here’s an example of a function that uses rest parameters:
function sum(...numbers: number[]): number { return numbers.reduce((total, num) => total + num, 0);}
Primitive Types
There are several primitive types that represent the most basic types of values. These primitive types include:
number: represents numeric values, including integers and floating-point numbers.
string: represents textual values, such as words and sentences.
boolean: represents logical values, either true or false.
null: represents the intentional absence of any object value.
undefined: represents the unintentional absence of any object value.
symbol: represents a unique identifier that can be used as an object property.
let age: number = 30;let name: string = "John";let isStudent: boolean = true;let favoriteColor: string | null = null;let phoneNumber: string | undefined = undefined;let id: symbol = Symbol("id");
Non-primitive types are types that are based on objects rather than simple values. These types include:
object: represents any non-primitive value, including arrays, functions, and objects.
array: represents an ordered list of values of a single type.
function: represents a callable object that can be invoked with arguments.
class: represents a blueprint for creating objects that have properties and methods.
interface: represents a contract that describes the shape of an object.
enum: represents a set of named constants.
Dynamically Generated Types
Dynamically generated types are types that are generated at runtime based on the shape of the data. There are several ways to generate types dynamically in TypeScript:
Index signatures: You can use index signatures to define an object type with dynamic keys. For example:
interface Dictionary<T> { [key: string]: T;}
This interface defines a dictionary type with a dynamic key of type string and a value of type T.
Type assertions: You can use type assertions to cast a value to a specific type at runtime.
const data = JSON.parse(jsonString) as MyType;
This code uses a type assertion to cast the parsed JSON data to a specific type called MyType.
Type guards: You can use type guards to check the type of a value at runtime and conditionally cast it to a specific type. For example:
function isPerson(obj: any): obj is Person { return "name" in obj && "age" in obj;}function printPerson(obj: any) { if (isPerson(obj)) { console.log(`Name: ${obj.name}, Age: ${obj.age}`); } else { console.log("Not a person"); }}
Intersection types: You can use intersection types to combine multiple types into one.
Union Types
A union type is a type that can represent values of multiple types. Union types are denoted by the | symbol between the types.
function printId(id: number | string) { console.log(`ID is ${id}`);}
Union types are useful when you want to write code that can handle multiple types of values.
Intersection Types
An intersection type is a type that combines multiple types into one. Intersection types are denoted by the & symbol between the types.
interface Named { name: string;}interface Loggable { log(): void;}function logName(obj: Named & Loggable) { console.log(`Name is ${obj.name}`); obj.log();}
Intersection types are useful when you want to write code that can handle objects with multiple sets of properties and methods.
Value Types
This interface definition ensures that this Event must have a kind property with one of the three specified values. This can help prevent errors and make the code more self-documenting.
You can use a type guard function to check the type of a value at runtime and conditionally cast it to a specific type. A type guard is a function that returns a boolean value and has a special obj is Type syntax that tells TypeScript that the value is of a specific type.
function isPerson(obj: any): obj is Person { return "name" in obj && "age" in obj;}
You can then use the type guard function to conditionally cast a value to a specific type. Here’s an example:
function printPerson(obj: any) { if (isPerson(obj)) { console.log(`Name: ${obj.name}, Age: ${obj.age}`); } else { console.log("Not a person"); }}
Type Assertions
Type assertions in TypeScript are a way to tell the compiler that you know more about the type of a value than it does. Type assertions are similar to type casting in other languages, but they don’t actually change the type of the value at runtime. Type assertions use the as keyword.
const myValue: any = "hello world";const myLength: number = (myValue as string).length;
This can be useful in JSX format too.
const myComponent = ( <MyComponent prop1={value1} prop2={value2} />) as JSX.Element;
Type Aliases
Type aliases in TypeScript are a way to create a new name for an existing type. They allow you to define a custom name for a type that may be complex or difficult to remember.
Type aliases can also be used with union types, intersection types, and other advanced type constructs. They can help make your code more readable and maintainable by giving complex types a simpler name.
Mapped Types
Mapped types in TypeScript are a way to create new types based on an existing type by transforming each property in the original type in a consistent way. Mapped types use the keyof keyword to iterate over the keys of an object type and apply a transformation to each key.
type Readonly<T> = { readonly [P in keyof T]: T[P];};interface Person { name: string; age: number;}const myPerson: Readonly<Person> = { name: 'John', age: 30 };myPerson.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property.
Mapped types can be used to create many other types, such as Partial, Pick, and Record. They are a powerful feature of TypeScript that can help make your code more expressive and maintainable.
Partial is a mapped type that creates a new type with all properties of the original type set to optional. Here’s an example:
A type predicate in TypeScript is a function that takes an argument and returns a boolean value indicating whether the argument is of a certain type. Type predicates are used to narrow the type of a variable or parameter based on a runtime check.
function isString(value: unknown): value is string { return typeof value === 'string';}function myFunc(value: unknown) { if (isString(value)) { // value is now of type string console.log(value.toUpperCase()); } else { console.log('Not a string'); }}
We define a function myFunc that takes an argument value of type unknown. We then use the isString function to check if value is of type string. If it is, we can safely call the toUpperCase method on value because TypeScript has narrowed the type to string.
never, undefined, null Types
never is a type that represents a value that will never occur. It is used to indicate that a function will not return normally, or that a variable will never have a certain value.
function throwError(message: string): never { throw new Error(message);}
undefined and null are both types and values. The undefined type represents a value that is not defined, while the null type represents a value that is explicitly set to null.
TypeScript also has a --strictNullChecks compiler option that can help prevent null and undefined errors. When this option is enabled, variables that are not explicitly set to null or undefined are considered to be of a non-nullable type. This means that you cannot assign null or undefined to these variables without first checking for their existence.
Classes
Classes in TypeScript are a way to define object-oriented programming (OOP) constructs. They allow you to define a blueprint for creating objects that have properties and methods.
Here’s an example of a class in TypeScript:
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); }}
In this example, the Person class has two properties (name and age) and a method (sayHello). The constructor method is used to initialize the properties when a new object is created.
You can create a new Person object like this:
const person = new Person("Alice", 30);person.sayHello(); // logs "Hello, my name is Alice and I am 30 years old."
One of the main differences between TypeScript and JavaScript classes is that TypeScript allows you to specify the types of class properties, method parameters, and return values. This helps catch type-related errors at compile-time rather than at runtime.
Another difference is that TypeScript provides access modifiers such as public, private, and protected, which allow you to control the visibility of class members. This can help you write more secure and maintainable code.
Finally, TypeScript classes can implement interfaces, which are contracts that describe the shape of an object. This can help enforce type checking on objects that implement the interface.
Access modifiers in TypeScript classes are keywords that determine the visibility of class members (properties and methods). There are three access modifiers in TypeScript:
public: Public members are accessible from anywhere, both inside and outside the class.
private: Private members are only accessible from within the class. They cannot be accessed from outside the class, not even from derived classes.
protected: Protected members are accessible from within the class and from derived classes. They cannot be accessed from outside the class hierarchy.
Here’s an example of a class with access modifiers:
class Person { public name: string; private age: number; protected address: string; constructor(name: string, age: number, address: string) { this.name = name; this.age = age; this.address = address; } public sayHello() { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); } private getAge() { return this.age; } protected getAddress() { return this.address; }}
In this example, the name property is public, so it can be accessed from anywhere. The age property is private, so it can only be accessed from within the Person class. The address property is protected, so it can be accessed from within the Person class and from derived classes.
Abstract Class
An abstract class is a class that cannot be instantiated directly. Instead, it is meant to be subclassed by other classes that provide concrete implementations of its abstract methods.
An abstract class can have both abstract and non-abstract methods. Abstract methods are declared without an implementation, and must be implemented by any concrete subclass. Non-abstract methods can have an implementation, and can be called by concrete subclasses.
Here’s an example of an abstract class in TypeScript:
abstract class Animal { abstract makeSound(): void; move(distanceInMeters: number) { console.log(`Animal moved ${distanceInMeters}m.`); }}class Dog extends Animal { makeSound() { console.log("Woof! Woof!"); }}const dog = new Dog();dog.makeSound(); // logs "Woof! Woof!"dog.move(10); // logs "Animal moved 10m."
In this example, the Animal class is an abstract class that defines an abstract makeSound method and a non-abstract move method. The Dog class is a concrete subclass of Animal that provides an implementation of the makeSound method. The Dog class can also call the move method inherited from Animal.
But then, what’s the difference between an abstract class and an interface? Take a look at the following example:
In this example, the Animal abstract class and the IAnimal interface both describe objects with a makeSound method and a move method. However, the Animal class is meant to be subclassed, while the IAnimal interface is meant to be implemented.
Some advantages of using inheritance instead of interfaces are:
Multiple Inheritance: An interface can be implemented by multiple classes, while an abstract class can only be subclassed by one class. This can help you create more flexible and reusable code.
Lighter Weight: An interface is a lighter weight construct than an abstract class, since it does not have any implementation details. This can help you write more modular and composable code.
Interfaces
In TypeScript, interfaces are contracts that describe the shape of an object. They define a set of properties and methods that an object must have in order to be considered an implementation of the interface.
Here’s an example of an interface in TypeScript:
interface Person { name: string; age: number; sayHello(): void;}
In this example, the Person interface has two properties (name and age) and a method (sayHello). Any object that implements the Person interface must have these properties and method.
You can use an interface to enforce type checking on objects that implement it. For example, you can define a function that takes a Person object as a parameter:
function greet(person: Person) { person.sayHello();}
In this example, the greet function takes a Person object as a parameter. TypeScript will give you an error if you try to call this function with an object that does not implement the Person interface.
While interfaces and classes have similar ways in defining object types, here are some differences:
Implementation: A class can have both properties and methods, while an interface can only have properties and method signatures. A class is an implementation of an object, while an interface is just a description of an object.
Instantiation: A class can be instantiated to create objects, while an interface cannot. An interface is just a contract that describes the shape of an object.
Inheritance: A class can inherit from another class or multiple classes, while an interface can only extend other interfaces.
Access Modifiers: A class can have access modifiers (public, private, protected) to control the visibility of its members, while an interface cannot.
The benefits of using an interface are the following:
Type checking: Interfaces allow you to enforce type checking on objects that implement them. This can help catch errors at compile-time rather than at runtime.
Code reuse: Interfaces can be used to define a common set of properties and methods that multiple objects can implement. This can help reduce code duplication and make your code more modular.
Abstraction: Interfaces can be used to abstract away implementation details and focus on the contract between objects. This can help make your code more maintainable and easier to reason about.
Polymorphism: Interfaces can be used to create polymorphic behavior, where different objects can be used interchangeably as long as they implement the same interface.
Polymorphism
Polymorphism is the ability of objects to take on multiple forms. In TypeScript, interfaces can be used to create polymorphic behavior, where different objects can be used interchangeably as long as they implement the same interface.
Here’s an example of polymorphism in TypeScript:
interface Shape { getArea(): number;}class Rectangle implements Shape { constructor(private width: number, private height: number) {} getArea() { return this.width * this.height; }}class Circle implements Shape { constructor(private radius: number) {} getArea() { return Math.PI * this.radius ** 2; }}function printArea(shape: Shape) { console.log(`The area of the shape is ${shape.getArea()}`);}const rectangle = new Rectangle(10, 20);const circle = new Circle(5);printArea(rectangle); // logs "The area of the shape is 200"printArea(circle); // logs "The area of the shape is 78.53981633974483"
In this example, the Shape interface defines a getArea method that returns a number. The Rectangle and Circle classes both implement the Shape interface, so they both have a getArea method. The printArea function takes a Shape object as a parameter, so it can be called with either a Rectangle or a Circle object. This is an example of polymorphism, where different objects can be used interchangeably as long as they implement the same interface.
Namespace
In TypeScript, a namespace is a way to group related code into a named scope. Namespaces can contain classes, interfaces, functions, and other code constructs.
You can define a namespace using the namespace keyword, and you can access its contents using the dot notation. Here’s an example:
In this example, the MyNamespace namespace contains an interface Person and a function greet. The Person interface is exported so that it can be used outside of the namespace. The greet function is also exported, and it takes a Person object as an argument.
To use the Person interface and the greet function, you can access them using the dot notation with the namespace name (MyNamespace.Person and MyNamespace.greet).
Enums
In TypeScript, an enum is a way to define a set of named constants. Enums are useful when you have a fixed set of values that a variable can take on, such as the days of the week or the colors of the rainbow.
Here’s an example of an enum in TypeScript:
enum Color { Red, Green, Blue,}let myColor: Color = Color.Red;console.log(myColor); // logs 0
In this example, the Color enum defines three named constants (Red, Green, and Blue). Each constant is assigned a numeric value (0, 1, and 2, respectively) by default. You can also assign string or numeric values explicitly:
enum Color { Red = "#ff0000", Green = "#00ff00", Blue = "#0000ff",}
Generics
Generics in TypeScript allow you to create reusable code components that can work with different types. They provide a way to define functions, classes, and interfaces that can work with a variety of data types, without having to write the same code multiple times.
Here’s an example of a generic function that takes an array of any type and returns the first element:
function getFirstElement<T>(arr: T[]): T { return arr[0];}
In this example, the syntax indicates that the function is generic and can work with any type. The T is a placeholder for the actual type that will be used when the function is called.
The arr parameter is an array of type T, and the function returns a value of type T. When the function is called, the actual type for T is inferred from the type of the array that is passed in.
Generics in TypeScript are commonly used in situations where you want to write code that can work with multiple types. Here are some common use cases for generics:
Collections: Generics can be used to create collections that can hold any type of data. For example, you can create a generic List class that can hold a list of any type of data.
Functions: Generics can be used to create functions that can work with any type of data. For example, you can create a generic identity function that returns its argument of type T.
Interfaces: Generics can be used to create interfaces that can work with any type of data. For example, you can create a generic Comparable interface that defines a method for comparing two objects of type T.
Classes: Generics can be used to create classes that can work with any type of data. For example, you can create a generic Repository class that provides CRUD operations for a data type T.
Modules
In TypeScript, a module is a way to organize code into reusable units. A module can contain classes, functions, interfaces, and other code constructs.
A module can be defined using the export keyword, which makes its contents available for use in other modules. You can also use the import keyword to import code from other modules.
Here’s an example of a module in TypeScript:
// math.tsexport function add(a: number, b: number): number { return a + b;}export function subtract(a: number, b: number): number { return a - b;}
In this example, the math.ts file defines a module that exports two functions (add and subtract). These functions can be imported and used in other modules:
In this example, the app.ts file imports the add and subtract functions from the math module using the import keyword.
Decorators
In TypeScript, a decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.
Decorators can be used to modify the behavior of a class or its members, or to annotate them with additional metadata. For example, you can use a decorator to add logging or validation to a method, or to mark a property as required.
Here’s an example of a decorator in TypeScript:
function log(target: any, key: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`Calling ${key} with arguments: ${JSON.stringify(args)}`); const result = originalMethod.apply(this, args); console.log(`Result of ${key}: ${JSON.stringify(result)}`); return result; }; return descriptor;}class Calculator { @log add(a: number, b: number) { return a + b; }}const calculator = new Calculator();console.log(calculator.add(1, 2)); // logs "Calling add with arguments: [1,2]" and "Result of add: 3"
In this example, the log function is a decorator that takes three arguments: the target object (the class prototype), the name of the decorated method (add), and a descriptor object that describes the method. The log function modifies the behavior of the method by adding logging statements before and after the original method is called.
The Calculator class has a method add that is decorated with the @log decorator. When the add method is called, the log decorator is executed, which logs the arguments and result of the method.
Similarities can occur on the class level. For example, in this following example, you can write a logger for instantiation.
function log(target: any) { console.log(`Creating instance of ${target.name}`);}@logclass Calculator { add(a: number, b: number) { return a + b; }}const calculator = new Calculator(); // logs "Creating instance of Calculator"console.log(calculator.add(1, 2)); // logs 3
Key takeaways from short pieces advice Kevin Kelly has collected over decades. I've turned those into different categories of advice that has resonated with me.
Over the past month, I have been making small enhancements to make the website 1% better. I’m excited to talk about many of them, some noticeable, some under the hood.
Major Updates
Tailwind, 🌑 Dark Mode, and Solving My Woes with Styling
Some major updates are around styling. Previously, I was using some CSS files to style the different pages. However, after a lot of tinkering with Tailwind, I went full in. And boy has it made it easier to update styling, especially around media queries.
I hate to think about media queries, write a quick template or some variables, when tailwind makes that super easy. Add to your classes, sm: or md:, or lg: and you’re good to go for what you want to style at a specific breakpoint. What it means is, you have to think about your smallest width first, then move on to where you want your next breakpoint and add that utility class.
Where I immediately saw this benefit was a bug I found with a list of navigation links. With a little bit of tinkering, I was able to make the classes from flex with a default of no wrapping, to flex-wrap then md:flex-no-wrap to get that desired effect.
Another super helpful thing was immediately adding dark mode support. With the prefix, dark:, it made my life so much easier. I saw some initial places where I needed to modify the website immediately, and having this utility class prefix allowed me to get straight to styling vs. thinking about CSS selectors and rules. That said, having a background in CSS is a huge help, and even if I had to write only CSS, I’d still be operable.
Also, you may now notice there are links next to headings. That’s all thanks to installing my first rehype plugin! I had no idea there was such a rich community of rehype and remark plugins for all different use cases. I’m excited to explore more of them.
Lastly, my RSS feed has been updated. The books and films are added to the main feed, as well as styling the feed page itself. I didn’t know it used a .xsl file, and I used a generic template.
New Collections 📚 🎥
I expanded on my library. The films, anti-library, and book reviews have been added. The lindy library is passable and needs a bit more design and explanation. I’m proud of adding the log alongside the movies and books. I plan to link the log back to those pages too, where I can.
A lot of tinkering with Tailwind helped make this a lot easier too. I knew the design needed to look a little different, so for the past month, I’ve been browsing the pages on my own until they looked the way I wanted.
Content Updates
My resume, about page, now page, and many, many other pages have been updated. This was a long time coming. For years, I’ve kept the same about page, being about the “brief” version vs. the Twitter version vs. the LinkedIn version. I don’t think that represented me well. After some inspiration from Derek Sivers’ about page, I restarted the process from scratch. I thought more about it as an autobiography, rather than a professional LinkedIn page. It’s a lot more personal, and hopefully, that shines than the corporate version that I wince at.
Search 🔍
I installed the Algolia Crawler earlier in the month and finally had time to add a search component over the weekend. A little more refinement is needed to get the search results to display correctly (specifically removing the navigation text), but we’ll get there.
Under the hood 🚘
Getting the initial setup with Netlify was a major lift. Everything else has felt like a smaller lift, but also necessary. For example, dependabot wasn’t installed, so I made sure dealing with package updates was automated. I included meta tags that were missing for SEO purposes, verifying Twitter cards and Open Graph links (like LinkedIn) to work correctly.
What’s next?
Honestly, I’m going to take a step back and work on the content. While I’d love to update the Lindy Library, I’d much rather do what I do best, and write more blog posts. I have been working on a personal content pipeline to gather ideas, slowly work through books, and put my own take out there.
If there are any issues you find with the website, feel free to open an issue on GitHub. Ta ta for now 👋.
Changelog
#43 Add Github Actions to automatically tag the repository by semantic versioning
#45 Migrate from vanilla CSS to Tailwind CSS
#48 Dark Mode added!
#50 Additions to “Logs” - Add concert and musical for 2023
#51 Addition to “Library” - Add a brief lindy library
#54 Install Dependabot to Github project
#60, #61 Add first rehype plugin to add relative links to headings
#62 Netlify configuration - add security headers and update timezone
#66 Added top navigation for blog
#75 Expand on the films section and add them to the main page enhancement
#76 Extend press to include more talks, papers, and updated formatting enhancement
#82 Updated meta tags for SEO. Twitter cards and Open Graph links should correctly render updated hero images
#86 RSS page styled, should make it a lot more readable
#91 Migate blog posts with book reviews to the library book detailed pages
"Writing for Software Developers focuses on teaching you how to envision, create, and publish mid-length technical tutorials and articles, but the principles and practices you’ll develop will help you write anything from a short README to an entire technical book. And regardless of why you write, this guide aims to provide you with the techniques you will need to reach your goals."
"Peter Seibel interviews 15 of the most interesting computer programmers alive today in Coders at Work, offering a companion volume to Apress’s highly acclaimed best-seller Founders at Work by Jessica Livingston."
Finishing up the book, Laws of UX. It’s a great primer on the topic and I’m looking forward to sharing it with my team. Also reading “Saving Time: Discovering a Life Beyond the Clock” by Jenny O’Dell.
Slowly moving to San Jose, California. The apartment has been finalized, and we have two months to move in. Also, planning my wedding for May 2024.
Deployed and continually making improvements to this website. I’m using Astro.js and re-learning Svelte along the way.
I’m nearly done with playing through Hogwarts Legacy.
I’ll add my schedule, written down so I have a better grasp of it.
It’s equivalent to writing it down on a calendar for the day. Helps remind me I have meetings coming up
I’ll also use this opportunity to interstitial journal to discover retroactively
This Day In The Past
I’ll review my daily review from last year to discover any insights
Mood Ranking
I’ll rank my mood out of 10, which I review at the end of the week.
This helps me see even if there are down days, there will be up days in the future.
Share your daily routine! If you’re systematic like me, I’d love to hear about it. If you free ball it, I’d love to hear from you too!
I took Tiago Forte’s course, “Building a Second Brain” back in 2021, and one of the first exercises we did was “12 Favorite Problems”. The idea is to lay out your favorite questions, and these questions are long-term problems without easy answers. The following are some questions that I continue to think about often, and take notes when I find new insights.
Questions
How can I build my relationship to be of equal partnership, while respecting what makes us unique?
How do I create and maintain a healthy lifestyle?
How do I maintain relationships with family, friends, and community that builds and encourages bonds, trust, and healthy lifestyles?
How do I incorporate rest alongside the stress of everyday life? Related: Tidying up and clear mental space
How do I build a solid financial future for my family and myself?
How can I grow my technical and interpersonal capabilities that continues to solve problems rather than cranking out widgets?
Interpersonal meaning soft-skills
What are the small, incremental changes that, if continued on a short time interval, can have compounding effects over time? Thinking habits, routines
How can I foster inspiration for creative output? Related: music, writing, and painting
How do I actively engage with content rather than passively consume it?
What are your favorite problems? Feel free to email them to me 😄.
One of those picturesque romance movies from a Merchany-Ivory production of an E. M. Forrester novel. And a punchable performance from Daniel-Day Lewis.
Cate Blanchett stars as Lydia Tár, chief conductor of a major German orchestra. In a strongly, yet still and resounding piece that leaves you contemplating why we stopped tolerating bad people making good art, and if that's ok.
A challenge to conventional time management advice, highlighting the finite nature of life and urging readers to prioritize meaningful experiences. The book emphasizes intentional living, encouraging a shift from constant busyness to a purpose-driven approach.
The author recounts her experiences in Bhutan and how it relates back to how the Bhutanese feel towards happiness and other contemplative thoughts on life.
A paper which provides an extensive exploration of memory, covering topics such as management, organization, and optimization. Despite being over a decade old, the paper remains highly relevant to software engineering today, as some of its insights and concepts continue to be applicable for optimal code performance.
Ira Glass, the host of This American Life, shared his insights on storytelling in a short 30 minute interview revolving around the importance of engaging the audience, using vivid language, and focusing on the structure and characters of the story.
When you start the year heartbroken and scared to start again, where do you go? In my mind, being 32 meant looking forward to getting married, starting a family, buying a home. Life had other plans for me. I took some time to reflect, and I started by selecting an annual theme. It was my “Year of Challenges”, where the theme at first meant to start again and regain a sense of agency. It ended as a way to look at events outside your control.
Wake Up Call
One of my biggest takeaways from the past year came from the book “Four Thousand Weeks ”, by Oliver Burkeman. “Seek out novelty in the mundane”, Burkeman writes. When you’re a child, more experiences are novel because those are first time experiences. When you’re an adult, most experiences are mundane because most experiences are routine. Burkeman’s suggestion for making those mundane activities novel is to take the time to notice them and reflect.
A way I’ve been tracking this is by utilizing a second brain and reviewing notes that I’ve saved throughout the week. Little tidbits from articles I read, ideas that are in their infancy, potential future projects I may want to start. It’s like reviewing your journal in a more systematic way, and remembering what your past self said to help inform your present self, and potentially make decisions for your future self. (In the future, I will write about my experiences with developing a second brain, what it means, and how I utilize it).
How Love Lasts
I never expected the year to be where I entered heart broken, and exited engaged. I had a lot of re-learning to do when it came to love. Furthermore, I read through How Not To Die Alone by Logan Ury to be an indispensable resource. Some big takeaways were “Date for a life partner, not a prom date”, and “Screw the spark”.
I had this erroneous belief that you should wait three years to propose to someone.
Year 1. The honeymoon phase.
Year 2. Share life experiences together through integrating with your partner’s family and friends.
Year 3. Share a home together.
But when you know, you know. Instead, I scrapped waiting years and I jumped straight to the ring and question. I was second guessing my plan. I wanted to surprise her at a rose garden in Oakland, but it was far too dark when we arrived. She was annoyed by the midterm elections, and I was thinking, what could a Plan B mean? I proposed in front of Fenton’s Creamery, and her annoyance became a delightful surprise. She was in tears, and I’ll never forget that moment.
A Run To Remember
I ran a marathon! I hadn’t been to the gym in two years, neglected my physical health during the COVID years, and was afraid to start working out again. What was different from the last time I found myself in similar circumstances was that I learned many lessons from my former physical trainer. I found an accountability partner in a friend who moved back to the area.
At first, it was just running every Saturday morning at Golden Gate Park. Then a different friend suggested I run the SF Half Marathon with her. I created a daily workout plan to get myself back in shape in 7 months, and it paid off. I ran the fastest half-marathon I could ever ask for, and it felt much better than the past two years combined. My biggest hope is that I take this feeling away with me for future years to come.
Exiting Thoughts
I intend to write about more experiences in short essays. Returning to writing is scary. I remember the video Ze Frank made about the “An Invocations on Beginnings”. He says “Let me think about the people who I care about the most, and how when they fail or disappoint me, I still love them, I still give them chances, and I still see the best in them. Let me extend that generosity to myself.” I think it’s hard to give yourself another chance, and easy to say “I’ll do this again another day”. Writing was my form of zen, and I miss it so much. Please consider continuing to read what I have to write, and support me in future endeavors.