Introducción a la biblioteca sólida de JavaScript | Programar Plus

Solid es una biblioteca JavaScript reactiva para crear interfaces de usuario sin un DOM virtual. Compila las plantillas hasta los nodos DOM reales una vez y envuelve las actualizaciones en reacciones detalladas de modo que cuando se actualiza el estado, solo se ejecuta el código relacionado.

De esta manera, el compilador puede optimizar el renderizado inicial y el tiempo de ejecución optimiza las actualizaciones. Este enfoque en el rendimiento lo convierte en uno de los marcos de JavaScript mejor calificados.

Sentí curiosidad por él y quería probarlo, así que dediqué un tiempo a crear una pequeña aplicación de tareas pendientes para explorar cómo este marco maneja los componentes de renderizado, el estado de actualización, la configuración de tiendas y más.

Aquí está la demostración final si no puede esperar para ver el código final y el resultado:

Empezando

Como la mayoría de los marcos, podemos comenzar instalando el paquete npm. Para usar el marco con JSX, ejecute:

npm install solid-js babel-preset-solid

Entonces, necesitamos agregar babel-preset-solid a nuestro archivo de configuración Babel, webpack o Rollup con:

"presets": ["solid"]

O si desea montar una aplicación pequeña, también puede usar una de sus plantillas:

# Create a small app from a Solid template
npx degit solidjs/templates/js my-app
 
# Change directory to the project created
cd my-app
 
# Install dependencies
npm i # or yarn or pnpm
 
# Start the dev server
npm run dev

Hay compatibilidad con TypeScript, por lo que si desea iniciar un proyecto de TypeScript, cambie el primer comando a npx degit solidjs/templates/ts my-app.

Crear y renderizar componentes

Para renderizar componentes, la sintaxis es similar a React.js, por lo que puede parecer familiar:

import { render } from "solid-js/web";
 
const HelloMessage = props => <div>Hello {props.name}</div>;
 
render(
 () => <HelloMessage name="Taylor" />,
 document.getElementById("hello-example")
);

Necesitamos comenzar importando el render función, luego creamos un div con algo de texto y un prop, y llamamos render, pasando el componente y el elemento contenedor.

Este código luego se compila en expresiones DOM reales. Por ejemplo, el ejemplo de código anterior, una vez compilado por Solid, se parece a esto:

import { render, template, insert, createComponent } from "solid-js/web";
 
const _tmpl$ = template(`<div>Hello </div>`);
 
const HelloMessage = props => {
 const _el$ = _tmpl$.cloneNode(true);
 insert(_el$, () => props.name);
 return _el$;
};
 
render(
 () => createComponent(HelloMessage, { name: "Taylor" }),
 document.getElementById("hello-example")
);

Solid Playground es bastante bueno y muestra que Solid tiene diferentes formas de renderizar, incluido el lado del cliente, el lado del servidor y el lado del cliente con hidratación.

Seguimiento de valores cambiantes con señales

Solid usa un gancho llamado createSignal que devuelve dos funciones: un captador y un definidor. Si está acostumbrado a usar un marco como React.js, esto puede parecer un poco extraño. Normalmente, esperaría que el primer elemento sea el valor en sí; sin embargo, en Solid, necesitamos llamar explícitamente al getter para interceptar donde se lee el valor para rastrear sus cambios.

Por ejemplo, si estamos escribiendo el siguiente código:

const [todos, addTodos] = createSignal([]);

Inicio sesión todos no devolverá el valor, sino una función. Si queremos usar el valor, necesitamos llamar a la función, como en todos().

Para una pequeña lista de tareas, esto sería:

import { createSignal } from "solid-js";
 
const TodoList = () => {
 let input;
 const [todos, addTodos] = createSignal([]);
 
 const addTodo = value => {
   return addTodos([...todos(), value]);
 };
 
 return (
   <section>
     <h1>To do list:</h1>
     <label for="todo-item">Todo item</label>
     <input type="text" ref={input} name="todo-item" id="todo-item" />
     <button onClick={() => addTodo(input.value)}>Add item</button>
     <ul>
       {todos().map(item => (
         <li>{item}</li>
       ))}
     </ul>
   </section>
 );
};

El ejemplo de código anterior mostraría un campo de texto y, al hacer clic en el botón “Agregar elemento”, actualizaría todos con el nuevo elemento y lo mostraría en una lista.

Esto puede parecer bastante similar a usar useState, entonces, ¿en qué se diferencia el uso de un captador? Considere el siguiente ejemplo de código:

console.log("Create Signals");
const [firstName, setFirstName] = createSignal("Whitney");
const [lastName, setLastName] = createSignal("Houston");
const [displayFullName, setDisplayFullName] = createSignal(true);
 
const displayName = createMemo(() => {
 if (!displayFullName()) return firstName();
 return `${firstName()} ${lastName()}`;
});
 
createEffect(() => console.log("My name is", displayName()));
 
console.log("Set showFullName: false ");
setDisplayFullName(false);
 
console.log("Change lastName ");
setLastName("Boop");
 
console.log("Set showFullName: true ");
setDisplayFullName(true);

Ejecutar el código anterior resultaría en:

Create Signals
 
My name is Whitney Houston
 
Set showFullName: false
 
My name is Whitney
 
Change lastName
 
Set showFullName: true
 
My name is Whitney Boop

Lo principal a notar es cómo My name is ... no se registra después de establecer un nuevo apellido. Esto se debe a que en este punto, nada está escuchando cambios en lastName(). El nuevo valor de displayName() solo se establece cuando el valor de displayFullName() cambia, es por eso que podemos ver el nuevo apellido mostrado cuando setShowFullName se establece de nuevo a true.

Esto nos brinda una forma más segura de realizar un seguimiento de las actualizaciones de valores.

Primitivas de reactividad

En esa última muestra de código, presenté createSignal, pero también un par de otras primitivas: createEffect y createMemo.

createEffect

createEffect rastrea las dependencias y se ejecuta después de cada renderizado donde una dependencia ha cambiado.

// Don't forget to import it first with 'import { createEffect } from "solid-js";'
const [count, setCount] = createSignal(0);
 
createEffect(() => {
 console.log("Count is at", count());
});

Count is at... registra cada vez que el valor de count() cambios.

createMemo

createMemo crea una señal de solo lectura que recalcula su valor cada vez que se actualizan las dependencias del código ejecutado. Lo usaría cuando desee almacenar en caché algunos valores y acceder a ellos sin reevaluarlos hasta que cambie una dependencia.

Por ejemplo, si quisiéramos mostrar un contador 100 veces y actualizar el valor al hacer clic en un botón, usando createMemo permitiría que el recálculo ocurra solo una vez por clic:

function Counter() {
   const [count, setCount] = createSignal(0);
   // Calling `counter` without wrapping it in `createMemo` would result in calling it 100 times.
   // const counter = () => {
   //    return count();
   // }
 
   // Calling `counter` wrapped in `createMemo` results in calling it once per update.
// Don't forget to import it first with 'import { createMemo } from "solid-js";'
   const counter = createMemo(() => {
       return count()
   })
 
   return (
       <>
       <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
       <div>1. {counter()}</div>
       <div>2. {counter()}</div>
       <div>3. {counter()}</div>
       <div>4. {counter()}</div>
       <!-- 96 more times -->
       </>
   );
}

Métodos de ciclo de vida

Solid expone algunos métodos del ciclo de vida, como onMount, onCleanup y onError. Si queremos que se ejecute algún código después del renderizado inicial, debemos usar onMount:

// Don't forget to import it first with 'import { onMount } from "solid-js";'
 
onMount(() => {
 console.log("I mounted!");
});

onCleanup es parecido a componentDidUnmount en React: se ejecuta cuando se vuelve a calcular el alcance reactivo.

onError se ejecuta cuando hay un error en el alcance del niño más cercano. Por ejemplo, podríamos usarlo cuando falla la obtención de datos.

Historias

Para crear almacenes de datos, Solid expone createStore cuyo valor de retorno es un objeto proxy de solo lectura y una función de establecimiento.

Por ejemplo, si cambiamos nuestro ejemplo de todo para usar una tienda en lugar de un estado, se vería así:

const [todos, addTodos] = createStore({ list: [] });
 
createEffect(() => {
 console.log(todos.list);
});
 
onMount(() => {
 addTodos("list", [
   ...todos.list,
   { item: "a new todo item", completed: false }
 ]);
});

El ejemplo de código anterior comenzaría registrando un objeto proxy con una matriz vacía, seguido de un objeto proxy con una matriz que contiene el objeto {item: "a new todo item", completed: false}.

Una cosa a tener en cuenta es que el objeto de estado de nivel superior no se puede rastrear sin acceder a una propiedad en él; es por eso que estamos registrando todos.list y no todos.

Si solo nos registramos todoen createEffect, veríamos el valor inicial de la lista, pero no el que aparece después de la actualización realizada en onMount.

Para cambiar los valores en las tiendas, podemos actualizarlos usando la función de configuración que definimos al usar createStore. Por ejemplo, si quisiéramos actualizar un elemento de la lista de tareas pendientes a “completado”, podríamos actualizar la tienda de esta manera:

const [todos, setTodos] = createStore({
 list: [{ item: "new item", completed: false }]
});
 
const markAsComplete = text => {
 setTodos(
   "list",
   i => i.item === text,
   "completed",
   c => !c
 );
};
 
return (
 <button onClick={() => markAsComplete("new item")}>Mark as complete</button>
);

Flujo de control

Para evitar la recreación inútil de todos los nodos DOM en cada actualización cuando se utilizan métodos como .map(), Solid nos permite usar ayudantes de plantilla.

Algunos de ellos están disponibles, como For para recorrer los elementos, Show para mostrar y ocultar elementos de forma condicional, Switch y Match para mostrar elementos que coinciden con una determinada condición, ¡y más!

A continuación, se muestran algunos ejemplos que muestran cómo utilizarlos:

<For each={todos.list} fallback={<div>Loading...</div>}>
 {(item) => <div>{item}</div>}
</For>
 
<Show when={todos.list[0].completed} fallback={<div>Loading...</div>}>
 <div>1st item completed</div>
</Show>
 
<Switch fallback={<div>No items</div>}>
 <Match when={todos.list[0].completed}>
   <CompletedList />
 </Match>
 <Match when={!todos.list[0].completed}>
   <TodosList />
 </Match>
</Switch>

Proyecto de demostración

Esta fue una rápida introducción a los conceptos básicos de Solid. Si desea jugar con él, hice un proyecto de inicio que puede implementar automáticamente en Netlify y clonar en su GitHub haciendo clic en el botón de abajo.

Implementar en Netlify

¡Este proyecto incluye la configuración predeterminada para un proyecto sólido, así como una aplicación Todo de muestra con los conceptos básicos que mencioné en esta publicación para comenzar!

Hay mucho más en este marco de lo que cubrí aquí, así que siéntase libre de consultar los documentos para obtener más información.