Guia em atualização...

Gerenciamento de estado

Se você veio de outros frameworks javascript, deve ter se deparado com o conceito de “Gerenciamento de estado”. Basicamente isso significa o compartilhamento de informações entre as várias partes de um app. Essas partes são os componentes. Muitos desses frameworks permitem a utilização de pacotes externos para essa tarefa. No svelte isso também acontece. Caso você queira, pode utilizar pacotes como Redux, RxJS, Zustand e outros.

Ou melhor ainda, utilizar o que o svelte nos oferece de forma nativa. Stores.

Na página sobre “props”, vimos como passar atributos e propriedas para os componentes. Isso resolve o problema em casos simples, mas, quando a aplicação cresce, precisamos de uma forma mais eficiente.

Imagine a seguinte situação!

<!-- App.svelte -->
<script>
	import Layout from './Layout.svelte'
	import Main from './Main.svelte'

	let name = 'João';
</script>

<Layout {name}>
	<Main {name} />
</Layout>
<!-- Layout.svelte -->

<!-- Nas próximas aulas veremos sobre esse <slot /> -->
<slot />
<!-- Main.svelte -->
<script>
	import Card from './Card.svelte'
	export let name
</script>

<Card {name} />
<!-- Card.svelte -->
<script>
	export let name;
</script>

<div>
	Card - {name}
</div>

Acima temos 4 arquivos diferentes.

  • App.svelte é o componente principal. Ele importa Main.svelte e Layout.svelte
  • <Layout /> é o componente “pai”. Dentro do arquivo dele temos um <slot />. Funciona como o children do React.
  • Main.svelte recebe name como propriedade e passa para o componente <Card />
  • Card.svelte recebe essa propriedade e exibe na tela dentro de uma <div>.

Apenas Card precisa da propriedade name. É um grande problema ter que passar as propriedades para os componentes até chegar no componente que realmente precisa. Isso faz com que os componentes acima dele recebam propriedades que não precisam.

O svelte oferece algumas maneiras de lidar com isso. Stores e Context. A diferença básica é que os valores dentro das stores não precisam ser usados apenas em componentes, mas também em módulos javascript comuns.

Uma store é um objeto que contém um método subscribe permitindo aos componentes se “inscreverem” e serem notificados quando o valor da store mudar.

Existem 3 tipos de stores: Gravável(writable), Legível(readable) e derivada(derived). Vamos ver cada uma delas.

Loja gravável (writable store)

Uma store “writable” permite que os assinantes modifiquem seus dados. Para criar é simples.

// Primeiro você importa o tipo de store que quer
import { writable } from 'svelte/store';

// Depois exporta o valor. Lembrando que aqui você não precisa usar uma "let".
export const count = writable(0);

Esse código acima será colocado em um arquivo js comum. Por exemplo: store.js.

Para utilizar, basta importar esse arquivo dentro dos nossos componentes.

<script>
	import { count } from './store.js'
</script>

Como dito anteriormente, esse valor é um objeto que possui um método subscribe. Vamos fazer esse componente se inscrever nessa store.

<script>
	import { count } from './store.js'

	count.subscribe((value) => console.log(value))
</script>

Esse callback dentro do método subscribe retorna o valor dentro da nossa store. Podemos utilizar esse valor assim:

gif

Agora, como modificamos essa valor? Bom, por ser uma store do tipo writable, além do método subscribe, essa store tem acesso a mais dois métodos: set e update. E são eles que usaremos para alterar o valor da store.

Definição:

  • set É um método que recebe um valor como parâmetro e define a store com esse valor. Sintaxe: nomeDaStore.set(novoValorDaStore). Usando esse método, o valor atual da store é redefinido. Perceba que não temos acesso ao valor anterior. Esse método substitui o valor da store inteira. É útil quando queremos resetar um valor. Por exemplo, um contador: nomeDaStore.set(0).

  • update É um método que recebe um callback como argumento. Esse callback usa o valor anterior da store e retorna um novo valor. Sintaxe: nomeDaStore.update((valorAnteriorDaStore) => valorModificado). É útil quando trabalhamos com arrays e objetos. Para adicionar um novo item a um array, faça: nomeDaStore.update((array) => [...array, item]).

Exemplo:

<script>
	import { count } from './store.js';
	let number;

	count.subscribe((value) => {
		number = value
	})

	function add10() {
		count.update((value) => value + 10)
	}

	function reset() {
		count.set(0)
	}
</script>

<h1>{number}</h1>
<button on:click={reset}>Resetar</button>
<button on:click={add10}>Add + 10</button>
gif

Agora que sabemos como fazer a inscrição em uma store, precisamos saber como cancelar essa assinatura. É como um site que você assina com seu cartão de crédito, uma hora você quer ou precisa cancelar.

Caso não seja feito esse cancelamento, isso resultará em um vazamento de memória quando o componente for instanciado e destruído várias vezes.

Quando chamamos o método subscribe ele retorna uma função unsubscribe. Mas onde chamamos essa função? Lembra das funções de ciclo de vida? Vamos usar nosso amigo onDestroy.

<script>
	import { onDestroy } from 'svelte';
	import { count } from './store.js'
	let number;

	const unsubscribe = count.subscribe((value) => {
		number = value
	})

	// Sempre que o componente for desmontado a função unsubscribe será executada.
	onDestroy(unsubscribe)
</script>

Isso tudo é feito para 1 única store. Imagine agora se tivéssemos várias.

<script>
	import { onDestroy } from 'svelte';
	import { count, user, log } from './store.js'
	let number;
	let userData;
	let systemLog;

	const unsubscribe = count.subscribe((value) => {
		number = value
	})

	const unsubscribe2 = user.subscribe((value) => {
		userData = value
	})

	const unsubscribe3 = log.subscribe((value) => {
		systemLog = value
	})

	onDestroy(unsubscribe)
	onDestroy(unsubscribe2)
	onDestroy(unsubscribe3)
</script>

Vai ficando chato escrever tanto código. Mas o svelte tem um jeito mais fácil de lidar com isso. Auto subscription!

Inscrição automática (auto subscription)

Muitos conceitos foram mostraros até agora sobre as stores: Criar a store, assinar, cancelar assinatura, definir valores, atualizar valores… Muita coisa. Vamos simplificar!

O exemplo anterior era assim:

<script>
	import { onDestroy } from 'svelte';
	import { count } from './store.js'
	let number;

	const unsubscribe = count.subscribe((value) => {
		number = value
	})


	onDestroy(unsubscribe)
</script>

Agora fica assim:

<script>
	import { count } from './store.js'
</script>

E para exibir o valor, basta adicionar um $ como prefixo da store.

<h1>{$count}</h1>

Uma explicação na documentação do svelte.

Sempre que você tiver uma referência a uma loja, poderá acessar seu valor dentro de um componente prefixando-o com o caractere $. Isso faz com que Svelte declare a variável prefixada, assine a loja na inicialização do componente e cancele a assinatura quando apropriado. Documentação

Viu o quanto é simples?

E como alteramos esse valor? Reatribuição!

<script>
	import { count } from './store.js'

	function add() {
		$count++
	}
</script>

<h1>{$count}</h1>
<button on:click={add}>+</button>
gif

Uma observação importante!

Se tentarmos criar uma variável com o prefixo $ o svelte irá impedir. Isso por que ele entende que essa variável é uma store.

gif

A loja “writable” permite que seus valores sejam alterados de fora, por outros componentes. No entanto, nem sempre é isso que queremos. As vezes teremos valores que serão apenas acessados e não faz sentido mudar o valor.

Para isso o svelte oferece a store “readable”.

Loja legível (readable store)

Para criar uma store “readable”, faça:

// Primeiro você importa o tipo de store que quer
import { readable } from 'svelte/store';

// Depois exporta o valor. Lembrando que aqui você não precisa usar uma "let".
export const date = readable(new Date());

E no App.svelte fazemos:

<script>
	import { date } from './store'
</script>

<h1>{$date}</h1>

O resultado é a exibição da data. Mas e aí? Poderíamos ter criado essa data através de uma let ou const. Sim, é verdade!

Porém, tanto a função “writable” quanto “readable” oferecem um segundo argumento. Vamos entender.

  • O primeiro argumento é o valor inicial - Ex: readable(0).
  • O segundo argumento é uma função que chamamos de “start” - Ex: readable(0, () => {}).
  • Dentro dessa função temos um callback “set” - Ex: readable(0, (set) => {}).
  • Esse calback set é utilizado para alterar o valor da store. Ex:
readable(0, (set) => {
	set(1);
});
  • Essa função deve retornar uma outra função que chamamos de “stop”. Ex:
readable(0, (set) => {
	set(1);

	return () => {};
});

Ficou confuso, eu sei!. Vamos detalhar um pouco mais. Primeiro, veja esse código em ação:

<!-- App.svelte -->
<script>
	import { date } from './store';
</script>

<h1>{$date}</h1>
// store.js
import { readable } from 'svelte/store';

export const date = readable(new Date(), (set) => {
	const interval = setInterval(() => {
		set(new Date());
	}, 1000);

	return () => {
		clearInterval(interval);
	};
});
gif

As funções que chamamos de “start” e “stop” são as identificadas abaixo:

// store.js
import { readable } from 'svelte/store';

export const date = readable(new Date(), function start(set) {
	const interval = setInterval(() => {
		set(new Date());
	}, 1000);

	return function stop() {
		clearInterval(interval);
	};
});

É o mesmo código porém usando funções nomeadas ao invés de arrow functions.

O funcionamento é o seguinte: A função “start” é executada quando acontece a primeira inscrição na store. Já a função “stop” acontece quando a última inscrição na store é cancelada.

Veja o exemplo abaixo:

gif

O que aconteceu aqui?

Simples. A função “start” foi executada na primeira inscrição: const unsubscribe1 = date.subscribe(() => {}). Na segunda, não foi executada. A função “stop” não foi executada quando fiz: unsubscribe1(), mas sim quando fiz unsubscribe2().

Imagine o seguinte: Você cria um espaço para uma festa e está aguardando as pessoas chegarem no local.

  • Quando a primeira pessoa chega você exibe uma mensagem: “Bem vindo, você é o primeiro a chegar”. Função “start”.

Não faz sentido fazer isso pra todo mundo, além de ser cansativo.

  • Quando a última pessoa vai embora da festa, você vai limpar o local. Função “stop”.

Não faz sentido limpar enquanto tem gente no espaço.

É assim que esse segundo argumento de writable e readable funciona.

Loja derivada (derived store)

Até você viu o poder das stores no svelte. Mas conheceu apenas o básico. Avançaremos agora em direção ao verdadeiro poder. Stores derivadas.

Deriva uma loja de uma ou mais outras lojas. O callback é executado inicialmente quando o primeiro assinante se inscreve e, em seguida, sempre que as dependências da loja mudam. Documentação

Derivar uma ou várias stores significa uní-las em uma nova store aplicando a funcionalidade que quisermos. Podemos, por exemplo, criar uma store com uma lista de marcas de veículos, uma store anexada a um campo de texto que será escrito pelo usuário e uní-las em uma store que retornará uma lista filtrada.

Vou mostrar esse exemplo e depois eu explico.

<!-- App.svelte -->
<script>
	import { onMount } from 'svelte'
	import { brands, searchText, filtered } from './store';

	onMount(async () => {
		const response = await fetch('https://parallelum.com.br/fipe/api/v1/carros/marcas')
		const json = await response.json()

		$brands = json
	})
</script>

<h1>Pesquisa</h1>
<input bind:value={$searchText} />

{#each $filtered as brand}
	<h5>{brand.nome}</h5>
{/each}
// store.js
import { writable, derived } from 'svelte/store';

export const brands = writable([]);
export const searchText = writable('');

export const filtered = derived([brands, searchText], ([$brands, $searchText]) => {
	return $brands.filter((brand) => {
		return brand.nome.toLowerCase().includes($searchText.toLowerCase());
	});
});
gif

Calma que vou explicar tudo. Vamos por partes.

Existem 4 opções para criar uma store derivada.

  • Store derivada simples síncrona
  • Store derivada simples “assíncrona”.Define um valor de forma assíncrona.
  • Store derivada múltipla síncrona
  • Store derivada múltipla “assíncrona”.Define um valor de forma assíncrona.

Detalhando:

  • Store derivada simples síncrona.

Formato: derived(storeName, ($value) => $value ).

storeName é o nome da store da qual você quer derivar. Exemplo:

import { writable, derived } from 'svelte/store';

const number = writable(1);

// Store derivada simples - síncrona
export const syncSimple = derived(number, ($num) => $num * 5);

$value representa o valor da store que você derivou. Pode ser um nome qualquer, até mesmo sem o $. Recomendo que deixe com o prefixo $ por convenção.

Essa store derivada vai retornar o valor da store number * 5 ($num * 5), ou qualquer outra operação que quisermos.

Para utilizar, o procedimento é o mesmo.

<script>
	import { syncSimple } from './store'
</script>

<h3>Store simples (síncrona) - valor: {$syncSimple}</h3>
  • Store derivada simples “assíncrona”

Formato: derived(storeName, ($value, set) => set($value), initialValue).

Essa store define o valor de forma assíncrona através do método set. Retornar o valor diretamente como antes não funcionará.

Exemplo: Isso funciona.

import { writable, derived } from 'svelte/store';

const number = writable(1);

// Store derivada simples - assíncrona
export const asyncSimple = derived(number, ($num, set) => set($num * 5));

Isso não.

import { writable, derived } from 'svelte/store';

const number = writable(1);

// Store derivada simples - assíncrona
export const asyncSimple = derived(number, ($num, set) => $num * 5);

initialValue É um argumento opcional. Como o valor a ser definido será assíncrono, essa definição pode levar um certo tempo e o valor da store será undefined até que isso aconteça.

export const asyncSimple = derived(number, ($num, set) => {
	setTimeout(() => set($num * 10), 2000);
});
gif

Agora com initialValue.

export const asyncSimple = derived(
	number,
	($num, set) => {
		setTimeout(() => set($num * 10), 2000);
	},
	'Carregando...'
);
gif
  • Store derivada mútipla “síncrona”

Formato: derived([store1, store2, ...], ([$value1, $value2, ...]) => $value1 + ...).

Esse tipo de store aceita como primeiro argumento um array de stores (Array de dependências), como segundo argumento um callback contendo um array com os valores dessas stores e retorna qualquer operação que você queira fazer.

Exemplo:

import { writable, readable, derived } from 'svelte/store';

const number = writable(1);
const text = readable('Svelte');

// Store derivada mútipla - síncrona
export const syncMultiple = derived([number, text], ([$num, $text]) => {
	return `${$text}! Nota ${$num * 1000}`;
});
  • Store derivada mútipla “assíncrona”

Formato: derived([store1, store2, ...], ([$value1, $value2, ...], set) => $value1 + ..., initialValue)

Essa store tem o mesmo funcionamento da store simples assíncrona, porém aceitando mútiplas stores.

import { writable, readable, derived } from 'svelte/store';

const number = writable(1);
const text = readable('Svelte');

// Store derivada mútipla - assíncrona
export const asyncMultiple = derived(
	[number, text],
	([$num, $text], set) => {
		setTimeout(() => {
			set(`${$text}! Nota ${$num * 1000}`);
		}, 1500);
	},
	'Qual a nota para o Svelte?'
);

Um exemplo completo.

<!-- App.svelte -->
<script>
	import { syncSimple, asyncSimple, syncMultiple, asyncMultiple } from './store'
</script>

<h3>Store simples (síncrona) - valor: {$syncSimple}</h3>
<h3>Store simples (assíncrona) - valor: {$asyncSimple}</h3>
<h3>Store mútipla (síncrona) - valor: {$syncMultiple}</h3>
<h3>Store mútipla (assíncrona) - valor: {$asyncMultiple}</h3>
// store.js
import { writable, readable, derived } from 'svelte/store';

// Stores apenas de exemplo
const number = writable(1);
const text = readable('Svelte');

// Store derivada simples - síncrona
export const syncSimple = derived(number, ($num) => $num * 5);

// Store derivada simples - assíncrona
export const asyncSimple = derived(
	number,
	($num, set) => {
		// 	setTimeout(() => set($num * 10), 1000)
	},
	'Carregando...'
);

// Store derivada mútipla - síncrona
export const syncMultiple = derived([number, text], ([$num, $text]) => {
	return `${$text}! Nota ${$num * 1000}`;
});

// Store derivada mútipla - assíncrona
export const asyncMultiple = derived(
	[number, text],
	([$num, $text], set) => {
		setTimeout(() => {
			set(`${$text}! Nota ${$num * 1000}`);
		}, 1500);
	},
	'Qual a nota para o Svelte?'
);
gif

Loja personalizada (custom store)

Se um objeto implementar corretamente o método subscribe, ele se torna uma store. Sendo assim, podemos criar stores com funcionalidades diferentes.

As stores são objetos que possuem os métodos subscribe, update e set, se forem writable, é claro. Dessa forma podemos desestruturar uma store.

const { subscribe, set, update } = writable();

Agora vamos criar uma store presonalizada a partir desses métodos. Primeiro importamos o tipo de store que queremos

import { writable } from 'svelte/store';

Depois criamos uma função que retorna um objeto.

import { writable } from 'svelte/store';

function createArray() {
	return {};
}

Desestruturamos a store.

import { writable } from 'svelte/store';

function createArray() {
	const { subscribe, set, update } = writable([0]);

	return {};
}

Agora criamos nossos métodos dentro do objeto e colocamos junto o subscribe. Não podemos esquecer dele.

import { writable } from 'svelte/store';

function createArray() {
	const { subscribe, set, update } = writable([0]);

	return {
		subscribe,
		add: (value) => update((arr) => [...arr, value]),
		remove: (quantity) => update((arr) => arr.slice(0, -quantity)),
		reset: () => set([0])
	};
}
  • O método add retorna o array anterior + o valor que quisermos adicionar ao array.
  • O método remove retira do array a quantidade de items que quisermos.
  • O método reset define o array para o valor que quisermos. No caso, quero que volte ao valor inicial.

Por fim, exportamos a função.

import { writable } from 'svelte/store';

function createArray() {
	const { subscribe, set, update } = writable([0]);

	return {
		subscribe,
		add: (value) => update((arr) => [...arr, value]),
		remove: (quantity) => update((arr) => arr.slice(0, -quantity)),
		reset: () => set([0])
	};
}

export const items = createArray();

E para utilizar é bem simples.

<script>
	import { items } from './store.js';
</script>

<h1>Items {@html $items}</h1>

<button on:click={() => items.add(1)}>+</button>
<button on:click={() => items.remove(3)}>-</button>
<button on:click={items.reset}>reset</button>

Essa tag {@html } serve para renderizar html. Fiz apenas para exibir os items do array. Leia mais

Note que na diretiva on:click eu fiz () => items.add(1) porque essa é uma função que tem parâmetros. Se não tivesse poderia fazer items.add, assim como fiz em items.reset. Volte na página sobre eventos se tiver dúvidas.

Demonstração desse código.

gif

Você pode se perguntar. Qual a utilidade disso? Bom, a principal delas é que você pode restringir o acesso aos métodos da store. Fazendo isso, a store será modificada do jeito que você definir, e não de qualquer forma. No exemplo acima temos o método reset que define o valor para exatamente o que foi definido. Dentro do componente esse método é chamado é não pode definir outro valor. Você cria um certo controle sobre suas stores. O que estamos fazendo são coisas bem simples, mas a medida que seu app cresce, tenho certeza que irá querer uma organização melhor.

Binding em lojas (store binding)

Quando uma store é do tipo writable podemos fazer binding em componentes.

Na página sobre binding, pegamos e definimos o valor de um input assim:

<script>
	let text = 'Svelte';
</script>

<h1>{text}</h1>

<input bind:value={text} />

Com stores é bem similar. Primeiro criamos nosso store.

// store.js
import { writable } from 'svelte/store';

export const text = writable('Svelte');

Depois utilizamos em nosso componente.

<!-- App.svelte -->
<script>
	import { text } from './store.js';
</script>

<h1>{$text}</h1>

<input bind:value={$text} />
gif