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.
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:
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.
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.
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.
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:
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.
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:
In this section, we focus on createSignal()
, the
foundational reactive concept within SolidJS.
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:
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:
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:
initialValue: T
- This is the initial value of the
signal. Since T
can be any type, this initial value
can be of any type.
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]
:
get: () => T
- This is a getter function that
returns the current value of the signal.
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.
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.
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.
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.
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:
createSignal()
Objects and Arrays are deeply equal
In this section, we unravel the mechanics of JSX updates within SolidJS (i.e. using signals within JSX)
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:
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()
.
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:
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.
Add...
createResource()
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.
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.
createStore()
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.
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:
createStore()
SignalcreateStore()
Signal using
produce()
createStore()
Signal using
reconcile()
createStore()
Signal using
reconcile()
createStore()
Signal using unwrap()
createStore()
Signal
Add...
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:
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:
fn
: A function that takes a single argument
v
of type T
and returns a value of
the same type T
.
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:
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).
Add...
createMemo()
Add...
Add...
In the context of SolidJS, here are the key differences between HTML and JSX:
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.
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.
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.
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 {/*...*/}
.
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.
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}>
).
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.
Add...