Kuasai Svelte dari dasar. Pelajari mengapa Svelte diciptakan, pahami core concepts seperti reactive declarations, two-way binding, stores, components, dan lifecycle hooks. Bangun complete production-ready weather dashboard app yang cover semua Svelte fundamentals dengan best practices.

Svelte merepresentasikan paradigm shift dalam frontend development. Berbeda dengan traditional frameworks yang run di browser, Svelte shift work ke compile time, resulting dalam smaller bundles dan better performance. Tapi mengapa Svelte ada, dan apa yang membuatnya fundamentally different?
Dalam artikel ini, kita akan explore Svelte's philosophy, understand mengapa Svelte diciptakan, dive deep ke core concepts, dan build complete production-ready weather dashboard application yang demonstrate semua fundamental Svelte patterns.
Sebelum Svelte, frontend development faced significant challenges:
Rich Harris created Svelte di 2016 dengan revolutionary approach:
Reactive declarations automatically update ketika dependencies change. Ini adalah Svelte's most powerful feature.
<script>
let count = 0;
let doubled = 0;
// ✅ Reactive declaration - updates ketika count changes
$: doubled = count * 2;
// ✅ Reactive block - runs ketika dependencies change
$: {
console.log(`count is now ${count}`);
}
// ✅ Reactive if statement
$: if (count > 10) {
console.log('count is greater than 10');
}
</script>
<button on:click={() => count++}>
Count: {count}, Doubled: {doubled}
</button>Two-way binding synchronizes component state dengan form inputs automatically.
<script>
let name = '';
let email = '';
let agreed = false;
// ✅ Two-way binding dengan bind:value
// Changes ke input automatically update variable
// Changes ke variable automatically update input
</script>
<input bind:value={name} placeholder="Enter name" />
<input bind:value={email} type="email" placeholder="Enter email" />
<input bind:checked={agreed} type="checkbox" />
<p>Name: {name}</p>
<p>Email: {email}</p>
<p>Agreed: {agreed}</p>Lifecycle hooks let you run code at specific times dalam component's life.
<script>
import { onMount, onDestroy, beforeUpdate, afterUpdate } from 'svelte';
let data = null;
// ✅ Runs setelah component di-mount ke DOM
onMount(async () => {
const response = await fetch('/api/data');
data = await response.json();
// Return cleanup function
return () => {
console.log('Component unmounted');
};
});
// ✅ Runs sebelum component updates
beforeUpdate(() => {
console.log('Component about to update');
});
// ✅ Runs setelah component updates
afterUpdate(() => {
console.log('Component updated');
});
// ✅ Runs ketika component destroyed
onDestroy(() => {
console.log('Component destroyed');
});
</script>
{#if data}
<p>{data.message}</p>
{/if}Stores provide reactive state management across components.
// stores.ts
import { writable, readable, derived } from 'svelte/store';
// ✅ Writable store - dapat di-update dari anywhere
export const count = writable(0);
// ✅ Readable store - hanya dapat di-update internally
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return () => clearInterval(interval);
});
// ✅ Derived store - computed dari other stores
export const doubled = derived(count, ($count) => $count * 2);
// Usage dalam component
<script>
import { count, doubled } from './stores';
</script>
<p>Count: {$count}</p>
<p>Doubled: {$doubled}</p>
<button on:click={() => count.update(n => n + 1)}>
Increment
</button>Components adalah reusable pieces dari UI yang accept props.
// Button.svelte
<script>
export let label = 'Click me';
export let disabled = false;
export let variant = 'primary';
</script>
<button class={variant} {disabled}>
{label}
</button>
<style>
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background: #667eea;
color: white;
}
.primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
// App.svelte
<script>
import Button from './Button.svelte';
</script>
<Button label="Submit" variant="primary" />
<Button label="Cancel" variant="secondary" disabled />Handle user interactions dengan event directives.
<script>
let count = 0;
function handleClick() {
count++;
}
function handleSubmit(event) {
event.preventDefault();
console.log('Form submitted');
}
function handleKeydown(event) {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
}
</script>
<button on:click={handleClick}>
Clicked {count} times
</button>
<form on:submit={handleSubmit}>
<input on:keydown={handleKeydown} />
<button type="submit">Submit</button>
</form>
<!-- Event modifiers -->
<button on:click|once={handleClick}>
Click once
</button>
<button on:click|preventDefault={() => console.log('clicked')}>
Prevent default
</button>Render different content based on conditions.
<script>
let isLoggedIn = false;
let count = 0;
</script>
<!-- if/else block -->
{#if isLoggedIn}
<p>Welcome back!</p>
{:else}
<p>Please log in</p>
{/if}
<!-- if/else if/else -->
{#if count === 0}
<p>No items</p>
{:else if count === 1}
<p>One item</p>
{:else}
<p>{count} items</p>
{/if}
<!-- Ternary-like dengan each -->
{#if items.length > 0}
<ul>
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul>
{:else}
<p>No items found</p>
{/if}Render lists efficiently dengan each block.
<script>
let todos = [
{ id: 1, title: 'Learn Svelte', completed: false },
{ id: 2, title: 'Build an app', completed: false },
];
function addTodo() {
todos = [...todos, {
id: todos.length + 1,
title: 'New todo',
completed: false
}];
}
</script>
<!-- Basic each -->
{#each todos as todo (todo.id)}
<div>
<input type="checkbox" bind:checked={todo.completed} />
<span class:completed={todo.completed}>
{todo.title}
</span>
</div>
{/each}
<!-- Dengan index -->
{#each todos as todo, index (todo.id)}
<p>{index + 1}. {todo.title}</p>
{/each}
<!-- Dengan else -->
{#each todos as todo (todo.id)}
<p>{todo.title}</p>
{:else}
<p>No todos</p>
{/each}
<button on:click={addTodo}>Add Todo</button>
<style>
.completed {
text-decoration: line-through;
opacity: 0.5;
}
</style>Add smooth animations dan transitions ke elements.
<script>
import { fade, slide, scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
let visible = true;
let items = [1, 2, 3];
function removeItem(index) {
items = items.filter((_, i) => i !== index);
}
</script>
<!-- Fade transition -->
{#if visible}
<div transition:fade>
This fades in dan out
</div>
{/if}
<!-- Slide transition -->
{#if visible}
<div transition:slide>
This slides in dan out
</div>
{/if}
<!-- Scale transition dengan options -->
{#if visible}
<div transition:scale={{ duration: 300 }}>
This scales in dan out
</div>
{/if}
<!-- Animate directive untuk list reordering -->
{#each items as item (item)}
<div animate:flip={{ duration: 200 }}>
{item}
<button on:click={() => removeItem(items.indexOf(item))}>
Remove
</button>
</div>
{/each}
<button on:click={() => visible = !visible}>
Toggle
</button>Styles adalah scoped ke components by default.
<script>
let isActive = false;
</script>
<div class="container">
<h1>Scoped Styles</h1>
<button class:active={isActive} on:click={() => isActive = !isActive}>
Toggle
</button>
</div>
<style>
/* ✅ Scoped ke component ini only */
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
}
h1 {
color: #333;
font-size: 2rem;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: #5568d3;
transform: translateY(-2px);
}
/* ✅ Class binding untuk dynamic styles */
button.active {
background: #10b981;
}
/* ✅ Global styles dengan :global() */
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
</style>Mari kita build complete weather dashboard application yang demonstrate semua Svelte fundamentals.
weather-dashboard/
├── src/
│ ├── components/
│ │ ├── WeatherCard.svelte
│ │ ├── SearchBar.svelte
│ │ ├── WeatherList.svelte
│ │ └── WeatherFilter.svelte
│ ├── stores/
│ │ └── weatherStore.ts
│ ├── types/
│ │ └── weather.ts
│ ├── App.svelte
│ └── App.css
├── package.json
└── vite.config.tsexport interface Weather {
id: string;
city: string;
country: string;
temperature: number;
feelsLike: number;
humidity: number;
windSpeed: number;
description: string;
icon: string;
timestamp: Date;
}
export type TemperatureUnit = 'celsius' | 'fahrenheit';
export type SortType = 'name' | 'temperature' | 'recent';import { writable, derived } from 'svelte/store';
import type { Weather, TemperatureUnit, SortType } from '../types/weather';
// ✅ Writable store untuk weather data
export const weatherList = writable<Weather[]>([]);
// ✅ Writable store untuk temperature unit
export const temperatureUnit = writable<TemperatureUnit>('celsius');
// ✅ Writable store untuk sort type
export const sortType = writable<SortType>('recent');
// ✅ Derived store untuk sorted dan filtered weather
export const sortedWeather = derived(
[weatherList, sortType],
([$weatherList, $sortType]) => {
const sorted = [...$weatherList];
switch ($sortType) {
case 'name':
return sorted.sort((a, b) => a.city.localeCompare(b.city));
case 'temperature':
return sorted.sort((a, b) => b.temperature - a.temperature);
case 'recent':
default:
return sorted.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
}
}
);
// ✅ Derived store untuk weather statistics
export const weatherStats = derived(
weatherList,
($weatherList) => {
if ($weatherList.length === 0) {
return { total: 0, avgTemp: 0, hottest: null, coldest: null };
}
const temps = $weatherList.map(w => w.temperature);
const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;
const hottest = $weatherList.reduce((a, b) =>
a.temperature > b.temperature ? a : b
);
const coldest = $weatherList.reduce((a, b) =>
a.temperature < b.temperature ? a : b
);
return {
total: $weatherList.length,
avgTemp: Math.round(avgTemp),
hottest,
coldest
};
}
);
// ✅ Store functions
export function addWeather(weather: Weather) {
weatherList.update(list => [weather, ...list]);
}
export function removeWeather(id: string) {
weatherList.update(list => list.filter(w => w.id !== id));
}
export function updateWeather(id: string, updates: Partial<Weather>) {
weatherList.update(list =>
list.map(w => w.id === id ? { ...w, ...updates } : w)
);
}
export function clearWeather() {
weatherList.set([]);
}<script lang="ts">
import { temperatureUnit } from '../stores/weatherStore';
import type { Weather } from '../types/weather';
export let weather: Weather;
export let onDelete: (id: string) => void;
let isExpanded = false;
$: displayTemp = $temperatureUnit === 'fahrenheit'
? Math.round((weather.temperature * 9/5) + 32)
: weather.temperature;
$: feelsLikeTemp = $temperatureUnit === 'fahrenheit'
? Math.round((weather.feelsLike * 9/5) + 32)
: weather.feelsLike;
const tempUnit = $temperatureUnit === 'fahrenheit' ? '°F' : '°C';
</script>
<div class="card" class:expanded={isExpanded}>
<div class="card-header" on:click={() => isExpanded = !isExpanded}>
<div class="location">
<h3>{weather.city}, {weather.country}</h3>
<p class="description">{weather.description}</p>
</div>
<div class="temperature">
<span class="value">{displayTemp}{tempUnit}</span>
<span class="icon">{weather.icon}</span>
</div>
</div>
{#if isExpanded}
<div class="card-details" transition:slide>
<div class="detail-row">
<span class="label">Feels Like:</span>
<span class="value">{feelsLikeTemp}{tempUnit}</span>
</div>
<div class="detail-row">
<span class="label">Humidity:</span>
<span class="value">{weather.humidity}%</span>
</div>
<div class="detail-row">
<span class="label">Wind Speed:</span>
<span class="value">{weather.windSpeed} m/s</span>
</div>
<div class="detail-row">
<span class="label">Updated:</span>
<span class="value">{new Date(weather.timestamp).toLocaleTimeString()}</span>
</div>
</div>
{/if}
<button class="delete-btn" on:click={() => onDelete(weather.id)}>
✕
</button>
</div>
<style>
.card {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.location h3 {
margin: 0;
font-size: 1.1rem;
color: #333;
}
.description {
margin: 4px 0 0 0;
font-size: 0.875rem;
color: #666;
text-transform: capitalize;
}
.temperature {
display: flex;
align-items: center;
gap: 12px;
}
.value {
font-size: 1.5rem;
font-weight: 600;
color: #667eea;
}
.icon {
font-size: 2rem;
}
.card-details {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e5e7eb;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 0.875rem;
}
.label {
color: #666;
}
.delete-btn {
position: absolute;
top: 8px;
right: 8px;
background: #ef4444;
color: white;
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.card:hover .delete-btn {
opacity: 1;
}
.card {
position: relative;
}
</style><script lang="ts">
import { addWeather } from '../stores/weatherStore';
import type { Weather } from '../types/weather';
let searchInput = '';
let isLoading = false;
let error = '';
async function handleSearch() {
if (!searchInput.trim()) return;
isLoading = true;
error = '';
try {
// Mock API call - replace dengan real weather API
const mockWeather: Weather = {
id: Date.now().toString(),
city: searchInput,
country: 'Country',
temperature: Math.round(Math.random() * 30),
feelsLike: Math.round(Math.random() * 30),
humidity: Math.round(Math.random() * 100),
windSpeed: Math.round(Math.random() * 20),
description: 'Partly cloudy',
icon: '⛅',
timestamp: new Date()
};
addWeather(mockWeather);
searchInput = '';
} catch (err) {
error = 'Failed to fetch weather data';
} finally {
isLoading = false;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
handleSearch();
}
}
</script>
<div class="search-bar">
<input
type="text"
bind:value={searchInput}
on:keydown={handleKeydown}
placeholder="Search for a city..."
disabled={isLoading}
/>
<button on:click={handleSearch} disabled={isLoading || !searchInput.trim()}>
{isLoading ? 'Loading...' : 'Search'}
</button>
{#if error}
<p class="error">{error}</p>
{/if}
</div>
<style>
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s;
}
input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
button:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #ef4444;
font-size: 0.875rem;
margin-top: 8px;
}
</style><script lang="ts">
import { sortedWeather } from '../stores/weatherStore';
import WeatherCard from './WeatherCard.svelte';
export let onDelete: (id: string) => void;
</script>
<div class="weather-list">
{#if $sortedWeather.length === 0}
<div class="empty-state">
<p>No weather data yet. Search for a city to get started!</p>
</div>
{:else}
{#each $sortedWeather as weather (weather.id)}
<WeatherCard {weather} {onDelete} />
{/each}
{/if}
</div>
<style>
.weather-list {
flex: 1;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
}
</style><script lang="ts">
import { temperatureUnit, sortType, weatherStats } from '../stores/weatherStore';
import type { TemperatureUnit, SortType } from '../types/weather';
</script>
<div class="filter-container">
<div class="filter-group">
<label>Temperature Unit:</label>
<select bind:value={$temperatureUnit}>
<option value="celsius">Celsius (°C)</option>
<option value="fahrenheit">Fahrenheit (°F)</option>
</select>
</div>
<div class="filter-group">
<label>Sort By:</label>
<select bind:value={$sortType}>
<option value="recent">Most Recent</option>
<option value="name">City Name</option>
<option value="temperature">Temperature</option>
</select>
</div>
<div class="stats">
<div class="stat">
<span class="label">Total:</span>
<span class="value">{$weatherStats.total}</span>
</div>
<div class="stat">
<span class="label">Avg Temp:</span>
<span class="value">{$weatherStats.avgTemp}°</span>
</div>
{#if $weatherStats.hottest}
<div class="stat">
<span class="label">Hottest:</span>
<span class="value">{$weatherStats.hottest.city}</span>
</div>
{/if}
</div>
</div>
<style>
.filter-container {
background: #f9fafb;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
gap: 20px;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
label {
font-weight: 600;
color: #374151;
font-size: 0.875rem;
}
select {
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.875rem;
cursor: pointer;
}
.stats {
display: flex;
gap: 16px;
margin-left: auto;
}
.stat {
display: flex;
gap: 6px;
font-size: 0.875rem;
}
.label {
color: #6b7280;
}
.value {
font-weight: 600;
color: #667eea;
}
</style><script lang="ts">
import { removeWeather, clearWeather } from './stores/weatherStore';
import SearchBar from './components/SearchBar.svelte';
import WeatherList from './components/WeatherList.svelte';
import WeatherFilter from './components/WeatherFilter.svelte';
</script>
<div class="app">
<header class="app-header">
<h1>Weather Dashboard</h1>
<p>Track weather across multiple cities</p>
</header>
<main class="app-main">
<div class="container">
<SearchBar />
<WeatherFilter />
<WeatherList onDelete={removeWeather} />
<button class="clear-btn" on:click={clearWeather}>
Clear All
</button>
</div>
</main>
</div>
<style>
:global(body) {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
text-align: center;
color: white;
padding: 40px 20px;
}
.app-header h1 {
margin: 0;
font-size: 2.5rem;
}
.app-header p {
margin: 10px 0 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
.app-main {
flex: 1;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
}
.clear-btn {
align-self: flex-end;
padding: 10px 20px;
background: #ef4444;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 20px;
}
.clear-btn:hover {
background: #dc2626;
transform: translateY(-2px);
}
@media (max-width: 640px) {
.app-header h1 {
font-size: 1.8rem;
}
.container {
padding: 20px;
}
}
</style>* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
width: 100%;
height: 100%;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Transitions */
@media (prefers-reduced-motion: no-preference) {
* {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
}// ✅ Gunakan reactive declarations untuk derived state
$: doubled = count * 2;
// ✅ Gunakan reactive blocks untuk side effects
$: if (count > 10) {
console.log('count exceeded 10');
}
// ❌ Hindari unnecessary reactivity
// Jangan gunakan $: untuk simple values yang tidak change
let staticValue = 'hello'; // No need for $:// ✅ Gunakan stores untuk shared state
import { count } from './stores';
// ✅ Subscribe ke stores dengan $ prefix
<p>{$count}</p>
// ✅ Gunakan derived stores untuk computed values
export const doubled = derived(count, $count => $count * 2);
// ❌ Hindari storing UI state dalam stores
// Keep UI state local ke components// ✅ Keep components focused dan small
// ✅ Gunakan meaningful prop names
// ✅ Provide default values untuk props
export let title = 'Default Title';
// ✅ Gunakan slots untuk composition
<slot />
// ❌ Hindari prop drilling
// Gunakan stores atau context untuk deeply nested data// ✅ Gunakan keyed each blocks untuk lists
{#each items as item (item.id)}
<Item {item} />
{/each}
// ✅ Gunakan animations sparingly
// ✅ Lazy load components ketika possible
// ✅ Gunakan onMount untuk expensive operations
// ❌ Hindari creating functions dalam templates
// ❌ Jangan gunakan index sebagai key dalam each blocks// ✅ Group related logic together
// ✅ Extract reusable components
// ✅ Gunakan TypeScript untuk type safety
// ✅ Keep styles scoped ke components
// ✅ Gunakan meaningful variable names// ❌ Wrong - tidak akan reactive
<p>{count}</p>
// ✅ Correct - reactive subscription
<p>{$count}</p>
// ✅ Atau subscribe manually
<script>
import { count } from './stores';
let unsubscribe;
onMount(() => {
unsubscribe = count.subscribe(value => {
// handle value
});
return unsubscribe;
});
</script>// ❌ Wrong - mutation tidak akan trigger reactivity
items.push(newItem);
// ✅ Correct - create new array
items = [...items, newItem];
// ✅ Atau gunakan array methods yang return new arrays
items = items.filter(item => item.id !== id);
// ❌ Wrong - object mutation
user.name = 'John';
// ✅ Correct - create new object
user = { ...user, name: 'John' };// ❌ Wrong - dapat cause issues dengan reordering
{#each items as item, index (index)}
<Item {item} />
{/each}
// ✅ Correct - gunakan unique identifier
{#each items as item (item.id)}
<Item {item} />
{/each}// ❌ Wrong - memory leak
onMount(() => {
const unsubscribe = store.subscribe(value => {
// handle value
});
// forgot to return cleanup
});
// ✅ Correct - return cleanup function
onMount(() => {
const unsubscribe = store.subscribe(value => {
// handle value
});
return unsubscribe;
});// ❌ Wrong - side effect dalam reactive declaration
$: {
fetch(`/api/user/${userId}`).then(r => r.json());
}
// ✅ Correct - gunakan onMount atau reactive block dengan proper handling
onMount(async () => {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
});
// ✅ Atau gunakan reactive block dengan proper structure
$: if (userId) {
loadUser(userId);
}Svelte merepresentasikan fundamental shift dalam how we think tentang frontend frameworks. Dengan move work ke compile time, Svelte delivers smaller bundles, better performance, dan more intuitive developer experience.
Weather dashboard application yang kita build demonstrate semua core Svelte concepts dalam action. Understanding reactive declarations, stores, components, dan lifecycle hooks adalah essential untuk building scalable Svelte applications.
Key takeaways:
Next steps:
Svelte adalah journey menuju simpler, faster, dan more enjoyable web development. Keep learning, building, dan pushing boundaries dari apa yang possible.