Skip to content

Allow creating hooks bound to a singleton store, without context #2353

Closed as not planned
Closed as not planned

Description

@EskiMojo14

If the API design is still open to discussion I'd like to propose removing the need to wrap the render tree in a . Does wrapping it give any benefit? From my perspective at companies running multiple redux stores this Provider mechanic is cumbersome. I realise multi-stores is not the norm but I think if selectors could work independent of the Provider the API would incur lower cognitive load. For instance:

// create selectors bound to a specific store
const store = configureStore()
const { useSignalSelector, useSelector } = createBoundHooks(store)

// use the selectors even if the React tree is not wrapped in a Provider
const value = useSelector((state) => state.value)

In my mind the Provider is only useful to limit propagation of state updates to sub-trees of the whole React tree, otherwise it is just cumbersome. I'd prefer to opt-in to using a Provider if I needed better performance of scoped updates. Additionally, signal selectors are probably not affected by the use of a higher-level Provider, or need not be.

I haven't confirmed any of these and I'd love to get feedback on the general idea.

EDIT: I made a hacky proof of concept work and got all tests to pass here - mikeduminy@3141ffa

Ah I see. Sure you would need to adjust your testing strategy slightly from the current recommendation if you wanted to test the bound hook specifically (beforeEach store.reset, jest.doMock injection of the new hook, setting the hook on the context, etc).

In my proposal however I'm not saying that the Provider mechanics need to disappear, just that they are not really needed for this new hook (and arguably for normal hooks too). Equally, the API does not needs to look exactly as described -- it represents a preferred possibility but I'd be just as happy to have a different one. For instance:

// create selectors bound to a specific store
const store = configureStore()
const useBoundSelector = createSelectorHook(store.toContext())

// use bound hook with or without a provider
const value = useBoundSelector((state) => state.value)

I totally buy the intent to keep this simple and as a drop-in replacement. I guess I'm advocating for exposing a createSignalSelectorHook just like there is a createSelectorHook today that can do almost exactly what I want.

Originally posted by @mikeduminy in #2318 (comment)

Activity

  1. EskiMojo14 commented on Jul 29, 2026

    @EskiMojo14
    CollaboratorAuthor

    @mikeduminy i've split this into a separate issue, as it's largely unrelated to the idea of using signals to track subscriptions.

    I've also attempted to demonstrate how treating the store as a singleton and binding hooks to it greatly complicates your test setup, in this repo

    Please feel free to check it out and have a play :)

  2. mikeduminy commented on Jul 29, 2026

    @mikeduminy

    @EskiMojo14 I appreciate you splitting this off and the work that went into creating a repo for it 🙏

    A summary of the history behind the above suggestion
    Global state means everyone can access everything, even the data of other slices. There are many reasons to restrict this such as governance of data, data shape, other parts of an app inappropriately "reaching in" to the state of a different slice, etc. In large scale apps this can become a real headache.

    A solution I've seen work well is to create separate stores per slice, as they will be truly isolated. The downside is the selector hooks aren't bound to a specific store by default, and lazy-loaded stores would mean lazily rewrapping the app root with more and more Providers. No problem, we can abuse some internals of react-redux (see discussion) to create bound selectors that do not need to be under a Provider.

    The downside of this? It's pretty custom, goes against the normal way of using redux, and it does not benefit from new capabilities of redux (like the potential signals idea).

    Looking ahead
    @EskiMojo14 A thought occurred to me, what if we could take the selectors that are generated from createSlice, which are already scoped to the slice, and generate hooks for each selector? If we had this we probably wouldn't have created multiple stores and thus not run into the Provider shenanigans.

  3. EskiMojo14 commented on Jul 29, 2026

    @EskiMojo14
    CollaboratorAuthor

    it's definitely possible (i'll have a play in the repo i linked), but generally we don't recommend it - hooks are far less flexible and composable than selectors are.

  4. mikeduminy commented on Jul 30, 2026

    @mikeduminy

    Composability is something you lose with the basic approach I've been describing, though I would consider it a worthwhile exchange.

    I've forked your repo and made a singleton work without mocking (https://github.com/mikeduminy/test-singleton-store/blob/main/src/bound/Counter.test.tsx#L7). This is accomplished by added a wrapper around combineSlices that makes the whole reducer resettable (https://github.com/mikeduminy/test-singleton-store/blob/main/src/shared/store.ts#L29) via a resetState function, called before each test (https://github.com/mikeduminy/test-singleton-store/blob/main/test/setup.tsx#L10).

  5. EskiMojo14 commented on Jul 30, 2026

    @EskiMojo14
    CollaboratorAuthor

    state isn't the only thing that can be contaminated between tests, it's just the simplest to demo. your middleware could kick off something async in the background, for example.

    the point is that it's still far simpler to provide a new store for each test, rather than having to know how to clean up your store each time

  6. mikeduminy commented on Jul 30, 2026

    @mikeduminy

    The state-clearing demo was the simplest to show, it's not the only claim I'm making. You're right that resetState() alone doesn't cancel in-flight async work (something that could resolve mid-way through the next test). But I don't think this is insurmountable.

    Humour me for a moment and let's see what it could take to try to make singleton stores work. They already work from a runtime perspective in shipped apps, it's testing that would require a different mindset.

    I built a store that's a stable facade over a swappable inner store: resetState() throws away the whole inner store (state, every middleware instance) and builds a fresh one, but every consumer keeps holding the same outer object (https://github.com/mikeduminy/test-singleton-store/blob/main/src/shared/store.ts#L54, it should probably be called resetStore()). dispatch/getState/etc are getters, not wrapper functions, so whoever reads dispatch keeps that exact function reference forever. A reset doesn't retarget in-flight work; it just becomes a call into an orphaned store nobody reads from anymore, which is essentially what happens in the current recommended approach (creating a new store for each test). I have tests showing this holds under real async races (a setTimeout+await dispatch, and a thunk dispatching via its own dispatch argument) - https://github.com/mikeduminy/test-singleton-store/blob/main/src/shared/store.test.ts#L30.

    So state contamination is solvable, with almost no burden on middleware/thunk authors.

    The one place this doesn't reach is orphaned work that never touches dispatch at all (writing to localStorage, a stray fetch() fired for side-effect logging. I initially thought that might be the cost of the singleton approach, but it isn't really one: a fresh store per test has the exact same issue, since neither approach cancels a timer or an in-flight request. So I don't think that's an argument for one approach over the other.

  7. mikeduminy commented on Aug 10, 2026

    @mikeduminy

    The question is if there is any benefit in supporting this paradigm in react-redux and, if so, should it be the recommended approach?
    I think so since it actually simplifies testing and it doesn't clobber the existing approach (both can be used). Users can reuse a store between tests or create a fresh one each time. The advantage is that selectors are scoped to slices, just like actions are.

  8. EskiMojo14 commented on Aug 10, 2026

    @EskiMojo14
    CollaboratorAuthor

    again, this is a massive overcomplication to avoid having a Provider at the top of your tree, which isn't an issue for 99.99% of people.

  9. mikeduminy commented on Aug 10, 2026

    @mikeduminy

    Just want to make sure we're not talking past each other. The Provider-avoidance thing is old ground, that was a hacky fix for a narrower problem at my last job (#2241), and I'm not proposing it here. Nobody should copy that part.

    What I'm actually talking about has nothing to do with Provider placement or how many stores you have. useSelector gives every component access to the entire global state, no matter who owns what. On a codebase with lots of teams sharing one store, that means team A can quietly read team B's slice with zero enforced boundary. Same failure mode as a microservice reaching straight into another service's database instead of calling its API. This happens with one store and one Provider too, it's not some edge case tied to my old multi-store setup.

    The actual proposal: have createSlice().selectors also generate hooks, similar to what RTK Query does for endpoints. Then an app could just ban raw useSelector and force people through slice-owned hooks instead, which enforces the boundary without touching store or Provider setup at all.

    I'm going to build this internally either way since I need it. I brought it up here because it feels useful enough to live in RTK itself and likely benefits any reasonably sized app, but if you still think it's out of scope, that's fine, I'll drop it.

    Also, no need to write a whole reply if you're not into it. A 👎 works fine, I'll get it.

  10. EskiMojo14 commented on Aug 10, 2026

    @EskiMojo14
    CollaboratorAuthor

    gotcha, i misunderstood - for the reasons stated previously though, I think we're generally against having a hook per selector.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Won't FixThis will not be worked on

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions