Contenido
Hooks
Carats te da estado y un ciclo de vida en el cliente mediante el paquete @carats/hooks.
En Carats no hay useEffect ni array de dependencias. El cuerpo de un hook se ejecuta en el servidor y en el cliente, así que todo lo que toque el DOM o la red debe vivir dentro de una llamada de hook.
El hook use()
La función use ofrece gestión de estado. A diferencia de los hooks de React, no es reactiva: asignar un valor no vuelve a renderizar nada, así que nada de la página se actualiza por sí solo. La otra mitad de la pareja es subscribe, que es el modo en que un cambio se vuelve visible. Lee el valor mientras renderizas, escribe el DOM desde un suscriptor y cambia el valor desde donde sea.
Uso básico
import { use } from '@carats/hooks';
export default function Counter(props: { start?: number }) {
const count = use(props.start ?? 0);
// get() is a plain read, safe on the server and in the browser
return <p class="counter">{count.get()}</p>;
}
Como no hay rerenderizado, lee el valor mientras renderizas y escribe el DOM desde un suscriptor:
import { use, afterMount } from '@carats/hooks';
export default function Counter() {
const count = use(0);
afterMount(() => {
const inc = document.getElementById('inc');
const dec = document.getElementById('dec');
const label = document.getElementById('count');
const unsub = count.subscribe((value) => {
if (label) label.textContent = String(value);
});
const increment = () => count.set((n) => n + 1);
const decrement = () => count.set((n) => n - 1);
inc?.addEventListener('click', increment);
dec?.addEventListener('click', decrement);
return () => {
unsub();
inc?.removeEventListener('click', increment);
dec?.removeEventListener('click', decrement);
};
});
return (
<div class="counter">
<p id="count">{count.get()}</p>
<button id="inc">+</button>
<button id="dec">-</button>
</div>
);
}
Interfaz de estado
La función use devuelve un objeto State con los métodos get, set y subscribe. set acepta un valor nuevo o una fábrica que recibe el actual.
type Getter<T> = () => T;
type Factory<T> = (currentValue: T) => T;
type Setter<T> = {
(fn: Factory<T>): T;
(value: T): T;
};
type Subscriber<T> = (value: T) => void;
type Subscribe<T> = (subscriber: Subscriber<T>) => () => void;
type State<T> = {
get: Getter<T>;
set: Setter<T>;
subscribe: Subscribe<T>;
};
declare function use<T>(): State<T>;
declare function use<T>(initialState: T): State<T>;
set devuelve el valor nuevo, así que puedes usar el resultado directamente cuando necesites el número actualizado enseguida.
`use` también se puede llamar fuera de un componente, que es la forma idiomática de compartir un único dato de estado entre páginas.
El método subscribe
import { use } from '@carats/hooks';
const state = use(0);
// Returns the unsubscribe function
const unsub = state.subscribe((value) => {
console.log('Count changed to:', value);
});
subscribe devuelve una función para darse de baja. Llámala desde la llamada clear de un hook afterMount o beforeMount; de lo contrario, el suscriptor sigue registrado después de que la página se sustituya.
Mantén separadas las dos cosas. Un manejador cambia el estado y un suscriptor escribe el DOM. Un manejador que además escriba el DOM da al elemento dos fuentes de verdad, y en cuanto se pierde una de ellas la página se contradice.
// Wrong: two places write to the DOM, so they can disagree
const onClick = () => {
count.set((n) => n + 1);
if (label) label.textContent = String(count.get());
};
// Right: the handler only changes state, the subscriber only writes the DOM
const unsub = count.subscribe((value) => {
if (label) label.textContent = String(value);
});
const onClick = () => count.set((n) => n + 1);
La llamada afterMount()
La llamada afterMount se ejecuta solo en el cliente, después de que se haya despachado el evento load. Úsala para llegar a elementos del DOM que creó el mismo componente, como la etiqueta de un contador o un botón. Puede devolver una llamada clear, que se ejecuta antes del siguiente paso de hidratación.
import { afterMount } from '@carats/hooks';
export default function MyComponent() {
afterMount(() => {
const button = document.getElementById('my-button');
const onClick = () => console.log('Button clicked!');
button?.addEventListener('click', onClick);
return () => button?.removeEventListener('click', onClick);
});
return <button id="my-button">Click me</button>;
}
Conserva una referencia al manejador exacto que registraste. Pasar una función de flecha nueva a removeEventListener crea una identidad de función distinta, así que el escuchador nunca llega a quitarse.
La llamada beforeMount()
La llamada beforeMount se ejecuta de inmediato en el cliente, sin esperar al evento load. Úsala para trabajo que no necesite el marcado del propio componente, como temporizadores, escuchadores de teclado o suscripciones. Igual que afterMount, puede devolver una llamada clear.
import { beforeMount } from '@carats/hooks';
export default function MyComponent() {
beforeMount(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer);
});
return <div>My Component</div>;
}
Recurre a afterMount cuando el trabajo dependa del marcado que produjo este componente, porque se ejecuta tras el evento load y el documento ya está completo. Recurre a beforeMount cuando no dependa de él, como un temporizador o un escuchador de teclado, para que el trabajo empiece antes.
clearHydrations()
Limpia todas las llamadas de hidratación registradas ejecutando sus llamadas clear. Es útil sobre todo en pruebas y al desmontar una aplicación a mano.
import { clearHydrations } from '@carats/hooks';
// Runs every registered clear callback and empties the registry
await clearHydrations();
Ejemplo completo
import { use, beforeMount, afterMount } from '@carats/hooks';
import './counter.sass';
interface CounterProps {
initialValue?: number;
}
export default function Counter(props: CounterProps) {
const count = use(props.initialValue ?? 0);
// beforeMount runs immediately and does not need this component's markup
beforeMount(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'ArrowUp') count.set((n) => n + 1);
if (e.key === 'ArrowDown') count.set((n) => n - 1);
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
});
afterMount(() => {
const label = document.getElementById('count');
const inc = document.getElementById('inc');
const dec = document.getElementById('dec');
const unsub = count.subscribe((value) => {
if (label) label.textContent = String(value);
});
const increment = () => count.set((n) => n + 1);
const decrement = () => count.set((n) => n - 1);
inc?.addEventListener('click', increment);
dec?.addEventListener('click', decrement);
return () => {
unsub();
inc?.removeEventListener('click', increment);
dec?.removeEventListener('click', decrement);
};
});
return (
<div class="counter">
<h2>Counter: <span id="count">{count.get()}</span></h2>
<button id="inc" type="button">+</button>
<button id="dec" type="button">-</button>
</div>
);
}
Definiciones de tipos
type MaybePromise<T> = T | Promise<T>;
type ClearCallback = () => MaybePromise<void>;
type HydrationCallback = () => MaybePromise<ClearCallback | void>;
declare function afterMount(callback: HydrationCallback): void;
declare function beforeMount(callback: HydrationCallback): void;
declare function clearHydrations(): Promise<void>;
declare function use<T>(): State<T>;
declare function use<T>(initialState: T): State<T>;