29

I am trying to get my head around the svelte 3 reactivity thing...

  1. I wanted to force refreshing a UI on a button click. I am using a custom component AsyncFetcher that accepts HTTP post data, and returns data object (http post result) for its slot.

  2. I wanted to have a disable functionality. So when the "Disable" button is clicked an http api is called followed by a refresh of the data view.

<script>
    export let id

    function onDisable() {
        fetch('disable-api-url', {id: id})
        // Then ??
        // What to do after the fetch call, to refresh the view
    }
</script>

<AsyncFetcher postParam={id} let:data>
    {data.name}

    <button on:click={??}>Refresh</button>
    <button on:click={onDisable}>Disable Item</button>
</AsyncFetcher>

I tried doing on:click={() => id=id} to trick it to refresh to no avail. If id would have been an object rather than string id={...id} would have worked, which unfortunately, is not the case here.

What would be a correct way to achieve this?

8 Answers 8

23

Using a component to manage fetches is very unconventional. Typically you'd fetch data inside onMount, or in an event handler:

<script>
  import { onMount } from 'svelte';

  let initialData;
  let otherData;

  onMount(async () => {
    const res = await fetch('some-url');
    initialData = await res.json();
  });

  async function update() {
    const res = await fetch('some-other-url');
    otherData = await res.json();
  }
</script>

{#if initialData}
  <p>the data is {initialData.something}</p>
{/if}

<button on:click={update}>update</button>
Sign up to request clarification or add additional context in comments.

Comments

14

While Rich Harris gives a completely serviceable answer, here's a solution for forcing Svelte to update a component to reflect an external change of its data (also posted here).

main.js; vanilla from the examples online, no special changes:

import App from './App.svelte';

var app = new App({
    target: document.body
});

export default app;

index.html; Note window.neek = {...}:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Svelte app</title>
    <script>
        window.neek = { nick: true, camp: { bell: "Neek" }, counter: 0 };
    </script>
    <script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>

App.svelte; Note $: notneek = window.neek and window.neek.update = ...:

<script>
    let name = 'world';
    $: notneek = window.neek;

    function handleClick() {
        notneek.counter += 1;
    }

    window.neek.update = function () {
        notneek = notneek;
    }
</script>

<h1>Hello { notneek.camp.bell }!</h1>

<button on:click={handleClick}>
    Clicked {notneek.counter} {notneek.counter === 1 ? 'time' : 'times'}
</button>

Since the update function is within the scope of App.svelte, it is able to force the re-render when called via window.neek.update(). This setup uses window.neek.counter for the internal data utilized by the button (via notneek.counter) and allows for the deep properties (e.g. neek.camp.bell = "ish") to be updated outside of the component and reflected once neek.update() is called.

In the console, type window.neek.camp.bell = "Bill" and note that Hello Neek! has not been updated. Now, type window.neek.update() in the console and the UI will update to Hello Bill!.

Best of all, you can be as granular as you want within the update function so that only the pieces you want to be synchronized will be.

Comments

8

To fetch data use the await block:

<script>
    async function fetchData() {
        const res = await fetch('/api')
        const data = await res.json()

        if (res.ok) {
            return data
        } else {
            throw new Error(data)
        }
    }
</script>

<style>
    .error {
        color: red;
    }
</style>

{#await fetchData}
    <p>Fetching...</p>
{:then data}
    <div>{JSON.stringify(data)}</div>
{:catch error}
    <div class="error">{error.message}</div>
{/await}

To refresh the data you need to trigger a rerender by updating a piece of related state, since this will rerun the await block. You can trigger a rerender by storing the fetch function in a piece of state and reassigning it when the refresh button is clicked:

<script>
    async function fetchData() {
        const res = await fetch('/api')
        const data = await res.json

        if (res.ok) {
            return data
        } else {
            throw new Error(data)
        }
    }

    let promise = fetchData()
</script>

<style>
    .error {
        color: red;
    }
</style>

<button on:click="{() => {promise = fetchdata()}}">Refresh</button>

{#await promise}
    <p>Fetching...</p>
{:then data}
    <div>{JSON.stringify(data)}</div>
{:catch error}
    <div class="error">{error.message}</div>
{/await}

1 Comment

Wow! this was an awesome answer. I just refactored my code from onMount() to this and it worked right away. Such a more beautiful solution. Thank you!
6

The key (no pun intended) to make a component fully reload, and not just update the inside values, is to use {#key value_to_watch} like:

{#key category_on}
<Testone a={category_on} />
{/key}

If category_on changes the <Testone/> component is fully reloaded REPL

Comments

4

Here's a somewhat hacky solution for forcing a rerender of a component not dependent on external data:

<script>
    // Await immediately resolved promise to react to value change.
    const forceUpdate = async (_) => {};
    let doRerender = 0;
</script>
{#await forceUpdate(doRerender) then _}
    <ForcedToRerender on:click={() => doRerender++} />
{/await}

I tried to find a more "native" solution, but this is what I ended up with. REPL: https://svelte.dev/repl/2dc5c7ca82bc450f8f7dd25d2be577b1?version=3.43.0

1 Comment

This worked perfectly for my use case. I had a list of items and I wanted to do something on a ctrl+click, but on OSX that brings up a context menu, so I had to use on:contextmenu|preventDefault but then my UI did not update, even though the state was being set. Using this and adding a doRerender++ to my contextmenu handler works perfectly.
2

I did this (made the component disappear and reappear with a timer):

<script>
    import ForcedToRerender from './ForcedToRerender.svelte'
    let visible = true
    let rerender = () =>
    {
        visible=false
        setTimeout(()=>{visible = true}, 100)
    }
</script>
{#if visible}
    <ForcedToRerender />
{/if}
<button on:click={rerender}>Rerender</button>

ForcedToRerender.svelte:

<script>
  import { onMount } from 'svelte'
    let num = 0
    let rnd = () => num = Math.random()
    onMount(rnd)
</script>
<div on:click={rnd}>
    {num}
</div>

This works, as you can see here.

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
2

sub.svelte.ts

    LastUpdate: number = $state(0);
    OnPropertyChanged() {
        this.LastUpdate = Date.now()
    }

page.svelte

    function(){
        ... 
        sub.OnPropertyChanged()  ;
        ...
    }
{#key sub.LastUpdate}
<Content ...
{/key}

1 Comment

Interesting. What if I want to trigger the key from a deeply nested component on page.svelte? Should I use setContext / getContext?
1

in my case, svelte did not flush the output,
cos i blocked the javascript event loop by running a benchmark at 100% cpu

in this case, the trick is to manually unblock the event loop with await sleep(10)

<script>
  function sleep(millisec = 0) {
    return new Promise((resolve, reject) => {
      setTimeout(_ => resolve(), millisec);
    });
  };
  let result = '';
  async function runBenchmark() {
    for (let step = 0; step < 10; step++) {

      // this needs 100% cpu, so no time for svelte render
      cpuburn(); result += `${step}: 1.234 sec\n`;

      // unblock the JS event loop, so svelte can render
      await sleep(10);
    }
  }
</script>

<pre>{result}</pre>

here is a repl (but currently it triggers a bug in the repl runtime)

solving this with synchronous function calls is probably not possible
(something like a $$svelte.forceTickSync())

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.