Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[2.0] The MIGRATION.md replacement for `from()` keeps the upstream subscription after dispose; add a subscription helper

Open
#4,182 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@devagrawal09 is already working on this.

Since Oct 21, 2026.

  • #4183 by @devagrawal09 — open

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
typescript

Research direction

Start with documentation/solid-2.0/MIGRATION.md and the consumeIterator behavior described in the issue; the minimal reproduction shows how disposal leaves the upstream subscription open. Compare the migration guidance with the From React page additions referenced in the payload. Done means the docs explain safe teardown and an official reusable subscription approach is available or the issue’s API proposal has a clear resolution.

Written by the indexing model from the issue text.

Description

Describe the bug

documentation/solid-2.0/MIGRATION.md ("from / observable → async iterators / effects") recommends:

const value = createMemo(async function* () {
  for await (const val of observable$) yield val;
});

After the owner is disposed, the upstream iterator's return() is not called. It runs only when the upstream produces another value, which may never happen. The cause is that the generator is suspended at an await (inside for await), so its return() is queued behind the pending step. Solid's side is correct: consumeIterator calls return() on dispose and on supersede.

Your Example Website or App

Minimal repro inline below (signals-only).

Steps to Reproduce the Bug or Issue
import { createMemo, createRoot, createRenderEffect, flush } from "@solidjs/signals";

let push: (v: number) => void;
let closed = 0;
const observable$: AsyncIterable<number> = {
  [Symbol.asyncIterator]: () => ({
    next: () => new Promise(r => (push = v => r({ value: v, done: false }))),
    return: async () => { closed++; return { value: undefined, done: true }; }
  })
};

const dispose = createRoot(d => {
  const value = createMemo(async function* () { for await (const v of observable$) yield v; }, { loadingValue: 0 });
  createRenderEffect(value, v => console.log("value", v));
  return d;
});
flush(); push!(1); /* await a few microtasks */ flush(); // logs 0, 1

dispose(); /* await 50 microtasks */
console.log(closed); // 0: upstream still subscribed
push!(2);  /* await */
console.log(closed); // 1: closes only after the next upstream value

By contrast, a hand-written iterator whose return() unsubscribes synchronously behaves well. I tested:

  • isPending stays false while the memo waits for the next value;
  • loadingValue covers the first read;
  • return() unsubscribes immediately on dispose;
  • bursts coalesce into one update with a dirty flag.

It's about 20 lines and easy to get wrong:

function fromSubscription<T>(subscribe: (cb: () => void) => () => void, getSnapshot: () => T): AsyncIterable<T> {
  return {
    [Symbol.asyncIterator]() {
      let dirty = true, done = false;
      let wake: ((r: IteratorResult<T>) => void) | undefined;
      const unsubscribe = subscribe(() => {
        dirty = true;
        if (wake) { const w = wake; wake = undefined; dirty = false; w({ value: getSnapshot(), done: false }); }
      });
      return {
        next: () => {
          if (done) return Promise.resolve({ value: undefined, done: true });
          if (dirty) { dirty = false; return Promise.resolve({ value: getSnapshot(), done: false }); }
          return new Promise(r => (wake = r));
        },
        return: () => {
          done = true; unsubscribe();
          wake?.({ value: undefined, done: true }); wake = undefined;
          return Promise.resolve({ value: undefined, done: true });
        }
      };
    }
  };
}
Expected behavior
  • Docs: fix the MIGRATION.md snippet so disposal actually unsubscribes, and document (or flag in dev) that async function* + await delays teardown until the next upstream value.
  • API: provide a helper equivalent to React's useSyncExternalStore / Solid 1's from:
    const online = createMemo(() => fromSubscription(subscribe, () => navigator.onLine), { loadingValue: true });
    
    MIGRATION.md says this is expected to "move into @solid-primitives". Given how central external stores are to incremental migrations (keeping existing domain stores and putting Solid on top), it may be worth a minimal version in core. Either way, a correct, reusable implementation should exist somewhere official.
Screenshots or Videos

No response

Platform
  • OS: macOS (Darwin 25.5)
  • Browser: n/a (reproduced in Node v24.18.0 with vitest, importing source)
  • Version: solid next @ b0ad1df50 (2026-10-20, after 2.0.0-rc.16)
Additional context

This came up while documenting useSyncExternalStore for the From React page (solidjs/solid-docs#1664). The From React additions describe the hand-written iterator and warn against async function* for this.

Dominant language
TypeScript
Stars
36.1k
Forks
1.1k
Avg merge
11h 42m
Merged PRs (30d)
350

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from solidjs/solid

All issues in solidjs/solid

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.