Svelte API多次加载并保留旧数据。

huangapple go评论59阅读模式
英文:

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 &lt;Feed /&gt; component:

&lt;script&gt;
	import Post from &#39;$lib/components/post/Post.svelte&#39;;
	import InfiniteScroll from &#39;$lib/components/structure/InfiniteScroll.svelte&#39;;
	import { createEventDispatcher } from &#39;svelte&#39;;
	import { api } from &#39;$lib/utils/api&#39;;

	import { onMount} from &#39;svelte&#39;;

	export let args = {};

	let currentPage = 1;
	let data = [];
	let newBatch = [];

	const dispatch = createEventDispatcher();

	async function fetchData() {
		await api.post.index(currentPage, args)
			.then((response) =&gt; {
				newBatch = response.data.data;
			})
	}

	onMount(() =&gt; {
		fetchData();
	});

	$: data = [...data, ...newBatch];
&lt;/script&gt;

	{#each data as post}
		&lt;Post {post} /&gt;
	{/each}

	&lt;!-- No more items --&gt;
	{#if newBatch.length}
		&lt;InfiniteScroll
			hasMore={newBatch.length}
			threshold={100}
			on:loadMore={() =&gt; {
				currentPage++;
				fetchData();
			}}
		/&gt;
	{:else}
		&lt;p class=&quot;mt-5 text-gray-400 text-center&quot;&gt;No more posts.&lt;/p&gt;
	{/if}

This is the skeleton for &lt;InfiniteScroll /&gt;:

&lt;script&gt;
	import { Preloader } from &#39;konsta/svelte&#39;;
	import { onDestroy, createEventDispatcher } from &#39;svelte&#39;;

	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(&#39;scroll&#39;, onScroll);
			element.addEventListener(&#39;resize&#39;, onScroll);
		}
	}

	const onScroll = (e) =&gt; {
		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 &lt;= threshold) {
			if (!isLoadMore &amp;&amp; hasMore) {
				dispatch(&#39;loadMore&#39;);
			}
			isLoadMore = true;
		} else {
			isLoadMore = false;
		}
	};

	onDestroy(() =&gt; {
		if (component || elementScroll) {
			const element = elementScroll ? elementScroll : component.parentNode;

			element.removeEventListener(&#39;scroll&#39;, null);
			element.removeEventListener(&#39;resize&#39;, null);
		}
	});
&lt;/script&gt;

&lt;div class=&quot;text-center mx-auto&quot;&gt;
	&lt;Preloader class=&quot;mt-4&quot; colors={{ iconIos: &#39;text-primary&#39; }} size=&quot;w-8 h-8&quot; /&gt;
&lt;/div&gt;
&lt;div bind:this={component} style=&quot;width:0px&quot; /&gt;

Then I have a user/[slug]/+page.svelte page where I load the profile data and the profile posts based on the user slug.

&lt;script&gt;
	import ProfileImage from &#39;$lib/components/ProfileImage.svelte&#39;;
	import { page } from &#39;$app/stores&#39;;
	import Feed from &#39;$lib/components/post/Feed.svelte&#39;;
	import { api } from &#39;$lib/utils/api&#39;;

	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(() =&gt; {
			loading = false;
		});
		friendship = user.data.friendship;
	}
&lt;/script&gt;

&lt;div class=&quot;w-1/3 mx-auto&quot;&gt;
	&lt;ProfileImage user={user.data.user} size=&quot;w-full&quot; link={false} /&gt;
&lt;/div&gt;

&lt;h1 class=&quot;text-center font-bold text-xl&quot;&gt;
	{user.data.user.full_name || &#39;-&#39;}
&lt;/h1&gt;

{#key userSlug}
	&lt;Feed
		args={{ user_slug: userSlug }}
	/&gt;
{/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 &lt;InfiniteScroll/&gt; inside &lt;Feed/&gt; 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 &lt;InfiniteScroll/&gt; 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 特殊元素,你可以完全删除 &lt;InfiniteScroll /&gt; 中的 addEventListenerremoveEventListener 调用。这可以确保你的事件监听器正确绑定,消除了中间的大型响应式块的需求,而且更加 "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 &lt;Feed/&gt; 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) =&gt; {
      newBatch = response.data.data
    })
    .finally(() =&gt; (isLoadMore = false));

}

Bind event listeners svelte:window in &lt;InfiniteScroll /&gt;

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 &lt;InfiniteScroll /&gt;. 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".

&lt;svelte:window bind:scrollY={onScroll} bind:resize={onScroll}/&gt;

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)}
  &lt;Preloader/&gt;
{:then user}
  &lt;h1 class=&quot;text-center font-bold text-xl&quot;&gt;
    {user.data.user.full_name || &#39;-&#39;}
  &lt;/h1&gt;
  {#key user.data.slug}
    &lt;Feed args={{user_slug: user.data.slug}}/&gt;
  {/key}
{/await}

huangapple
  • 本文由 发表于 2023年7月3日 17:30:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/76603481.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定