Solid-js

A declarative, efficient, and flexible JavaScript library for building user interfaces

You can try some of the examples mentioned below in the official tutorial, some of which are referenced and simplified in this article

This article covers some of solid’s main content, but for more details, refer to the Solid API documentation

Solid is similar to React and uses JSX syntax, but unlike React, components are only initialized once. Instead of re-rendering the entire component when state changes, This is similar to setup for Vue3

Why Solid

Solid’s website gives the following reasons

  • High performance – Consistently top recognized BENCHMARKS for UI speed and memory utilization
  • Powerful – composable reaction primitives combine with the flexibility of JSX
  • Pragmatic – Reasonable and tailored apis make development fun and easy
  • Productivity – Ergonomics and familiarity make it a breeze to build something simple or complex

The main advantage

Small package size – compiles to direct DOM operations, no virtual DOM, minimal runtime (similar to Svelte), Easy to use as a standalone webComponent embedded in other applications – React like experience for quick use

Quick start

New project

npx degit solidjs/templates/js my-app
cd my-app
npm i
npm run dev
Copy the code

Basic example

Here we render the App component into the Body container

Here modify the default example and try it from scratch

// App.JSX
import { render } from "solid-js/web";

function App() {
  return (
    <div>Solid My App</div>
  );
}
Component declarations can also be used directly with arrow functions
/* const App = ()=> (
      
Solid My App
); * /
render(() = > <App />.document.querySelector("body")); Copy the code

Doesn’t it look very familiar, like React, very comfortable

Import component, pass component, props

React is used in a similar way, but you cannot deconstruct props or it will become unreactive

// App.JSX
import { render } from "solid-js/web";

import Component1 from "./Component1.jsx";

function App() {
  return (
    <div>
      Solid My App
      <Component1 text={"component1"} >
        <div>children</div>
      </Component1>
    </div>
  );
}

render(() = > <App />.document.querySelector("body"));

// Component1.jsx
export default function Component1(props) {
  return (
    <div>
      {props.text}
      {props.children}
    </div>)}Copy the code

reactive

createSignal

Signal is the most basic reactive unit in Solid. This function is similar to useState in React, but the return function is used to get the value that was obtained by calling it, rather than directly, as React does. Here is a basic Counter example

import { createSignal } from "solid-js";

export default function Counter() {
  const [count, setCount] = createSignal(0);
  return (
    <button onClick={()= > setCount(count() + 1)}>
      {count()}
    </button>)}Copy the code
createMemo

CreateMemo is used to generate read-only derived values, similar to computed in Vue, which, like above, requires a call to get the value

import { createSignal, createMemo } from "solid-js";

export default function Counter() {
  const [count, setCount] = createSignal(0);
  // The square of count is derived from count and updated automatically when dependencies change
  const countPow2 = createMemo(() = > count() ** 2);
  return (
    <button onClick={()= > setCount(count() + 1)}>
      {count()} | {countPow2()}
    </button>)}Copy the code
createEffect

CreateEffect is typically used for side effects. It runs side effects when state changes. It is similar to useEffect in React but automatically collects dependencies without explicitly declaring them, just like watchEffect in Vue

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

export default function Counter() {
  const [count, setCount] = createSignal(0);
  // This side effect is rerun whenever the dependency changes
  createEffect(() = > console.log(count()));
  return (
    <button onClick={()= > setCount(count() + 1)}>
      {count()}
    </button>)}Copy the code

If you need to explicitly declare dependencies, refer to Solid createEffect to explicitly declare dependencies

batch

Solid’s reactivity is synchronous, meaning that the DOM is updated on the next line after any changes. In most cases, this is perfectly fine, because Solid’s granular rendering is just the propagation of updates in a reactive system. “Rendering” two unrelated changes doesn’t actually mean wasted work. What if the change is related? Solid’s Batch assistant allows multiple changes to be queued and then applied simultaneously before notifying the observer. Signal values updated in batch processing are not committed until they are completed. Refer to the following example of not using Batch

import { render } from "solid-js/web";
import { createSignal, batch } from "solid-js";

const App = () = > {
  const [firstName, setFirstName] = createSignal("John");
  const [lastName, setLastName] = createSignal("Smith");
  const fullName = () = > {
    console.log("Running FullName");
    return `${firstName()} ${lastName()}`
  } 
  const updateNames = () = > {
    console.log("Button Clicked");
    setFirstName(firstName() + "n");
    setLastName(lastName() + "!");
  }
  
  return <button onClick={updateNames}>My name is {fullName()}</button>
};

render(App, document.getElementById("app"));
Copy the code

In this example, we update two states when the button is clicked, it triggers two updates, and you can see the log in the console, so let’s modify updateNames to package the set call into a batch.

 const updateNames = () = > {
    console.log("Button Clicked");
    batch(() = > {
      setFirstName(firstName() + "n");
      setLastName(lastName() + "!"); })}Copy the code

Now, only one update is triggered for the same element

style

Create a style file for use below

/* main.module.css */
.container {
  width: 100px;
  height: 100px;
  background-color: green;
}
.text {
  font-size: 20px;
  color: red;
}
Copy the code
The basic use

The style is also very similar to React, except that it uses a class instead of a className

import style from "./main.module.css";

export default function Container() {
  return (
    <div class={style.container}>
      <span class={style.text}>text</span>
    </div>)}Copy the code
classList

The following is an example of a click-to-switch class

import style from "./main.css";
import { createSignal } from "solid-js";

export default function Container() {
  const [hasTextClassName, setHasTextClassName] = createSignal(false);
  return (
    <div 
      classList={{[style.container]: true[style.text]: hasTextClassName()}}onClick={() = >setHasTextClassName(! hasTextClassName()) } > text</div>)}Copy the code

Basic control flow

Much of the control flow can do the same with JSX, but with better performance than JSX, Solid can optimize it even more

Fallback is displayed after a failure

For

Simple reference keying cycle control flow.

export default function Container() {
  return (
    <div>
      <For 
        each={} [1,2,3,4,5,6,7,8,9,10] 
        fallback={<div>Failed</div>}
      >
        {(item) => <div>{item}</div>}
      </For>
    </div>)}Copy the code
Show

The Show control flow is used to conditionally render portions of the view. It is similar to the ternary operator (a? B: c), but perfect for templated JSX.

import { createSignal } from "solid-js";

export default function Container() {
  const [count, setCount] = createSignal(10);
  return (
    <div>{/* Render when count > 5 */}<Show 
        when={count() > 5} 
        fallback={<div>Failed</div>}
      >
        <div>content</div>
      </Show>
    </div>)}Copy the code
Switch

Switch is useful when there are more than two mutually exclusive conditions. It can be used for simple routing and things like that.

import { createSignal } from "solid-js";

export default function Container() {
  const [count, setCount] = createSignal(10);
  return (
    <div>
      <Switch fallback={<div>Failed</div>} ><Match when={count() >5} ><div>count > 5</div>
        </Match>
        <Match when={count() < 5} >
          <div>count < 5</div>
        </Match>
      </Switch>
    </div>)}Copy the code
Index

The non-index iteration loop controls the flow. If you’re iterating over something like an object instead of an array, use Index

export default function Container() {
  return (
    <div>
      <Index 
        each={{
          name: "name",
          gender: "male",
          age: 100.address: "address".}}fallback={<div>Failed</div>}
      >
        {(item) => <div>{item}</div>}
      </Index>
    </div>)}Copy the code
ErrorBoundary

Error boundary


function ErrorComponent() {
  // Throw an error
  throw new Error("component error");
  return (
    <div>content</div>)}export default function Container() {
  return (
    <ErrorBoundary fallback={<div>Failed</div>} ><ErrorComponent></ErrorComponent>
    </ErrorBoundary>)}Copy the code
Portal

The React Portal works in the same way as the React Portal to render elements outside of components. This is a good example for modal Windows, message prompts, etc. : Render elements directly under the body

export default function Container() {
  return (
    <Portal mount={document.querySelector("body")} >
      <div>content</div>
    </Portal>)}Copy the code
Other control flows

Refer to API documentation

The life cycle

Mount: onMount; uninstall: onCleanup

import { onMount, onCleanup } from "solid-js";
export default function Container() {
  onMount(() = > {
    console.log("onMount");
  });
  onCleanup(() = > {
    console.log("onCleanup");
  });
  return (
    <div>content</div>)}Copy the code

The binding

ref

Ref is used to get the DOM node itself

export default function Container() {
  let $container;
  return (
    <div ref={$container}>
      container
    </div>)}Copy the code

<div ref={props. Ref}></div>

spread

Sometimes your components and elements accept a variable number of attributes, and it makes sense to pass them as objects rather than individually. This is especially true when wrapping DOM elements in components, which is a common practice when creating design systems to do this, we use extension operators… . We can pass an object with a variable number of attributes:

function Info(props) {
  return (
    <div>
      <div>{props.name}</div>
      <div>{props.speed}</div>
      <div>{props.version}</div>
      <div>{props.website}</div>
    </div>
  );
}

const pkg = {
  name: "solid-js".version: 1.speed: "⚡".website: "https://solidjs.com"};function Main() {
  return (
    <Info 
      name={pkg.name}
      version={pkg.version}
      speed={pkg.speed}
      website={pkg.website}
    >
    </Info>)}/ / is equivalent to
function Main() {
  return (
    <Info 
      {. pkg}
    >
    </Info>)}Copy the code

Store/nested response

One of the reasons for the fine-grained reactivity in Solid is that it can handle nested updates independently. You can have a list of users, and when we update a name, we update only one location in the DOM, without differentiating the list itself. Few (even reactive) UI frameworks can do this.

createStore

Used to create a store that can be used to precisely nest reactions

This function creates a signal tree as a proxy, allowing individual values in nested data structures to be tracked independently. The create function returns a read-only proxy object and a setter function

The For tag above is useful here, because using JSX directly refreshes the entire expression without fine-grained updates

Let’s take a look at an example without using store. Here we use some sample data and iteratively render with the For tag. Click the check box to toggle its selection state

import { render } from "solid-js/web";
import { For, createSignal } from "solid-js";

const App = () = > {
  const [state, setState] = createSignal(
    {
      Initialize an array of objects with the id, text, completed properties
      todos: [{id: 1.text: 1.completed: false},
        {id: 2.text: 2.completed: false},
        {id: 3.text: 3.completed: false},
        {id: 4.text: 4.completed: false}}]);// Modify the selection state of the checked box
  const toggleTodo = (id) = > {
    setState({
      todos: state().todos.map((todo) = >( todo.id ! == id ? todo : { ... todo,completed: !todo.completed }
      ))
    });
  }

  return (
    <>
      <For each={state().todos}>
        {(todo) => {
          const { id, text } = todo;
          console.log(`Creating ${text}`)
          return <div>
            <input
              type="checkbox"
              checked={todo.completed}
              onchange={[toggleTodo, id]} / >
            <span
              style={{ "text-decoration": todo.completed ? "line-through" : "none}} ">{text}</span>
          </div>
        }}
      </For>
    </>
  );
};

render(App, document.getElementById("app"));
Copy the code

Will find the console with the click on each output, this is because the destruction of reconstruction of the element, every time we just change a property, but to rebuild elements, it is a waste If we use the store can have more accurate reaction, without the need to rebuild elements, only in the original position update Modify the code above for the following code, Run it again and click, and you’ll see that elements are no longer destroyed and rebuilt, which ensures high performance

The specific use of store is explained below

import { render } from "solid-js/web";
import { For } from "solid-js";
import { createStore } from "solid-js/store";

const App = () = > {
  const [store, setStore] = createStore(
    {
      Initialize an array of objects with the id, text, completed properties
      todos: [{id: 1.text: 1.completed: false},
        {id: 2.text: 2.completed: false},
        {id: 3.text: 3.completed: false},
        {id: 4.text: 4.completed: false}}]);// Modify the selection state of the checked box
  const toggleTodo = (id) = > {
    setStore(
      "todos".(t) = > t.id === id, 
      'completed'.(completed) = >! completed ); };return (
    <>
      <For each={store.todos}>
        {(todo) => {
          const { id, text } = todo;
          console.log(`Creating ${text}`)
          return <div>
            <input
              type="checkbox"
              checked={todo.completed}
              onchange={[toggleTodo, id]} / >
            <span
              style={{ "text-decoration": todo.completed ? "line-through" : "none}} ">{text}</span>
          </div>
        }}
      </For>
    </>
  );
};

render(App, document.getElementById("app"));
Copy the code

CreateSignal is changed to createStore. Because createStore returns a read-only proxy directly, not a Getter, it does not need to be called. Using signal to set the value directly is simply a matter of iterating over the raw data, changing and generating new data. In most applications, this is the case, but Solid has some optimization policies for this situation. Store values can be set like react setState, Let objects shallow merge but here we’re using a different approach solid supports, which lets Solid know what we’ve changed in detail so that fine-grained updates can be made. In the example above we changed toggleTodo to look like this

  (id) => {
    setStore(
      "todos".(t) = > t.id === id, 
      'completed'.(completed) = >! completed ); };Copy the code

This is the path syntax in Solid. Refer to the official API documentation and paths can be string keys, key arrays, iterated objects ({from, to, by}), or filter functions. This provides incredible expressiveness for describing state changes.

const [state, setState] = createStore({
  todos: [{task: 'Finish work'.completed: false }
    { task: 'Go grocery shopping'.completed: false }
    { task: 'Make dinner'.completed: false}}]); setState('todos'[0.2].'completed'.true);
/ / {
// todos: [
// { task: 'Finish work', completed: true }
// { task: 'Go grocery shopping', completed: false }
// { task: 'Make dinner', completed: true }
/ /]
// }

setState('todos', { from: 0.to: 1 }, 'completed'.c= >! c);/ / {
// todos: [
// { task: 'Finish work', completed: false }
// { task: 'Go grocery shopping', completed: true }
// { task: 'Make dinner', completed: true }
/ /]
// }

setState('todos'.todo= > todo.completed, 'task'.t= > t + '! ')
/ / {
// todos: [
// { task: 'Finish work', completed: false }
// { task: 'Go grocery shopping! ', completed: true }
// { task: 'Make dinner! ', completed: true }
/ /]
// }

setState('todos', {}, todo= > ({ marked: true.completed: !todo.completed }))
/ / {
// todos: [
// { task: 'Finish work', completed: true, marked: true }
// { task: 'Go grocery shopping! ', completed: false, marked: true }
// { task: 'Make dinner! ', completed: false, marked: true }
/ /]
// }
Copy the code
produce

Solid strongly recommends using shallow immutable mode to update status. By separating reads and writes, we can better control the responsiveness of the system without the risk of losing track of proxy changes as we move through the component layer. However, sometimes mutations are easier to understand. For this reason, immer-inspired Solid provides a produce for store variability to follow the example above, Modify toggleTodo for

  const toggleTodo = (id) = > {
    setStore(
      "todos", 
      produce((todos) = > {
        todos.push({ id: ++todoId, text, completed: false}); })); };Copy the code
More about store

asynchronous

lazy

In applications where certain components are loaded only when used, they are packaged separately and loaded on demand at certain times, Solid also provides methods to use lazy instead of normal static import statements

import Component1 from "./Component1.jsx";
Copy the code

Replace with

const Component1 = lazy(() = > import("./Component1"));
Copy the code

Because lazy accepts only the Promise of a Solid component as an argument, you can also attach some behavior at load time

createResource

Create a signal that can manage asynchronous requests. Fetcher is an asynchronous function that takes the return value provided by sourceIf and returns a Promise, whose resolution value is set in the resource. Fetcher is not reactive, so use the optional first parameter if you want it to run more than once. If the source resolves to false, NULL, or undefined, it is not fetched. Official website online trial

const [data, { mutate, refetch }] = createResource(getQuery, fetchData);
/ / get the value
data();
// Check if it is loading
data.loading;
// Check if there is an error
data.error;
// Set the value directly
mutate(optimisticValue);
// Refresh and request again
refetch();
Copy the code
Suspense

Suspense works with asynchronous components to display the content given in fallback while it is not finished loading

const Component1 = lazy(() = > import("./Component1"));

export default function App() {
  return (
    <Suspense fallback={<div>loading...</div>} ><Component1></Component1>
    </Suspense>)}Copy the code
More asynchronous content

conclusion

  • Solid is high performance and has a very small packaging volume, suitable for packaging as independent modules embedded in other projects
  • Solid is easy to get started with, and fits into the habits of React or Vue3 developers
  • JSX in Solid returns DOM elements directly, which is intuitive and pure
  • Solid has places where it needs to use what it specifies to achieve high performance, and high performance is not free
  • Solid is not used much at present, and the ecology needs to be improved