How to trigger/force update a Svelte component

19,085

Solution 1

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>

Solution 2

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.

Solution 3

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())

Solution 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

Solution 5

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}
Share:
19,085
Leone
Author by

Leone

Updated on June 18, 2022

Comments

  • Leone
    Leone almost 2 years

    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?