I have several service classes (with some "get data for these params" and some "calculate stuff for these params" methods) I'd like to inject into several components in my Svelte component hierarchy. At the moment, I'm seeing the following options for that, none of them very attractive:
In Vue, I would write a plugin that adds to the properties available in all Vue components. What is the Svelte way to do this?
I ran into this same issue. When there's no setup needed it's easy enough to do what Rich Harris suggested and export the services from a separate JS file, but I had a wrinkle: lazy loading. Different from your issue, but similar in that the services aren't immediately available.
I wanted to lazy load Firebase with a dynamic import since it's fairly heavy. That meant the services didn't have access to the DB until the import()
promise resolved, so exporting them wasn't as straightforward. I found a couple solutions:
You mentioned a store seems like overkill since services are static. You can use context for this! One catch: setContext
needs to be called synchronously during initialization (i.e. not in onMount
). If your config happens synchronously you should be able to avoid the extra layer, but here's what I did:
Create an extra component at the top of the app to instantiate the services and pass them as a prop into the root App
, where you pass them to setContext
. You need the extra component above App
to ensure the services exist before setting them in context. Using Svelte's {#await promise}
syntax, you can make sure the initialization happens before you pass the prop:
InitServices.svelte
<script>
import config from './config';
import createServices from './services';
import App from './App.svelte';
const services = createServices(config);
</script>
{#await promise then services}
<App {services} />
{/await}
App.svelte
<script>
import Child from './Child.svelte';
export let services;
setContext('services', services);
</script>
<Child /> <-- contains Grandchild.svelte
Grandchild.svelte
<script>
const services = getContext('services');
const promise = services.getData();
</script>
{#await promise then data}
<div>Hello {data.username}!</div>
{/await}
Wrinkle #2: I had written some tests with @testing-library/svelte
, and they were now complaining about the services coming from getContext
since there wasn't a parent calling setContext
.
One solution is to create a TestContextWrapper.svelte
, which works but adds some extra complexity (not a lot, but it's not nothing). Here's an example of that: https://svelte-recipes.netlify.app/testing/#testing-the-context-api
So... I decided to replace context with a store and it ended up working great. The reactive store is a benefit for testing since you can instantiate the store with mocked services and update it dynamically when you need to.
I still passed the services in as a prop at the top of my app to avoid null checks, but otherwise this cleaned things up a lot. services-store.js
import { writable } from 'svelte/store';
export const serviceStore = writable(null);
InitServices.svelte (Same as above)
App.svelte
<script>
import serviceStore from './services-store';
import Child from './Child.svelte';
export let services;
serviceStore.set(services);
</script>
<Child /> <-- contains Grandchild.svelte
Grandchild.svelte
<script>
import serviceStore from './services-store';
const promise = $serviceStore.getData();
</script>
{#await promise then data}
<div>Hello {data.username}!</div>
{:catch err}
<p>Error! {err.message}</p>
{/await}
Grandchild.test.js
import serviceStore from './services-store';
let mockService;
beforeEach(async () => {
mockService = { getData: jest.fn().mockResolvedValue('helloitsjoe') };
serviceStore.set(mockService);
});
...
it('shows username', async () => {
render(Grandchild);
const name = await findByText('helloitsjoe');
expect(name).toBeTruthy();
});
it('shows error', async () => {
// Override mock service
mockService.getData.mockRejectedValue(new Error('oh no!'));
render(Grandchild);
const message = await findByText('oh no!');
expect(message).toBeTruthy();
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With