Skip to content

[Svelte 5] Functions using onMount fail tests due no component initialization #13136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
karbica opened this issue Sep 5, 2024 · 5 comments
Closed

Comments

@karbica
Copy link

karbica commented Sep 5, 2024

Describe the bug

Functions (not components) that call onMount throws an error. The error states that onMount can only be used in component initialization.

'lifecycle_outside_component
`onMount(...)` can only be used during component initialisation'

The purpose of calling onMount in a function is so that mounting logic can be done on behalf of the component. The attached StackBlitz project showcases an example.

Reproduction

Linked below is a working sample. In summary, there is a useMouseCoords function that attaches mousemove listeners in an onMount function that updates state runes. That function is then used inside App.svelte to read those state runes after calling the onMount function.

https://stackblitz.com/edit/vitejs-vite-2fltxz?file=tests%2Fone.test.ts

Function sample

// use-mouse-coords.svelte.ts

import { onMount } from 'svelte';

export function useMouseCoords() {
  let x = $state(0);
  let y = $state(0);

  function onMousemove(e: MouseEvent) {
    x = e.clientX;
    y = e.clientY;
  }

  console.log('useMouseCoords: outside of onMount');
  function init() {
    console.log('useMouseCoords: inside of onMount');
    window.addEventListener('mousemove', onMousemove);
    return () => window.removeEventListener('mousemove', onMousemove);
  }

  onMount(init);

  return {
    get x() {
      return x;
    },
    get y() {
      return y;
    },
  };
}

Test sample

// one.test.ts

import { it, expect } from 'vitest';
import Counter from '../src/lib/Counter.svelte';
import { useMouseCoords } from '../src/lib/use-mouse-coords.svelte.ts';
import { render, screen } from '@testing-library/svelte';

it('runs correctly when rendering a component', () => {
  render(Counter); // this includes an `onMount` call
  expect(screen.getByRole('button')).toBeInTheDocument(); // PASS
});

it('throws an error about onMount can only be used in component initialization', () => {
  expect(() => useMouseCoords()).toThrowError(
    'lifecycle_outside_component\n`onMount(...)` can only be used during component initialisation'
  ); // PASS
});

Logs

N/A

System Info

System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 18.20.3 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.15.6 - /usr/local/bin/pnpm
  npmPackages:
    svelte: ^5.0.0-next.243 => 5.0.0-next.243

Severity

annoyance

@karbica
Copy link
Author

karbica commented Sep 5, 2024

I get that a component passed to mount (exported by svelte) sets up a bunch of contextual requirements so that onMount works successfully. However the mount interface only accepts components.

React has ways to test their hooks. I'm looking for a way to test these reactive functions that contain lifecycle and runes as the Svelte counterpart.

@paoloricciuti
Copy link
Member

onMount calls $effect under the hood and $effect needs a parent effect or a root effect to avoid memory leaks of it's dependencies. You could wrap the onMount call in an $effect.root to make this work but please consider that usually you also need to clean up the root effect after the usage.

Here's an example with $effect.root and as you can see i'm able to call the function in the module itself without the error.

Now this might be fine since the effect is decently simple, doesn't have any dependency and it will be garbage collected when the page closes. If you want a more involved solution you could keep count of the subscribers inside the getters and only register an effect if someone is actually getting the value from within an effect. You can use $effect.tracking for that....let me know if you want an example of that too.

@dummdidumm
Copy link
Member

This will also be covered in our new docs, you can take a peek here: https://github.com/sveltejs/svelte/blob/main/documentation/docs/05-misc/02-testing.md#using-runes-inside-your-test-files

Closing because this is not a bug within Svelte

@dummdidumm dummdidumm closed this as not planned Won't fix, can't repro, duplicate, stale Sep 5, 2024
@karbica
Copy link
Author

karbica commented Sep 5, 2024

Thanks for the explanation on this @paoloricciuti. That was one of the runes I breezed over but now it's worth taking a deeper look. If you don't mind, I would also like to see a sample of tracking subscribers within getters if the value is actually intended to be read. I'm curious how that would operate.

@dummdidumm Thank you for sharing the write up on this exact scenario. Those are very helpful. I would only ask one thing related to the document you linked. In there, there is a reference to these modules which is being used for the tests:

import { multiplier } from './multiplier.svelte.js'; // used in multiplier.svelte.test.js
import { logger } from './logger.svelte.js'; // used in logger.svelte.test.js

It would be nice to see the implementation of multiplier and logger even as simple as it may seem. The document is already dumping the contents of the test files, why not the functions being tested as well?

Thanks for taking a look at my issue and providing me guidance. I really appreciate it.

@novaotp
Copy link

novaotp commented Dec 30, 2024

onMount calls $effect under the hood and $effect needs a parent effect or a root effect to avoid memory leaks of it's dependencies. You could wrap the onMount call in an $effect.root to make this work but please consider that usually you also need to clean up the root effect after the usage.

Here's an example with $effect.root and as you can see i'm able to call the function in the module itself without the error.

Now this might be fine since the effect is decently simple, doesn't have any dependency and it will be garbage collected when the page closes. If you want a more involved solution you could keep count of the subscribers inside the getters and only register an effect if someone is actually getting the value from within an effect. You can use $effect.tracking for that....let me know if you want an example of that too.

Weird because I can't get onMount to work because it complains about not being in a lifecycle, even though using $effect works fine.

I removed the onMount in favor of BROWSER variable from esm-env so it's fine but I wonder why it doesn't work ?

Edit : it's not fine since I have other functions that need a cleanup...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants