Comenzamos con el capítulo final, donde se recopilan los aspectos más importantes a tener en cuenta para poder seguir trabajando con Qwik.
Se añadirán las notas sobre cosas a tener en cuenta, para no tener que andar buscando en el libro página por página.
Como ayuda si se añadirá una mención de que capítulo corresponde a ese concepto, con el objetivo de facilitar el proceso de búsqueda.
Se aprovechará haciendo una comparativa de como se implementa en React, comparándolo con Qwik.
Algunos ejemplos de código de Qwik haciendo una comparativa con React para mostrar como sería para implementar la misma funcionalidad.
El ejemplo más básico con el que trabajamos a la hora de utilizar un componente.
export const HelloWorld = component$(() => {
return <div>Hello world</div>;
});
export function HelloWorld() {
return <div>Hello world</div>;
}
Capítulo donde se trabaja con este concepto:
Componente sencillo con un botón en el que se implementa el evento click
.
export const Button = component$(() => {
return <button onClick$={() => console.log('click')}>Click me</button>;
});
export function Button() {
return <button onClick={() => console.log('click')}>Click me</button>;
}
Capítulos donde se trabaja con este concepto:
Uso de un componentes sencillo con props
para pasar información de manera dinámica a los
componentes y poder reutilizarlos, como podría ser un componente de detalles, botones,...
export const Parent = component$(() => {
const userData = useStore({ count: 0 });
return <Child userData={userData} />;
});
interface ChildProps {
userData: { count: number };
}
export const Child = component$<ChildProps>(({ userData }) => {
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<p>Count: {userData.count}</p>
</>
);
});
export const Parent = (() => {
const [ count, setCount ] = useState<number>(0);
const increment = (() => {
setCount((prev) => prev + 1)
})
return <Child count={count} increment={increment} />;
})
interface ChildProps {
count: number;
increment: () => void;
}
export const Child = ((props: ChildProps) => {
return (
<>
<button onClick={props.increment}>Increment</button>
<p>Count: {props.count}</p>
</>
);
})
Capítulos donde se trabaja con este concepto:
Para gestionar el valor del estado local de una manera sencilla y hacer que se muestre de manera reactiva la información correspondiente a ese valor que queramos mostrar.
export const LocalStateExample = component$(() => {
const count = useSignal(0);
return <div>Value is: {count.value}</div>;
});
export function UseStateExample() {
const [value, setValue] = useState(0);
return <div>Value is: {value}</div>;
}
Capítulo donde se trabaja con este concepto:
Ejemplo del típico ejemplo de componente en el que añadimos un elemento, para contabilizar el estado de ese contador mediante un hook acorde a ello.
export const Counter = component$(() => {
const count = useSignal(0);
return (
<>
<p>Value is: {count.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</>
);
});
export function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>Value is: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</>
);
}
Capítulos donde se trabaja con este concepto:
Ejemplo de un componente de reloj, donde hacemos uso de los conocimientos de crear componentes, trabajo de estados y uso de los ciclos de vida para crear un reloj que irá actualizándose segundo a segundo.
export const Clock = component$(() => {
const seconds = useSignal(0);
useVisibleTask$(({ cleanup }) => {
const interval = setInterval(() => {
seconds.value++;
}, 1000);
cleanup(() => clearInterval(interval));
});
return <p>Seconds: {seconds.value}</p>;
});
export function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return () => clearInterval(interval);
});
return <p>Seconds: {seconds}</p>;
}
Capítulos donde se trabaja con este concepto:
Trabajo de consumo de APIs haciendo uso de los ciclos de vida con la función track
.
export const Fetch = component$(() => {
const url = useSignal('https://api.github.com/repos/BuilderIO/qwik');
const responseJson = useSignal(undefined);
useTask$(async ({ track }) => {
track(() => url.value);
const res = await fetch(url.value);
const json = await res.json();
responseJson.value = json;
});
return (
<>
<p>{responseJson.value?.name} has {responseJson.value?.stargazers_count} ✨'s</p>
<input name="url" bind:value={url} />
</>
);
});
export function Fetch() {
const [url, setUrl] = useState('https://api.github.com/repos/BuilderIO/qwik');
const [responseJson, setResponseJson] = useState(undefined);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json) => setResponseJson(json));
}, [url]);
return (
<>
<p>{responseJson?.name} has {responseJson?.stargazers_count} ✨'s</p>
<input name="url" onInput={(ev) => setUrl((ev.target as HTMLInputElement).value)} />
</>
);
}
Capítulos donde se trabaja con este concepto:
Para poder trabajar con los estados globales y así hacer uso de la información desde cualquier componente,
sin hacer uso de los props
.
import {
useContext,
useContextProvider,
useSignal,
createContextId, // (A partir de la 0.18.1)
} from '@builder.io/qwik';
export const MyContext = createContextId<Signal<number>>('my-context');
export const Parent = component$(() => {
const message = useSignal('some example value');
useContextProvider(MyContext, message);
return (
<>
<Child />
</>
);
});
export const Child = component$(() => {
const message = useContext(MyContext);
return <p>{message.value}</p>;
});
export const MyContext = createContext({ message: 'some example value' });
export default function Parent() {
return (
<MyContext.Provider value={{ message: 'updated example value' }}>
<Child />
</MyContext.Provider>
);
}
export const Child = () => {
const value = useContext(MyContext);
return <p>{value.message}</p>;
};
Capítulos donde se trabaja con este concepto:
Creamos un componente con un elemento input donde implementaremos la opción con retardo, para que se asigne
un valor en un tiempo especificado. En este caso el retardo será de 1000ms
que es igual a
1sg
.
export const DebouncedInput = component$(() => {
const inputText = useSignal('');
const debouncedValue = useSignal('');
useTask$(({ track, cleanup }) => {
track(() => inputText.value);
const debounced = setTimeout(() => {
debouncedValue.value = inputText.value;
}, 1000);
cleanup(() => clearTimeout(debounced));
});
return (
<>
<input bind:value={inputText} />
<p>{debouncedValue.value}</p>
</>
);
});
export const DebouncedInput = () => {
const [value, setValue] = useState('');
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const debounced = setTimeout(() => setDebouncedValue(value), 1000);
return () => {
clearTimeout(debounced);
};
}, [value]);
return (
<>
<input value={value} onChange={(ev) => setValue((ev.target as HTMLInputElement).value))} />
<p>{debouncedValue}</p>
</>
);
};
Capítulos en los que se trabaja con estos conceptos:
En este caso vamos a crear un ejemplo donde mediante el evento onClick$()
vamos a ejecutar una
función que genera los valores de rojo, verde y azul de manera aleatoria.
export const DynamicBackground = component$(() => {
const red = useSignal(0);
const green = useSignal(0);
const blue = useSignal(0);
return (
<div
style={{
background: `rgb(${red.value}, ${green.value}, ${blue.value})`,
}}
>
<button
onClick$={() => {
red.value = Math.random() * 256;
green.value = Math.random() * 256;
blue.value = Math.random() * 256;
}}
>
Change background
</button>
</div>
);
});
export function DynamicBackground() {
const [red, setRed] = useState(0);
const [green, setGreen] = useState(0);
const [blue, setBlue] = useState(0);
return (
<div
style={{
background: `rgb(${red}, ${green}, ${blue})`,
}}
>
<button
onClick={() => {
setRed(Math.random() * 256);
setGreen(Math.random() * 256);
setBlue(Math.random() * 256);
}}
>
Change background
</button>
</div>
);
}
Capítulos donde se trabaja con este concepto:
Creamos un componente que renderiza una lista de elementos. En este caso, por ejemplo mostramos una lista de equipos de la NBA con su año de fundación.
export const NBATeams = component$(() => {
const nbaTeams = [
{ "name": "Atlanta Hawks", "foundation": 1946 },
{ "name": "Boston Celtics", "foundation": 1946 },
{ "name": "Brooklyn Nets", "foundation": 1967 },
{ "name": "Charlotte Hornets", "foundation": 1988 },
{ "name": "Chicago Bulls", "foundation": 1966 },
{ "name": "Cleveland Cavaliers", "foundation": 1970 },
{ "name": "Dallas Mavericks", "foundation": 1980 },
{ "name": "Denver Nuggets", "foundation": 1967 },
{ "name": "Detroit Pistons", "foundation": 1941 },
{ "name": "Golden State Warriors", "foundation": 1946 },
{ "name": "Houston Rockets", "foundation": 1967 },
{ "name": "Indiana Pacers", "foundation": 1967 },
{ "name": "LA Clippers", "foundation": 1970 },
{ "name": "Los Angeles Lakers", "foundation": 1947 },
{ "name": "Memphis Grizzlies", "foundation": 1995 },
{ "name": "Miami Heat", "foundation": 1988 },
{ "name": "Milwaukee Bucks", "foundation": 1968 },
{ "name": "Minnesota Timberwolves", "foundation": 1989 },
{ "name": "New Orleans Pelicans", "foundation": 2002 },
{ "name": "New York Knicks", "foundation": 1946 },
{ "name": "Oklahoma City Thunder", "foundation": 1967 },
{ "name": "Orlando Magic", "foundation": 1989 },
{ "name": "Philadelphia 76ers", "foundation": 1946 },
{ "name": "Phoenix Suns", "foundation": 1968 },
{ "name": "Portland Trail Blazers", "foundation": 1970 },
{ "name": "Sacramento Kings", "foundation": 1923 },
{ "name": "San Antonio Spurs", "foundation": 1967 },
{ "name": "Toronto Raptors", "foundation": 1995 },
{ "name": "Utah Jazz", "foundation": 1974 },
{ "name": "Washington Wizards", "foundation": 1961 }
];
return (
<ul>
{nbaTeams.map((team) => (
<li key={team.name + team.foundation}>
{team.name} ({team.foundation})
</li>
))}
</ul>
);
});
export function NBATeams() {
const nbaTeams = [
{ "name": "Atlanta Hawks", "foundation": 1946 },
{ "name": "Boston Celtics", "foundation": 1946 },
{ "name": "Brooklyn Nets", "foundation": 1967 },
{ "name": "Charlotte Hornets", "foundation": 1988 },
{ "name": "Chicago Bulls", "foundation": 1966 },
{ "name": "Cleveland Cavaliers", "foundation": 1970 },
{ "name": "Dallas Mavericks", "foundation": 1980 },
{ "name": "Denver Nuggets", "foundation": 1967 },
{ "name": "Detroit Pistons", "foundation": 1941 },
{ "name": "Golden State Warriors", "foundation": 1946 },
{ "name": "Houston Rockets", "foundation": 1967 },
{ "name": "Indiana Pacers", "foundation": 1967 },
{ "name": "LA Clippers", "foundation": 1970 },
{ "name": "Los Angeles Lakers", "foundation": 1947 },
{ "name": "Memphis Grizzlies", "foundation": 1995 },
{ "name": "Miami Heat", "foundation": 1988 },
{ "name": "Milwaukee Bucks", "foundation": 1968 },
{ "name": "Minnesota Timberwolves", "foundation": 1989 },
{ "name": "New Orleans Pelicans", "foundation": 2002 },
{ "name": "New York Knicks", "foundation": 1946 },
{ "name": "Oklahoma City Thunder", "foundation": 1967 },
{ "name": "Orlando Magic", "foundation": 1989 },
{ "name": "Philadelphia 76ers", "foundation": 1946 },
{ "name": "Phoenix Suns", "foundation": 1968 },
{ "name": "Portland Trail Blazers", "foundation": 1970 },
{ "name": "Sacramento Kings", "foundation": 1923 },
{ "name": "San Antonio Spurs", "foundation": 1967 },
{ "name": "Toronto Raptors", "foundation": 1995 },
{ "name": "Utah Jazz", "foundation": 1974 },
{ "name": "Washington Wizards", "foundation": 1961 }
];
return (
<ul>
{nbaTeams.map((team) => (
<li key={team.name + team.foundation}>
{team.name} ({team.foundation})
</li>
))}
</ul>
);
}
Capítulo donde se trabaja con este concepto:
Formas incorrectas / correctas que tenemos para poder hacer uso de los hooks (y custom hooks) en Qwik.
useHook(); // <-- ❌ No funcionará por estar fuera de component$
export default component$(() => {
useCustomHook(); // <-- ✅ Dentro de component$ y en la raíz
if (condition) {
useHook(); // <-- ❌ Aunque esté dentro de component$, no está en la raíz
}
useTask$(() => {
useNavigate(); // <-- ❌ No podemos usar un hook dentro de otro
});
const myQrl = $(() => useHook()); // <-- ❌ Debe de estar en la raíz
return <button onClick$={() => useHook()}></button>; // <-- ❌ No se puede con acciones
});
// Dentro de una función denominada custom hook funciona como en component$
function useCustomHook() {
useHook(); // <-- ✅ Funciona por estar en raíz
if (condition) {
useHook(); // <-- ❌ No está en la raíz, está dentro de condición
}
}
Capítulo donde se trabaja con este concepto:
Esta porción de código os recomiendo que la tengáis siempre a mano mientras estéis aprendiendo. Luego, cuando ya tengáis todo bien interiorizado, inconscientemente lo aplicaréis perfectamente sin pensar mucho.
Routing Avanzado — Enrutamiento mediante parámetros
Información sobre este apartado aquí.
Teniendo la siguiente distribución de directorios:
src/
└── routes/
├── index.tsx # /
...
└── product/
└── [productId]/
├── index.tsx # /product/1234
└── details/
└── index.tsx # /product/1234/details
Accedemos mediante /product/<id-del-producto>
para la página del producto principal y
/product/<id-del-producto>/details
para los detalles del producto.
Obtención del parámetro ruta desde una URL
Información sobre este apartado aquí.
Usando como base la distribución de directorios anterior.
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
// Obtenemos toda la información acerca de la ubicación respecto a la navegación
const location = useLocation();
return (
<div>
<div>Product Item Details [productId] - Info page</div>
// URL princ. + path + valor de parámetro de ruta
<p>Href: {location.url.href}</p>
// path + valor de parámetro de ruta
<p>Pathname: {location.url.pathname}</p>
// Valor de parámetro de ruta
<p>Product Id: {location.params.productId}</p>
</div>
);
});
Añadiendo en el navegador esta ruta: http://127.0.0.1:5173/product/1234/details/
.
Opción | Resultado |
---|---|
Href |
http://127.0.0.1:5173/product/1234/details/ |
Pathname |
/product/1234/details/ |
Product Id |
1234 |
Capturar los parámetros mediante rutas comodín
Información sobre este apartado aquí.
Teniendo la siguiente distribución de directorios:
src/
└── routes/
├── index.tsx # /
...
└── product/
└── [...productIds]/
└── index.tsx # /product/1234/123/121
Pudiendo obtener los ids de los productos implementando este código:
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const location = useLocation();
return (
<>
<div>Product Item [...productIds] - Principal Page</div>
// IDs de los productos
<p>Product Id: {JSON.stringify(location.params.productIds)}</p>
</>
);
});
Accedemos mediante /product/<id-del-producto-1>/.../<id-del-producto-n>
para la
página del producto principal con los ids seleccionados.
Agrupación
Información sobre este apartado aquí.
Teniendo en cuenta una estructura así:
src/
└── routes/
└── (products)/
├── detail/
| └── index.tsx
└── preview/
└── index.tsx
Al haber añadido un directorio (<nombre-directorio)
, al introducir la ruta NO hay que
añadir el nombre-directorio
en la ruta.
Ruta introducida | ¿Válida? |
---|---|
/detail |
✅ |
/preview |
✅ |
/products/detail |
❌ |
/products/preview |
❌ |
/products |
❌ |
Para implementar de forma muy fácil integraciones de librerías en nuestros proyectos con pocos pasos.
Para ejecutarlo y ver la lista de disponibles:
npm run qwik add
Más información al detalle: