[ROUGH DRAFT]
SolidJS Enlightenment

By Cody Lindley & gpt-4

A better, faster, and more modern React, 99.9% of the Time!

1 : Overview

This section provides a high-level introduction to SolidJS, emphasizing its main architectural concepts and its distinctive approach to state management, components, and JSX. It doesn't serve as a step-by-step guide to getting started with SolidJS, but rather lays the groundwork for a deeper understanding of the unique features and advantages that SolidJS brings to modern UI development on the web platform. Expect this section to lay a foundation of words and concepts and the rest of the book to explain these words and concepts using code.

1.1 - What is SolidJS?

SolidJS, fundamentally, is a fine-grained reactive system leveraging signals and effects for the purpose of state management in the context of UI components.

The developer experience in SolidJS closely resembles that of React in terms of using components and JSX. However, SolidJS's design philosophy diverges in a number of key areas. Unlike React, which relies on a virtual DOM to batch updates and apply them efficiently to the real DOM, SolidJS compiles components down to highly efficient imperative code that operates directly on the DOM. This results in performance advantages, with less memory usage and faster update times.

While both React and SolidJS use a component-based architecture, the way they handle reactivity and state management differs. React uses a more coarse-grained reactivity model, typically through the use of hooks like useState and useEffect, where changes in state trigger a re-render of the component and its children.

SolidJS, on the other hand, adopts a fine-grained reactivity model. It leverages reactive primitives such as createSignal and createStore that make components reactive to state changes at a more granular level. This means that only the parts of the component that rely on a piece of state will be updated when that state changes, leading to potentially fewer unnecessary updates and better performance.

In terms of state management, SolidJS encourages the use of stores which provide an immutable state container. This approach offers clear structure and predictability, which can simplify state management in larger applications.

Lastly, although SolidJS uses JSX syntax similar to React, it treats JSX as a compile-time rather than run-time construct. This allows for better optimization during the compile step, contributing to the overall performance characteristics of SolidJS.

In conclusion, SolidJS presents a compelling blend of familiar developer experience borrowed from React and innovative solutions for performance and efficiency, making it a unique player in the landscape of JavaScript UI libraries.

SolidJS should be comprehended thought the following three lenses:

  1. Components: SolidJS components are stateless functions that return JSX elements (i.e., UI stuff via CSS & HTML). These components serve as factories for creating DOM elements that can encompass reactive values.

  2. JSX: SolidJS JSX is employed for creating and managing user interface elements. Components can return JSX elements, which may represent native DOM elements or other components.

  3. State Management: SolidJS uses signals to manage application state (Note: SolidJS stores are just a tree of signals). Effects and JSX updates subscribe to signals. When a signal changes, only the parts of the UI that subscribe/track that specific signal are updated.

Let's delve deeper into each of these aspects to understand how they work together.

1.2 - Components

Components, a core concept in many modern JavaScript UI libraries, are also central to SolidJS. In Solid, components are simple, lightweight, and run-once composable functions that accept a props object and return JSX elements. The returned JSX elements can be native DOM elements or other components, similar to how components work in React.

However, unlike React, components in Solid are not stateful themselves and do not have instances. Instead, they serve as simple functions for DOM elements and reactivity, making them incredibly lightweight.

This design approach allows for better performance and more efficient memory usage, as each component only concerns itself with the creation of DOM elements and reactivity, rather than maintaining its own state and lifecycle.

Keep In Mind:

  1. "Components disappear in SolidJS" denotes SolidJS's build-time component compilation. Instead of maintaining runtime component instances like other frameworks, SolidJS components are merely functions that yield a UI description in JSX form. On compilation, these functions execute, converting their JSX into highly optimized JavaScript code for direct DOM manipulation. After this, component functions "disappear" as they leave no trace in the runtime code, leaving only the reactive values managing UI state.
  2. In contrast to React's repeated component calls during state or props changes, SolidJS components render just once. Following this, SolidJS's reactive system manages updates, keeping the DOM and data synchronized without necessitating re-rendering. This makes reasoning about DOM changes dead simple.

1.3 - JSX

JSX is a XML-like syntax extension for JavaScript that allows you to write HTML-like code in your JavaScript code. It allows us to define HTML/DOM elements and components in the same file as JavaScript code. JSX is not unique to SolidJS.

It's frequently used in libraries and frameworks like React and SolidJS to define the structure and appearance of the UI in a way that's often more intuitive and easier to understand than using pure JavaScript.

The cost of using JSX is that you need to use a transpiler to convert the JSX code into JavaScript code that the browser can understand.

1.4 - State Management (via signals, effects, and JSX updates)

SolidJS state management consist of signals, effects, and JSX updates.

Signals are values or states that can be tracked. Signals produce a reactive value that can be used in an effect or JSX Updates. When a signal's value changes, all effects and JSX updates tracking the signal are updated.

Solid JS provides the following patterns and helper functions to create a reactive signal:

Signal Description
createSignal() This function is used to create a basic reactive value or state. It returns two functions: a getter function to read the current value and a setter function to update the value.
Generic Derived Signal Generic generic derived signals are plain JavaScript functions that contain getter functions from createSignal(). The value of a generic derived signal is derived based on the value of other signals. Derived Signals must return a value not just reference a getter function. Derived values can not be set, instead they are set by the value of the signals they reference.
createMemo() This function is similar to createComputed(), but unlike createComputed(), createMemo() returns a getter function for the reactive computation, allowing its result to be read synchronously in other reactive contexts or computations. It's used when you want to create a memoized derived value that you can use in multiple places without causing the derivation function to run more than necessary.
createResource() This function is designed to handle the results of asynchronous requests. It is useful for managing states that involve fetching data from an API, a database, or other external data sources.
createStore() This function provides a structured interface for managing tree-like collections of signals. It's used to handle complex state management scenarios.
createSelector() Creates a conditional signal that only notifies subscribers when entering or exiting their key matching the value. Useful for delegated selection state.

Once a signal is set up, there are two primary ways to track a change in a signal:

JSX Updates - Within JSX you can reference signals. The simplest form of this is referencing a createSignal() getter function from within JSX. When signals change value the value is sent out to all the places in the JSX that reference the signal.

Effects (aka "side effects") - Roughly speaking effects are functions that reference signals from within their body and re-run when a signal value changes. Effects do not return values. This is what separates them from signals. SolidJS provides several different helper functions for creating effects:

Effect Description
createEffect() Creates a function that runs whenever its dependencies change. This is used for creating generic reactive side effects.
createDeferred() Creates a side effect that runs after all other synchronous effects. It's useful for situations where you need some computation to happen after everything else.
createRenderEffect() Creates an effect that runs whenever its dependencies change, but only during the component render phase. It's useful for updates that need to be tied to the rendering of the component.
createReaction() Creates a function that runs when its dependencies change. Unlike createEffect(), it doesn't run initially when created but only when its dependencies change.
createComputed() createComputed(): This function is used to create side effects that are dependent on the state. When the state that createComputed() depends on changes, it triggers the function passed into createComputed(). However, it doesn't return a value. Its main use is for triggering effects or actions rather than computing new values.

Keep In Mind:

  1. The term "signal" in JavaScript state management isn't universally standardized, but it typically refers to a mechanism for notifying parts of an application about state changes.

2 : createSignal() Signal

In this section, we focus on createSignal(), the foundational reactive concept within SolidJS.

2.1 - Using createSignal()

A signal is the fundamental reactive value in SolidJS that allows you to create a piece of state and update and observe changes to it.

When you call createSignal(), you pass it an initial value and it returns two functions:

  1. A getter function that allows you to read the current value of the signal.
  2. A setter function that allows you to update the value of the signal.

Whenever the setter function is called, any effects or JSX updates that depend on the signal will be re-run automatically.

Here is a simple example:

2.2 - Deciphering the createSignal() TypeScript Declaration

function createSignal<T>(
  initialValue: T,
  options?: { equals?: false | ((prev: T, next: T) => boolean) }
): [get: () => T, set: (v: T) => T];

This declares a function called createSignal which is generic over a type T. This means that T can be any type, and createSignal will work with values of that type.

createSignal takes two arguments:

  1. initialValue: T - This is the initial value of the signal. Since T can be any type, this initial value can be of any type.

  2. options?: { equals?: false | ((prev: T, next: T) => boolean) } - This is an optional argument. If provided, it should be an object with a single property equals. equals can be either false or a function. If it's a function, it should take two arguments of type T and return a boolean. This function is used to compare the previous and next values of the signal. If equals is set to false, it forces the signal to always trigger its listeners when the value changes.

createSignal returns a tuple [get: () => T, set: (v: T) => T]:

  1. get: () => T - This is a getter function that returns the current value of the signal.

  2. set: (v: T) => T - This is a setter function that allows you to update the value of the signal. It takes a new value of type T and returns the same value.

In summary, createSignal in SolidJS is a function that creates a signal with an initial value and optional custom equality checking function.

2.3 - createSignal() Tracks Primitive Value Changes and Reference Changes

The createSignal() signal will track changes to primitive values like numbers, strings, and booleans, as well as reference changes to objects and arrays (i.e. a new array or object is used).

Make sure you clearly understand, as previously demonstrated in the code example, that createSignal() by default does not track changes to object properties or array items. In other words, CreateSignal by default won't broadcast a change to effects or JSX when values within an object or array change. It will only broadcast a change when a completely new object or new array is passed to the setter function.

The downside, performance wise to using an array or object with createSignal() is that sending an update always cause the signal to notify its subscribers a change occurred. This is because the change that is being detect is a reference change not a deep equality check of the object or array.

2.4 - Force createSignal() to Always Trigger Updates

This code demonstrates how the use of the createSignal() equals option can change the behavior of updates.

When equality checks are disabled, the associated effects run on every update, regardless of whether the value changes or not.

2.5 - Getting a Previous Value in a createSignal() Setter Function

We can pass a function to the set function of a createSignal when the new value of the signal depends on the current value. Here is an example:

import { createSignal } from "solid-js";

function Counter() { 
  const [count, setCount] = createSignal(0);
  return (
    <>
      <p>Count: {count()}</p>
      <button
        onclick={() => {
          // Use the current count to calculate the new count
          setCount((currentCount) => currentCount + 1);
          // However this works too:
          // setCount(() => count() + 1);
        }}
      >
        Increment
      </button>
    </>
  );
}

In this example, the setCount function is passed a function that takes the current count and returns the new count. This is useful because the new count depends on the current count. The function passed to setCount is called with the current value of the count, and the returned value is used as the new value.

This pattern can be especially useful in more complex situations where the new value depends on the current value in a non-trivial way.

2.6 - Passing the createSignal() "equal" option a Function

The equal option of the createSignal function in SolidJS, in addition to accepting a boolean value, will also take a function. If the equal option is sent a function the function will be passed the current signal value and the next signal value from calling the createSignal setter function. In side this function you can create custom logic based on these two values. If the function returns true, the new value is considered equal to the current value and the signal is not updated. If the function returns false, the signal is updated with the new value.

Below is a use cases for passing a function to the equal option:

2.7 - How to Cancel Updates when createSignal() Objects and Arrays are deeply equal

3 : JSX Updates

In this section, we unravel the mechanics of JSX updates within SolidJS (i.e. using signals within JSX)

3.1 - What is a "JSX Update"?

Placing JavaScript within JSX that references a value from a Signal is consider using a JSX update. This is just a semantical term to suggest that the value is dynamic and the value will get updated when a signal value is updated (i.e., set)

Remember the following signals produce reactive values that can be used in JSX:

Signal Description
createSignal() This function is used to create a basic reactive value or state. It returns two functions: a getter function to read the current value and a setter function to update the value.
Derived Signal Generic derived signals are plain JavaScript functions that contain getter functions from createSignal(). The value of a generic derived signal is derived based on the value of other signals. Derived Signals must return a value not just reference a getter function.
createMemo() This function is similar to createComputed(), but unlike createComputed(), createMemo() returns a getter function for the reactive computation, allowing its result to be read synchronously in other reactive contexts or computations. It's used when you want to create a memoized derived value that you can use in multiple places without causing the derivation function to run more than necessary.
createResource() This function is designed to handle the results of asynchronous requests. It is useful for managing states that involve fetching data from an API, a database, or other external data sources.
createStore() This function provides a structured interface for managing tree-like collections of signals. It's used to handle complex state management scenarios.
createSelector() Creates a conditional signal that only notifies subscribers when entering or exiting their key matching the value. Useful for delegated selection state.

In the code example below an example of creating three signals, that are used in JSX, as JSX updaters is demonstrated (i.e., {getCount()}, {getDoubleCount()}, {getStore.tripleCount}).

Note the following two points:

  • When the signal is changed, and it sends out updates, the JSX is updated
  • The update does not happen because the JSX is run again, this happens because SolidJS maintains a fine grained relationship between the signal and the location in the JSX that requires the update.

4 : Generic Derived Signals

In this section, we investigate generic derived signals in SolidJS. We'll learn how to create them and discuss the performance implications of using a generic derived signal v.s. helper functions like createMemo().

4.1 - Using Generic Derived Signals

A generic derived signal occurs when you create a function that accesses a signals getter (ex., createSignal() or createStore() getter) from within the functions scope. This function can then be invoked as a JSX update.

Consider this example:

4.2 - Generic Derived Signals Performance Considerations

You should be aware a generic derived signal function is invoked everywhere it is referenced in JSX when the signal that it houses changes, always! The multiple calls could obviously become a performance issue if the generic derived signal was doing some intensive computing. In the code example below note how many times the console.log() is invoked (i.e., twice because the function is invoked twice in the JSX).

If performance matters, you should replace generic derived signals with createMemo() effects. See createMemo() in the effect section. Using a createMemo() instead of a generic derived signal would only invoke the function once regardless of how many times it is referenced in the JSX, and invoked twice every time the signal contain in the generic derived signal is updated.

5 : createResource() Signal

Add...

5.1 - Using createResource()

6 : createStore() Signal

In this section, we delve into createStore, a pivotal tool in SolidJS for managing complex state. We'll explore its usage in tracking granular changes within deeply nested objects and arrays, illuminating how this powerful function underpins efficient and reactive state management in SolidJS applications.

6.1 - What is a SolidJS store?

A SolidJS store is essentially a sophisticated reactive state container, similar in nature to a createSignal() but with an enhanced ability to handle complex and structured data. It's specifically designed for managing intricate state, particularly when dealing with nested or interconnected values.

The beauty of a SolidJS store lies in its simplicity for handling such complexities. By providing a structured interface for both accessing and updating state, it simplifies the process of state management, making it more manageable and less error-prone.

At its core, a SolidJS store is simply an object brimming with reactive values — each value akin to those created by the createSignal() function. So don't overcomplicate SolidJS stores. Think of them as an organized collection of createSignal() values, offering a higher level of state management.

6.1 - Using the createStore()

6.2 - Why use createStore() instead of createSignal()

The createSignal() function in SolidJS tracks changes to primitive values (booleans, strings, numbers) and reference changes to objects and arrays. When creating a createSignal() value from an array or object, the signal will notify all JSX updates and effects each time there's a change, which happens every time the signal is set. This is because createSignal() keeps track of changes in the object or array references but not individual changes within them.

const [signal, setSignal] = createSignal({ key:"value" }); 
setSignal({ key: "new value" }); // This will trigger updates because the object reference has changed
setSignal({ key: "value" }); // So, will this, object reference changed

If you want to monitor values within arrays or objects, createStore() should be used instead. Stores in SolidJS create a hierarchy or nested object of signals that can each be individually tracked and updated. This means that a change to a specific property of an object will only trigger updates and effects that are tracking that one property, and not those tracking the entire object.

const store = createStore({ key: "value", keyA: "valueA" });
// Updating a property inside the store
setStore("key", "new value"); // Only notifies JSX updates and effects tracking 'key'

In this example, updates and effects that are tracking the key property will be notified of the change, rather than every component that's watching the entire object. This provides a more granular level of control over how changes affect your application.

6.3 - Getting values from a createStore() Signal

Accessing values in a store created with createStore in SolidJS is straightforward and intuitive. The store object behaves much like a regular JavaScript object for reading values. The createStore() function returns a tuple. The first element of the tuple is a reference to the object that represents the store's current state.

Here's a code example to illustrate:

6.4 - Setting values from a createStore() Signal

6.5 - Setting values from a createStore() Signal using produce()

6.6 - Setting values from a createStore() Signal using reconcile()

6.7 - Deleting Objects in a createStore() Signal using reconcile()

6.8 - Get Only the Underlying Data from a createStore() Signal using unwrap()

6.9 - Using a Custom Getter function in an object within a createStore() Signal

7 : createEffect() Effect

Add...

7.1 - What is createEffect()?

SolidJS effects provide a mechanism for performing side effects in response to changes in reactive dependencies (i.e., signals).

In its essence, an effect is a function that runs when its dependencies change. SolidJS, with its fine-grained reactivity system, only triggers effects when the specific dependencies involved change, ensuring optimal performance. This behavior is similar to the React Hook useEffect, yet, unlike React, SolidJS tracks dependencies automatically, relieving you from the task of manually specifying them.

Keep In Mind:

  1. Effects in SolidJS always run once by default to initialize and set up any side effects based on the initial values of their dependencies. This allows the effect to properly respond to any changes in its dependencies from the very beginning. By running the effect once during its initial setup, you ensure that any side effects, such as DOM manipulation, event listeners, or network requests, are appropriately established based on the current state of the component or the application. This initial run allows the effect to synchronize its side effects with the current state of the reactive data. After the initial run, the effect will run again whenever any of its dependencies change. This helps to keep the side effects in sync with the changing data and ensures that the effect's behavior is consistent across the lifetime of the component. In summary, effects in SolidJS run once by default to:
    1. Initialize and set up side effects based on the initial values of their dependencies.
    2. Synchronize side effects with the current state of the reactive data.
    3. Ensure consistent behavior of the effect as the component state or application data changes.

7.2 - Deciphering the createEffect() TypeScript Declaration

function createEffect<T>(fn: (v: T) => T, value?: T):

This declaration can be broken down as follows:

  • createEffect is a generic function, denoted by <T>. The T is a placeholder for whatever type you pass to the function when you use it.

  • The function accepts two parameters:

    1. fn: A function that takes a single argument v of type T and returns a value of the same type T.
    2. value: An optional parameter of type T.

  • The function returns void, meaning it does not return anything.

However, the typical usage of createEffect is to pass in a function with no arguments, which performs some operation based on reactive values. When those reactive values change, the function passed to createEffect is run again. Here's an example:

7.3 - Using createEffect()

import { createSignal, createEffect } from "solid-js";
import type { Component } from "solid-js";

const MyComponent: Component = () => {
  // Create a signal (reactive state) with an initial value of 0
  // `count` is a getter function to access the value, and `setCount` is a setter function to change the value
  const [count, setCount] = createSignal(0);

  // Create an effect that runs whenever its dependencies change.
  // In this case, it depends on the `count` signal, so it runs whenever `count` changes
  createEffect(() => {
    // Log the current value of `count` to the console
    console.log(`Count changed to ${count()}`);
  });

  // Return a button that increments `count` by 1 whenever it is clicked
  return (
    <button onClick={() => setCount(count() + 1)}>
      Change count Signal by 1
    </button>
  );
}

Inside MyComponent, we create a reactive state or signal named count with an initial value of 0. The createSignal function returns a getter-setter pair (a tuple), which we destructure into count (getter) and setCount (setter).

Next, we establish an effect using createEffect. This effect logs the current value of count to the console. The function passed to createEffect is executed every time count changes because count is a dependency of this effect.

Finally, we return a button from the MyComponent function. When this button is clicked, it increments the count signal by 1 using setCount.

As a result, each time you click the button, it increases the count signal value by 1, and the console logs the new value of count. This demonstrates the reactive nature of SolidJS, as changes to the state (the count signal) automatically trigger effects (the console log).

8 : createMemo Effect

Add...

8.1 - Using the createMemo()

8.1 - What is the different between createMemo() and a Derived Signal?

9 : Components In-depth

Add...

9.1 - A Components LifeCycle

10 : JSX In-depth

Add...

10.1 - Differences between HTML and JSX

In the context of SolidJS, here are the key differences between HTML and JSX:

  1. Self-Closing Tags: In HTML, certain tags, known as void elements, do not require a closing tag. Examples include <input>, <br>, and <img>. JSX, however, does not have void elements - all elements must either have a closing tag or be self-closed. So, <input> in HTML would be <input /> in JSX.

  2. JavaScript Expressions: JSX allows you to embed JavaScript expressions within braces {}. This is a key feature that enables React, SolidJS, and other libraries to render dynamic content. HTML, being a markup language, doesn't natively support JavaScript expressions in the same way.

  3. Single Element Return: In JSX, each component must return a single root element. If you want to return multiple elements without wrapping them in a div, you can use a Fragment, denoted by <></>. HTML doesn't have this limitation.

  4. Comments and Special Tags: JSX does not support HTML comments (<!--...-->) or unique tags like <!DOCTYPE>. In JSX, comments are written using the JavaScript comment syntax {/*...*/}.

  5. CamelCase property naming convention: JSX uses the camelCase naming convention for attributes and props, while HTML uses kebab-case. For instance, the HTML attribute tabindex is written as tabIndex in JSX.

  6. Event Handlers: In HTML, event handlers are added as attributes and written in all lowercase (e.g., <button onclick="handleClick()">). In JSX, event handlers are written in camelCase and are passed the actual function, not a string (e.g., <button onClick={handleClick}>).

  7. Style Attribute: In HTML, the style attribute takes a string of CSS properties (e.g., <div style="color: blue; font-size: 16px">). In JSX, the style attribute takes an object where the keys are camelCased versions of the CSS property names (e.g., <div style={{ color: 'blue', fontSize: '16px' }}>).

These differences make JSX a powerful tool for creating dynamic UIs with SolidJS and other similar JavaScript libraries. However, it also means that developers need to adjust their syntax slightly when moving from writing HTML to JSX.

11 : SolidJS Conventions

Add...

11.1 - Global State v.s. Component State

11.2 - Labeling Signals

11.1 - Labeling Effects