英文:
Svelte API loads multiple times and keeps old data
问题
以下是您要翻译的内容:
Initial note: this is an ssr = false
project and I cannot/do not use server data, all API calls are made with axios
inside pages/components. I removed as much code as I could to keep only what matters.
I have a <Feed />
component:
<script>
import Post from '$lib/components/post/Post.svelte';
import InfiniteScroll from '$lib/components/structure/InfiniteScroll.svelte';
import { createEventDispatcher } from 'svelte';
import { api } from '$lib/utils/api';
import { onMount} from 'svelte';
export let args = {};
let currentPage = 1;
let data = [];
let newBatch = [];
const dispatch = createEventDispatcher();
async function fetchData() {
await api.post.index(currentPage, args)
.then((response) => {
newBatch = response.data.data;
})
}
onMount(() => {
fetchData();
});
$: data = [...data, ...newBatch];
</script>
{#each data as post}
<Post {post} />
{/each}
<!-- No more items -->
{#if newBatch.length}
<InfiniteScroll
hasMore={newBatch.length}
threshold={100}
on:loadMore={() => {
currentPage++;
fetchData();
}}
/>
{:else}
<p class="mt-5 text-gray-400 text-center">No more posts.</p>
{/if}
This is the skeleton for <InfiniteScroll />
:
<script>
import { Preloader } from 'konsta/svelte';
import { onDestroy, createEventDispatcher } from 'svelte';
export let threshold = 0;
export let horizontal = false;
// export let elementScroll;
let elementScroll;
export let hasMore = true;
const dispatch = createEventDispatcher();
let isLoadMore = false;
let component;
$: {
if (component || elementScroll) {
const element = elementScroll ? elementScroll : component.parentNode;
element.addEventListener('scroll', onScroll);
element.addEventListener('resize', onScroll);
}
}
const onScroll = (e) => {
const element = e.target;
const offset = horizontal
? e.target.scrollWidth - e.target.clientWidth - e.target.scrollLeft
: e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop;
if (offset <= threshold) {
if (!isLoadMore && hasMore) {
dispatch('loadMore');
}
isLoadMore = true;
} else {
isLoadMore = false;
}
};
onDestroy(() => {
if (component || elementScroll) {
const element = elementScroll ? elementScroll : component.parentNode;
element.removeEventListener('scroll', null);
element.removeEventListener('resize', null);
}
});
</script>
<div class="text-center mx-auto">
<Preloader class="mt-4" colors={{ iconIos: 'text-primary' }} size="w-8 h-8" />
</div>
<div bind:this={component} style="width:0px" />
Then I have a user/[slug]/+page.svelte
page where I load the profile data and the profile posts based on the user slug.
<script>
import ProfileImage from '$lib/components/ProfileImage.svelte';
import { page } from '$app/stores';
import Feed from '$lib/components/post/Feed.svelte';
import { api } from '$lib/utils/api';
export let userSlug = null;
export let ownProfile = null;
let loading = true;
let user = { data: { user: {}, friends: [], friendship: null } };
let friendship = null;
$: {
fetchData($page.params.slug);
}
async function fetchData(slug) {
loading = true;
userSlug = ownProfile ? null : slug;
user = await api.user.show(userSlug).finally(() => {
loading = false;
});
friendship = user.data.friendship;
}
</script>
<div class="w-1/3 mx-auto">
<ProfileImage user={user.data.user} size="w-full" link={false} />
</div>
<h1 class="text-center font-bold text-xl">
{user.data.user.full_name || '-'}
</h1>
{#key userSlug}
<Feed
args={{ user_slug: userSlug }}
/>
{/key}
From within this page I can visit different profiles, and this caused the data not to reload (ie: changed from user/profile-a
to user/profile-b
, the data was still from profile a
) so I used $: { fetchData($page.params.slug) }
which hopefully is the right strategy.
But is probably what is now causing this issue:
When I scroll to the bottom, the <InfiniteScroll/>
inside <Feed/>
loads more posts, but as you can see from this video: https://imgur.com/a/tpVTMDd, when I visit Profile A, then Profile B, then Profile C and I scroll down to the bottom, the <InfiniteScroll/>
fires and calls the API of all profiles I visited and keeps in memory all the data running multiple API calls instead of the current profile only.
I think I have to do "destroy" of the element probably but I don't know how.
英文:
Initial note: this is an ssr = false
project and I cannot/do not use server data, all API calls are made with axios
inside pages/components. I removed as much code as I could to keep only what matters.
I have a <Feed />
component:
<script>
import Post from '$lib/components/post/Post.svelte';
import InfiniteScroll from '$lib/components/structure/InfiniteScroll.svelte';
import { createEventDispatcher } from 'svelte';
import { api } from '$lib/utils/api';
import { onMount} from 'svelte';
export let args = {};
let currentPage = 1;
let data = [];
let newBatch = [];
const dispatch = createEventDispatcher();
async function fetchData() {
await api.post.index(currentPage, args)
.then((response) => {
newBatch = response.data.data;
})
}
onMount(() => {
fetchData();
});
$: data = [...data, ...newBatch];
</script>
{#each data as post}
<Post {post} />
{/each}
<!-- No more items -->
{#if newBatch.length}
<InfiniteScroll
hasMore={newBatch.length}
threshold={100}
on:loadMore={() => {
currentPage++;
fetchData();
}}
/>
{:else}
<p class="mt-5 text-gray-400 text-center">No more posts.</p>
{/if}
This is the skeleton for <InfiniteScroll />
:
<script>
import { Preloader } from 'konsta/svelte';
import { onDestroy, createEventDispatcher } from 'svelte';
export let threshold = 0;
export let horizontal = false;
// export let elementScroll;
let elementScroll;
export let hasMore = true;
const dispatch = createEventDispatcher();
let isLoadMore = false;
let component;
$: {
if (component || elementScroll) {
const element = elementScroll ? elementScroll : component.parentNode;
element.addEventListener('scroll', onScroll);
element.addEventListener('resize', onScroll);
}
}
const onScroll = (e) => {
const element = e.target;
const offset = horizontal
? e.target.scrollWidth - e.target.clientWidth - e.target.scrollLeft
: e.target.scrollHeight - e.target.clientHeight - e.target.scrollTop;
if (offset <= threshold) {
if (!isLoadMore && hasMore) {
dispatch('loadMore');
}
isLoadMore = true;
} else {
isLoadMore = false;
}
};
onDestroy(() => {
if (component || elementScroll) {
const element = elementScroll ? elementScroll : component.parentNode;
element.removeEventListener('scroll', null);
element.removeEventListener('resize', null);
}
});
</script>
<div class="text-center mx-auto">
<Preloader class="mt-4" colors={{ iconIos: 'text-primary' }} size="w-8 h-8" />
</div>
<div bind:this={component} style="width:0px" />
Then I have a user/[slug]/+page.svelte
page where I load the profile data and the profile posts based on the user slug.
<script>
import ProfileImage from '$lib/components/ProfileImage.svelte';
import { page } from '$app/stores';
import Feed from '$lib/components/post/Feed.svelte';
import { api } from '$lib/utils/api';
export let userSlug = null;
export let ownProfile = null;
let loading = true;
let user = { data: { user: {}, friends: [], friendship: null } };
let friendship = null;
$: {
fetchData($page.params.slug);
}
async function fetchData(slug) {
loading = true;
userSlug = ownProfile ? null : slug;
user = await api.user.show(userSlug).finally(() => {
loading = false;
});
friendship = user.data.friendship;
}
</script>
<div class="w-1/3 mx-auto">
<ProfileImage user={user.data.user} size="w-full" link={false} />
</div>
<h1 class="text-center font-bold text-xl">
{user.data.user.full_name || '-'}
</h1>
{#key userSlug}
<Feed
args={{ user_slug: userSlug }}
/>
{/key}
From within this page I can visit different profiles, and this caused the data not to reload (ie: changed from user/profile-a
to user/profile-b
, the data was still from profile a
) so I used $: { fetchData($page.params.slug) }
which hopefully is the right strategy.
But is probably what is now causing this issue:
When I scroll to the bottom, the <InfiniteScroll/>
inside <Feed/>
loads more posts, but as you can see from this video: https://imgur.com/a/tpVTMDd, when I visit Profile A, then Profile B, then Profile C and I scroll down to the bottom, the <InfiniteScroll/>
fires and calls the API of all profiles I visited and keeps in memory all the data running multiple API calls instead of the current profile only.
I think I have to do "destroy" of the element probably but I don't know how.
答案1
得分: 3
你需要将事件处理程序传递给 removeEventListener
,目前你正在传递 null
。
另外,你在一个响应式块中添加监听器,我强烈建议不要这样做,因为你可能会意外地添加多个监听器。如果有一些条件逻辑来确定要监视的元素,请只执行一次,以确保监听器只会被添加到和从该元素中移除一次。
英文:
You have to pass the event handler to removeEventListener
, currently you are passing null
.
Also, you add listeners in a reactive block, I would highly recommend not doing that, as you might unexpectedly add more than one listener. If there is some conditional logic for determining the element to watch, do that once so you can be sure that listeners are only ever added to and removed from said element.
答案2
得分: 1
以下是翻译好的内容:
将 isLoadMore
提升到 <Feed/>
组件中
(与你在 +page.svelte
中一样)
这样做可以解决一个潜在的问题,即因为 JavaScript 事件循环的原因,loadMore
事件在滚动时触发,但在预期之外的时间处理,从而导致问题。
let isLoadMore = false;
async function fetchData() {
if (isLoadMore) return;
isLoadMore = true;
await api.post.index(currentPage, args)
.then((response) => {
newBatch = response.data.data
})
.finally(() => (isLoadMore = false));
}
在 <InfiniteScroll />
中绑定事件监听器到 svelte:window
我不确定你当前的代码 - 传递 null
作为 removeEventListener
的参数。我的理解是你必须传入函数作为第二个参数。
无论如何,如果你使用 svelte:window
特殊元素,你可以完全删除 <InfiniteScroll />
中的 addEventListener
和 removeEventListener
调用。这可以确保你的事件监听器正确绑定,消除了中间的大型响应式块的需求,而且更加 "Svelte 风格"。
<svelte:window bind:scrollY={onScroll} bind:resize={onScroll}/>
查看 Svelte 的 {#await}
块
与 $: { fetchData($page.params.slug) }
不同,你可能更喜欢像下面的代码一样做。这可能会避免你使用 null-ish/无效用户状态初始化组件。与你的帖子没有特定关联 - 只是注意到你没有使用这个功能。
{#await fetchData($page.params.slug)}
<Preloader/>
{:then user}
<h1 class="text-center font-bold text-xl">
{user.data.user.full_name || '-'}
</h1>
{#key user.data.slug}
<Feed args={{user_slug: user.data.slug}}/>
{/key}
{/await}
英文:
Might be worth creating a small Sveltekit repo on GitHub that actually reproduces the issues - but here's some potential problems and solutions:
Raise isLoadMore
to the <Feed/>
component
(same as you have in the +page.svelte
)
This fixes a potential issues where loadMore
events have fired onScroll but handled at unexpected times because of the JS event loop.
let isLoadMore = false;
async function fetchData() {
if (isLoadMore) return;
isLoadMore = true;
await api.post.index(currentPage, args)
.then((response) => {
newBatch = response.data.data
})
.finally(() => (isLoadMore = false));
}
Bind event listeners svelte:window
in <InfiniteScroll />
I'm not sure about your current code - passing null
as the argument to removeEventListener
. My understanding was you had to pass in the function as the second argument.
Regardless, if you use the svelte:window
special element, you can entirely remove the addEventListener
and removeEventListener
calls in <InfiniteScroll />
. This makes sure that your event listeners are bound correctly, removes the need for the big reactive block in the middle, and is just more "sveltey".
<svelte:window bind:scrollY={onScroll} bind:resize={onScroll}/>
Check out the Svelte {#await}
block
Instead of $: { fetchData($page.params.slug) }
, you might prefer to do something like the code below. It might save you from initializing components with null-ish/invalid user state. Not specifically related to your post - just noticed you weren't using the feature.
{#await fetchData($page.params.slug)}
<Preloader/>
{:then user}
<h1 class="text-center font-bold text-xl">
{user.data.user.full_name || '-'}
</h1>
{#key user.data.slug}
<Feed args={{user_slug: user.data.slug}}/>
{/key}
{/await}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论