Reputation: 561
I'm learning Svelte and I want to use data from one JSON API in three components. The data looks like this:
{
"stats": {
"currentYear": {
"total": 6,
"success": 6
},
"thirty": {
"total": 30,
"success": 28
},
"hundred": {
"total": 100,
"success": 92
},
"allTime": {
"total": 789,
"success": 728
}
},
"heatmap": {
...
},
"other": {
...
}
}
I retrieve the data via onMount
in the App.svelte
main component via async fetch, this works well. Then I want to pass each object to its corresponding component, so the stats
object gets passed to Stats.svelte
, the heatmap
object to Heatmap.svelte
etc.
To illustrate my issue, in Stats.svelte
I am trying to display percentage values for each time period, for example:
Also, the CSS class for each will be based on some threshold values to change the colour (x >= 95: green, 95 > x >= 90: yellow, x < 90: red).
So some basic computation is needed which I wanted to have in a generic function, like shown below.
The stats
object does get passed in from the parent component App.svelte
, and if all I wanted to do is to show its values in the HTML via the {#await}
block, this would work fine. However, I want to do some calculations, so I wanted to call a function that would use the stats
object's data, but I do not know how to call this function at the right moment. Calling it on onMount
does not work, because it's too early, the data coming in from the parent component has not yet been received.
<script>
import { onMount } from "svelte"
export let stats
let currentYearClass, currentYearStat
const calcPercentage = async (period) => {
currentYearStat = stats[period].currentYearSuccess * 100 / stats[period].currentYearTotal
currentYearClass = 'green'
}
onMount( async () => {
calcPercentage('currentYear')
})
</script>
<div id="stats">
{#await stats}
<div>Waiting for stats ...</div>
{:then stats}
<div class="{currentYearClass}" id="currentYear">{currentYearStat}</div>
...
...
{/await}
</div>
Upvotes: 3
Views: 7776
Reputation: 16451
There are several ways to do this, but one way would be to have the calcPercentage
take in the stats
as an argument and then call it reactively.
export let stats
let currentYearClass, currentYearStat
const calcPercentage = (stats, period) => {
currentYearStat = stats[persion}......
currentYearClass = 'green'
}
$: stats && calcPercentage(stats, 'currentYear')
Edit: Some Explaining
The first problem with the original solution, something you probably noticed is that the component is mounted without the correct data, making stats to be undefined.
The above solution works in two steps:
$: stats && calcPercentage(stats, 'currentYear')
This first part defines a reactive statement, it will check if stats evaluates to true, something it will do unless it is undefined, false or 0. If stats is true it will then execute the function.
The second part is a the same function as before, I added the stats argument here although it is strictly not required in this situation, as the function will, because of the earlier execute everytime stats changes and is a true-like value.
With those two in place, when mounting the reactive statement will fail because stats is undefined, the function is not executed. Once the data comes in, it will be re-evaluated, stats is no longer undefined and the function fires.
Extra
When the reactive statement would be of the form:
$: myfunction(myvar)
It will execute for every value change of myvar, even during the mounting (consider it going from non exisiting to undefined ?). This means you would have to move your check into the function itself, for some scenarios this might be desired, an example is where this actually part of an assignment and the function itself is defined outside the component
import heavyCalc from 'heavy/calc/function`
$: value = heavyCalc(otherValue)
Upvotes: 5
Reputation: 11
I'd propose to render the Stats.svelte
component only after the data is loaded using {#await}
:
In App.svelte
do this:
{#await promise}
<div>Loading...<div>
{:then stats}
<Stats {stats} />
{/await}
In Stats.svelte
your data will be ready to use.
Upvotes: 1
Reputation: 917
You didn't show how stats
is passed down to this component, but we can extrapolate two problems from your original solution:
stats
prop isn't passed in right away, therefore stats
is undefined
during onMount
.onMount
is only executed when a component is mounted to the DOM (see documentation). If stats
changes during the lifetime of the component calcPercentage
will not be rerun.You're looking for a way to run calcPercentage
every time stats
changes. You should use a reactive statement for this:
$: if (stats) calcPercentage('currentYear')
This reactive statement will run every time stats
changes.
The if
ensures that calcPercentage
only runs if stats
has a truthy value. undefined
is falsy, so calcPercentage
will not run as long as stats
is undefined
.
A more elegant (but less resilient) approach would be to only render your Stats
component after the data has been loaded using {#await}
, as described by @Ayoub Fiad's answer.
Please note that the Svelte compiler does not know that calcPercentage
depends on stats
, only values which directly appear within the reactive statement will become dependencies. In this case it knows to rerun when stats
changes because stats
is used directly in the if
-block. The alternative for situations where you don't need such an if
is to make calcPercentage
take stats
as a parameter, as described in @Stephane Vanraes' answer.
Upvotes: 1