How to trigger/force update a Svelte component
Asked Answered
I

7

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?

Immorality answered 4/7, 2019 at 15:44 Comment(0)
S
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>
Sectary answered 4/7, 2019 at 19:56 Comment(0)
R
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.

Rintoul answered 14/12, 2019 at 5:12 Comment(0)
A
7

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}
Accelerant answered 25/12, 2021 at 19:46 Comment(1)
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!Dicast
N
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

Nonmetallic answered 23/9, 2021 at 12:26 Comment(1)
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.Aer
B
3

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

Bushmaster answered 10/9, 2023 at 14:31 Comment(0)
G
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.

Gezira answered 30/9, 2021 at 11:54 Comment(1)
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.Statis
M
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())

Morphogenesis answered 15/3, 2021 at 10:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.