I have a layout page and each component inherits from it. My problem is onMount function is being called twice for each component. It makes many issues because my onMount function contains calls to API. It means that calls to Api are doubled. For a long time, I did not know why it happens. Recently I noticed that the layout component has a wrapper that triggers the transition. When I removed transition onMount was called one time as expected. From another hand, I don't want to lose transition because the page looks bad. How to solve this problem?
PageTransitions.svelte:
<script>
import { fly } from "svelte/transition";
export let refresh = "";
</script>
{#key refresh}
<div in:fly={{ y: -50, duration: 250, delay: 300 }} out:fly={{ y: -50, duration: 250 }} class="flex flex-1">
<slot>Zawartość</slot>
</div>
{/key}
layout.svelte:
<script>
import { googleMap } from "./../stores.js";
import { page } from "$app/stores";
import Footer from "$lib/Footer.svelte";
import Header from "$lib/Header.svelte";
import PageTransitions from "$lib/PageTransitions.svelte";
import Notifications from "svelte-notifications";
import "../app.css";
import { GOOGLE_API_KEY } from "$lib/constants.js";
import { setContext } from "svelte";
import { writable } from "svelte/store";
const url = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&libraries=places,directions`;
let isSideMenuOpen = setContext("isSideMenuOpen", writable(false));
$: loaded = $googleMap.loaded;
const onLoad = () => {
$googleMap.loaded = true;
console.log("Google Maps SDK loaded…", window.google);
};
</script>
<svelte:head>
{#if !loaded}
<script
src={url}
type="application/javascript"
defer
async
on:load={onLoad}></script>
{/if}
</svelte:head>
<Notifications>
<div
class="h-screen flex flex-col bg-gray-50 dark:bg-gray-900"
class:overflow-hidden={isSideMenuOpen}
>
<Header />
<PageTransitions refresh={$page.path}>
<slot>Strona</slot>
</PageTransitions>
<Footer />
</div>
</Notifications>
modules.svelte (example component):
<script context="module">
export const ssr = false;
export const prerender = true;
export async function load({ session }) {
if (!session.authenticated) {
return {
status: 302,
redirect: "/auth/login",
};
}
return {
props: {
authenticated: session.authenticated,
token: session.token,
user: session.user,
},
};
}
</script>
<script>
import { slide } from 'svelte/transition';
import GenericTable from "$lib/GenericTable.svelte";
import { profile, modules } from "../../stores";
import { getModuleDataURL, getModuleTypeDataURL, get, del } from "$lib/api";
import GenericRow from "$lib/Rows/ModuleRow.svelte";
import { onMount } from "svelte";
import Loader from "$lib/Loader.svelte";
import { getNotificationsContext } from "svelte-notifications";
const { addNotification } = getNotificationsContext();
let loading = false;
export let authenticated;
export let token;
export let user;
const siteHeader = "Moduły";
const rootPath = "modules";
let filters = {
moduleType: null,
imei: "",
serialNumber: "",
phoneNumber: "",
isActive: null,
desc: "",
id: null,
};
const filtersChange = () => {
let filteredItems = [...$modules.items];
$modules.filters = false;
if (
filters.moduleType ||
filters.id ||
filters.desc.length > 2 ||
filters.isActive !== null ||
filters.imei.length > 2 ||
filters.serialNumber.length > 2 ||
filters.phoneNumber.length > 2
) {
$modules.filters = true;
}
if (filters.moduleType) {
filteredItems = filteredItems.filter(
(item) => item.moduleType.id === filters.moduleType.value
);
}
if (filters.isActive !== null) {
filteredItems = filteredItems.filter(
(item) => item.isActive === filters.isActive
);
}
if (filters.imei.length > 2) {
filteredItems = filteredItems.filter((item) =>
item.imei.includes(filters.imei)
);
}
if (filters.serialNumber.length > 2) {
filteredItems = filteredItems.filter((item) =>
item.serialNumber.includes(filters.serialNumber)
);
}
if (filters.phoneNumber.length > 2) {
filteredItems = filteredItems.filter((item) =>
item.phoneNumber.includes(filters.phoneNumber)
);
}
if (filters.desc.length > 2) {
filteredItems = filteredItems.filter((item) =>
item.desc ? item.desc.includes(filters.desc) : false
);
}
if (filters.id === "DESC") {
filteredItems.sort((a, b) => (a.id > b.id ? 1 : -1));
} else if (filters.id === "ASC") {
filteredItems.sort((a, b) => (a.id < b.id ? 1 : -1));
}
$modules.filteredModules = filteredItems;
};
const urlModuleType = getModuleTypeDataURL();
const urlModuleData = getModuleDataURL();
const deleteItem = async (id) => {
loading = true;
const url = getModuleDataURL() + "?id=" + id;
try {
const result = await del(url, token, false);
if (result.status) {
$modules.items = $modules.items.filter((item) => id !== item.id);
$modules.filteredModules = $modules.filteredModules.filter(
(item) => id !== item.id
);
addNotification({
text: "Usunięto moduł o id - " + id,
position: "bottom-right",
type: "success",
removeAfter: 4000,
});
} else throw result.message;
} catch (err) {
console.error(err);
addNotification({
text: err,
position: "bottom-right",
type: "danger",
removeAfter: 4000,
});
}
loading = false;
};
onMount(async () => {
loading = true;
$modules.filters = false;
$modules.filteredGroups = [];
try {
const resultModules = await get(
urlModuleData + "?$orderby=isActive desc,id",
token
);
if (resultModules.data) $modules.items = resultModules.data.items;
const resultModuleTypes = await get(urlModuleType, token);
if (resultModuleTypes.data)
$modules.moduleTypes = resultModuleTypes.data.items;
} catch (err) {
console.error(err);
addNotification({
text: "Podczas komunikacji z serwerem wystąpił błąd. Spróbuj raz jeszcze. Jeśli problem się powtórzy, skontaktuj się z administratorem aplikacji.",
position: "bottom-right",
type: "danger",
removeAfter: 4000,
});
}
loading = false;
});
let virtualListData = [];
$: virtualListData = $modules.filters
? $modules.filteredModules
: $modules.items;
let moduleTypes = [];
$: moduleTypes = $modules.moduleTypes.map((item) => ({
value: item.id,
label: item.type,
}));
const widths = ["5%", "15%", "10%", "10%", "15%", "15%", "20%", "10%"];
let listTableHead = [];
$: listTableHead = [
{
id: 1,
name: "ID",
filter: true,
type: "id",
},
{
id: 2,
name: "IMEI",
filter: true,
label: "imei",
type: "text",
placeholder: "Wpisz minimum 3 znaki",
onInput: filtersChange,
},
{
id: 3,
name: "Typ modułu",
filter: true,
label: "moduleTypes",
type: "select",
placeholder: "-",
items: moduleTypes,
select: (e) => {
filters.moduleType = e.detail;
filtersChange();
},
clear: () => {
filters.moduleType = null;
filtersChange();
},
},
{
id: 4,
name: "Aktywność",
filter: true,
label: "isActive",
type: "select",
placeholder: "-",
items: [
{ value: true, label: "Aktywny" },
{ value: false, label: "Nieaktywny" },
],
select: (e) => {
filters.isActive = e.detail.value;
filtersChange();
},
clear: () => {
filters.isActive = null;
filtersChange();
},
},
{
id: 5,
name: "Numer telefonu",
filter: true,
label: "phoneNumber",
type: "text",
placeholder: "Wpisz minimum 3 znaki",
onInput: filtersChange,
},
{
id: 6,
name: "Numer seryjny",
filter: true,
label: "serialNumber",
type: "text",
placeholder: "Wpisz minimum 3 znaki",
onInput: filtersChange,
},
{
id: 7,
name: "Opis",
filter: true,
label: "desc",
type: "text",
placeholder: "Wpisz minimum 3 znaki",
onInput: filtersChange,
},
{
id: 8,
name: "Dodaj nowy moduł",
type: "addNew",
},
];
</script>
<main class="w-full flex flex-1 overflow-y-auto bg-red-100">
<div class="p-2 overflow-y-auto flex flex-1">
{#if loading}
<Loader />
{/if}
<div
transition:slide
style="display:{loading ? 'none' : 'block'}"
class="flex flex-col flex-1 items-center justify-center"
>
<GenericTable
{siteHeader}
{filtersChange}
{deleteItem}
{virtualListData}
{widths}
{listTableHead}
{filters}
{rootPath}
{GenericRow}
/>
</div>
</div>
</main>
What I tried:
Why does this happen?
There is a related issue documented here (without solution).
The root cause is a timing issue.
layout.svelte
changes the <slot/>
in PageTransitions.svelte
.
This causes the new slot/page to load and even fire the onMount.
The new page would also be visible on screen for milliseconds, since no transition happend yet!
After the slot was changed, Svelte triggers a $page
store to update.
In you code, this causes the transition to update ({#key refresh}
) and therefore loading the <slot/>
again.
There is no way to change the timing between the <slot/>
exchange and $page
store set without modifing Svelte itself.
This code fires the transition and only causes one component mount.
layout.svelte:
<script context="module">
export const load = async ({ url }) => ({ props: { refresh: url } });
</script>
<script>
import { googleMap } from "./../stores.js";
import Footer from "$lib/Footer.svelte";
import Header from "$lib/Header.svelte";
import PageTransitions from "$lib/PageTransitions.svelte";
import Notifications from "svelte-notifications";
import "../app.css";
import { GOOGLE_API_KEY } from "$lib/constants.js";
import { setContext } from "svelte";
import { writable } from "svelte/store";
export let refresh;
const url = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&libraries=places,directions`;
let isSideMenuOpen = setContext("isSideMenuOpen", writable(false));
$: loaded = $googleMap.loaded;
const onLoad = () => {
$googleMap.loaded = true;
console.log("Google Maps SDK loaded…", window.google);
};
</script>
<svelte:head>
{#if !loaded}
<script
src={url}
type="application/javascript"
defer
async
on:load={onLoad}></script>
{/if}
</svelte:head>
<Notifications>
<div
class="h-screen flex flex-col bg-gray-50 dark:bg-gray-900"
class:overflow-hidden={isSideMenuOpen}
>
<Header />
<PageTransitions {refresh}>
<slot>Strona</slot>
</PageTransitions>
<Footer />
</div>
</Notifications>