Skip to content
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

Adding thunks and query apis dynamically #3148

Open
dbartholomae opened this issue Feb 2, 2023 · 19 comments
Open

Adding thunks and query apis dynamically #3148

dbartholomae opened this issue Feb 2, 2023 · 19 comments

Comments

@dbartholomae
Copy link

Hi there!

When writing big applications with redux, especially with multiple teams working in the same frontend on different domains, it can become complicated to colocate module code, as redux currently strongly suggests a pattern where middleware and reducers are added to the store centrally when creating the store. This also causes trouble when code-splitting as well as potentially performance degredation due to reducers that are not needed for the currently active part of the app.

There even is a solution for this from the past, redux-dynamic-modules, a Microsoft package that allows to bundle reducers, sagas, thunk,... into a module and load that module at runtime.
Unfortunately, redux-dynamic-modules uses its own createStore function, which makes it non-obvious to use with redux toolkit. At the same time, redux toolkit already partially supports this dynamic behavior: Listeners can be added and removed at runtime.

Since redux-toolkit is supposed to be opinionated, there is an opportunity here: If we extend the pattern from listeners to other redux concepts, we could define a pattern that allows other library maintainers to follow.

What I'm proposing is to add a module middleware that listenes for certain actions, similar to the ones defined by listeners, and uses them to adjust reducers and middleware on runtime, as well as a module format that makes it easy to bundle up modules and add them to a store on runtime.

If there is interest in this, I'm happy to work on this and provide PRs, or start with an implementation external to redux-toolkit. But I do believe that in this case, an implementation outside the core will be less impactful for the community, as it is mostly about setting standards that other libraries can abide by.

@phryneas
Copy link
Member

phryneas commented Feb 2, 2023

I'm not sure if this is a good idea for middleware beyond the listener middleware, as that could change the Dispatch type at random runtime times - but I think most middleware could be plugged into the listener middleware.

For reducers, there is a proposal by me: #2776

@dbartholomae
Copy link
Author

Thanks, that's important context! I'll read through the linked threads and then find the best place to continue this conversation.

@markerikson
Copy link
Collaborator

@dbartholomae : can you give an example of where you need to add middleware dynamically at runtime? What specific use cases are you trying to solve?

@dbartholomae
Copy link
Author

Mainly for adding apis from code-split modules. In a bigger app, there are often multiple independent APIs that do not depend on each other for caching and benefit from multiple teams being able to use them independently (putting them in the same API e.g. requires to ensure different names for endpoints). If apis where implemented on top of listeners, I don't think this would be necessary and they could be added on top of listeners as well.

For this specifically, I would see three alternatives:

  1. Allow injecting middleware. This would be the most powerful, but also most dangerous. It could be linked to documenting, maybe even in types, the expectation that the type of dispatch is not changed.
  2. Ensure that api middleware is built on top of the listener api. This would be my personal favorite, as it would show a clear opinionated way forward for anyone developing on top of RTK. But I don't know how hard this would be to achieve, or whether it would have any problematic performance effects.
  3. Augment enhanceEndpoints/injectEndpoints in a way that allows codesplitting. This I would not do, as it risks overwriting endpoints by accident, and also introduces an additional way for codesplitting.

Overall, I would imagine something along the following lines (strongly inspired by redux-dynamic-modules). Examples are obviously very contrived, but I tried to covers as many parts of the api as possible while keeping the code simple.

// main App
export const store = configureStore({
  reducer: mainReducer
  // default middleware includes a new middleware to load modules
})

export type RootState = ReturnType<typeof mainReducer>;

export interface RootExtraServices {}

type Dispatch = ThunkDispatch<RootState, RootExtraServices, AnyAction>;

export const useDispatch = untypedUseDispatch<Dispatch>;

export function useSelector<Selected>(
  selector: (state: RootState) => Selected,
  equalityFn?: (left: Selected, right: Selected) => boolean,
) {
  return untypedUseSelector(selector, equalityFn);
}
// auth module

interface AuthExtraServices {
  authSDK: AuthSDK
}

const authApi = createApi({
  reducerPath: 'authApi',
  baseQuery: fetchBaseQuery(),
  endpoints: (builder) => {
    login: builder.mutation({
      queryFn: ({username, password}, _queryApi, extra) => {
        const { authSDK } = extra as AuthExtraServices
        return authSDK.login(username, password)
      }
    }),
    logout: builder.mutation({
      queryFn: (_arg, _queryApi, extra) => {
        const { authSDK } = extra as AuthExtraServices
        return authSDK.logout()
      }
    })
  }
})

const authenticatedSlice = createSlice({
  name: 'authenticated',
  initialState: false,
  extraReducers: (builder) => {
    builder.addCase(authApi.endpoints.login.fulfilled, () => true)
      .addCase(authApi.endpoints.logout.pending, () => false)
  }
})

export const authModule = createModule({
  name: 'auth',
  apis: [authApi],
  slices: [authenticatedSlice],
  finalActions: [logout()],
  // The next line would be neat, but I'm not sure if it is achievable
  extra: { authSDK },
})

export const logout = authApi.endpoints.logout.initiate

type AuthState = ReturnValue<typeof authModule.reducer>

type Dispatch = ThunkDispatch<RootState & AuthState, RootExtraServices & AuthExtraServices, AnyAction>;

// Each module can build its own correctly typed helpers so that code in each module can be sure of what types are available
export const useDispatch = untypedUseDispatch<Dispatch>;

export function useSelector<Selected>(
  selector: (state: RootState & AuthState) => Selected,
  equalityFn?: (left: Selected, right: Selected) => boolean,
) {
  return untypedUseSelector(selector, equalityFn);
}
// StopWorkingReminder that depends on the auth module

const workingSlice = createSlice({
  name: 'stopWorking',
  initialState: 'working',
  reducers: {
    notifyPause() {
      return 'notifying'
    }
    forcePause() {
      return 'forcedPause'
    }
  }
})

const startNotification = stopWorkingModule.createAsyncThunk(
  // Users will need to manually ensure uniqueness here
  'stopWorking/startNotification',
  (_, { dispatch }) => {
    await waitFor(20 * 60_000)
    dispatch(workingSlice.actions.notifyPause())
    await waitFor(5 * 60_000)
    dispatch(workingSlice.actions.forcePause())
  }
)

const logoutOnPauseListener = {
  actionCreator: workingSlice.actions.forcePause,
  effect: (_, {dispatch}) => dispatch(logout()),
})

export const stopWorkingModule = createModule({
  name: "stopWorking",
  dependencies: [authModule],
  slices: [workingSlice],
  listeners: [logoutOnPauseListener],
  initialActions: [startNotification()],
})
// A component like this could be used to actually load the modules, so this is not user land anymore, but implementation inside the library

function ModuleGate({modules, children}) {
  const [loading, setLoading] = useState(true)
  const dispatch = useDispatch()
  useEffect(() => {
    for (let module of modules) {
      for (let dependency of module.dependencies) {
        dispatch(dependency.loadModule());
      }
      dispatch(module.loadModule());
    }
    setLoaded(false)
    return // imagine similar clean up logic here
  }, [modules])

  if (loading) return null
  return children
}

I've also thought about an interface that uses the builder pattern to module.addReducer etc., which would allow to automatically ensure correctly scoped action types and reducer paths and potentially to make custom extras per module easier. The drawback would be that state type could no longer be inferred from apis and reducers, that's why I decided against it.

For implementation, the loadModule action that is dispatched would contain reducers, api middlewares, etc., and these could then be centrally added by the module middleware to the store.

Btw. happy to move the discussion into one of the existing issues if you feel like it fits better there.

@markerikson
Copy link
Collaborator

Hmm. I'm not sure what you mean by "if APIs were implemented on top of listeners". Those are two very different things, implementation-wise. The listener middleware can only listen and respond to actions after they've been handled, whereas the custom middleware for RTKQ does a lot more complex work internally, including behaviors that are literally not possible for the listener middleware in any way (such as intercepting a "probe" action synchronously and returning a result).

I'm also not sure what you mean by "Augment enhanceEndpoints/injectEndpoints in a way that allows codesplitting", because those are the tools that RTKQ provides specifically for code splitting.

In an ideal scenario, there would still only be one single createApi call in the app. That API's middleware is added once to the store at startup, statically. From there, the rest of these "separate APIs" would actually just be the one API object, updated by calling .injectEndpoints() multiple times. From the way you're describing things, it sounds like the main blocker for that is the potential collision of endpoint names. How likely is the name collision scenario in your case?

For the record I'm open to discussing the larger "general-purpose store enhancement at runtime" case, but I first want to see how feasible it is to address your team's situation with the existing capabilities.

@markerikson
Copy link
Collaborator

Actually, as a separate question, have you looked at https://github.com/fostyfost/redux-eggs ? It's the most recent "dynamic Redux modules" lib I've seen posted, but I don't know if anyone's actually using it.

@dbartholomae
Copy link
Author

Hmm. I'm not sure what you mean by "if APIs were implemented on top of listeners". Those are two very different things, implementation-wise.

I'm not familiar with the internal implementation of RTK Query. I interpreted phryneas first comment as "it should be possible to solve all relevant behaviour with listeners and without additional middleware", so I assumed that RTK query was part of that. If it isn't, then I would go with an implementation that allows to inject middleware to have more flexibility - though that's not an interface we might want to actually expose, at least initially.

From the way you're describing things, it sounds like the main blocker for that is the potential collision of endpoint names.

It is one of the blockers, but there are others. The biggest one I think is not being able to have custom base queries, as they might have different urls (auth.example.com vs. payment.example.com) or even different ways to authenticate. Another one is tags, which suffer from the same name collision problems as endpoints.

How likely is the name collision scenario in your case?

I ran into it multiple times, latest last month, when I tried to convert an app with multiple APIs into one. Typical examples include domain objects with different definitions per domain, e.g. the analytics service might have a different understanding of what an "account" is from the user profile service or the invoicing service. A concrete tangible example from a project in the past where I used redux dynamic modules (before there was RTK), was an application that allowed lawyers to communicate with their clients. In the beginning, we had both lawyers and clients in the same frontend but with separate APIs. While the long-term solution was to split even the frontend into two, the migration path meant to first split the api code in the frontend. The apis obviously had a lot of similar endpoints with different data.
In general, there are also many use cases which might not be obvious for greenfield projects, because code wouldn't be written that way, but for brownfield projects they are necessary for the migration path.

For the record I'm open to discussing the larger "general-purpose store enhancement at runtime" case, but I first want to see how feasible it is to address your team's situation with the existing capabilities.

Makes a lot of sense - let's first explore the problem space before jumping into the solution space.

Actually, as a separate question, have you looked at https://github.com/fostyfost/redux-eggs ? It's the most recent "dynamic Redux modules" lib I've seen posted, but I don't know if anyone's actually using it.

I haven't used it, but from the documentation, it suffers a similar problem as enhanceApi: It requires an explicit dependency on the think it injects into. The beauty of redux-dynamic-modules and the listener implementation is that modules don't need to know about the thing they are enhancing (except for needing to find a unique name), and the only thing that is needed from the existing store is the dispatch function, which already is injected everywhere anyways.

@markerikson
Copy link
Collaborator

Okay, those do sound like legit use cases for adding multiple RTKQ API definitions, each with their own middleware.

Can you clarify what you mean by "redux-eggs requires an explicit dependency on the thing it injects into"?

FWIW, my main thoughts here atm are:

  • Overall we do try to focus RTK on the "80-90%" use cases of things that most people are doing with Redux, and it feels like your scenario is falling outside that
  • Dynamic runtime loading and code splitting are things that have crossed my mind some over the last couple years, so I'm not completely against adding something related to those...
  • But given that Lenz and I already have our hands full and then some with all the existing issues and concepts around RTK/RTKQ, I don't think either of us have the bandwidth to design new APIs for RTK around this set of use cases. (We also don't have the experience building apps that need this sort of functionality either to help guide us in the design.)
  • Given that a library like redux-eggs exists, it might be better to look at contributing to that and improving it, rather than putting this in RTK directly (in the same way that next-redux-wrapper and redux-persist already exist and solve use cases not handled by RTK)

@dbartholomae
Copy link
Author

Overall we do try to focus RTK on the "80-90%" use cases of things that most people are doing with Redux, and it feels like your scenario is falling outside that

I don't have an industry overview, but I would be surprised if most of devs are working on small-size projects. There are most likely less projects that need this kind of code splitting, but they most likely have more devs working on them, so depending on how you count, I would say that it could actually affect 80-90% of devs working with Redux. But I don't have numbers on this, and the Twitter poll you did indicated only ~35% of devs using code splitting.
I also understand that there are limitations to what you can support, and of course that takes precedence.

The reasons why I am pushing for considering this, are as follows:

  1. This is a very common use case for bigger frontend projects. (This is just my personal observation, though, so data points from others might be important to verify, as the other projects don't have that many downloads so either others experience less of this problem, or all create their custom workarounds).
  2. This is a standard that could affect the full ecosystem, as other projects might want to also support dynamic loading. Therefore it might make sense to take control of this inside the core project.
  3. There already is a method for this inside redux-thunk which is already considered core, so extending the same method to other parts of the project would ensure that we don't end up with multiple different patterns.
  4. Their is no clear alternative yet. The biggest existing project (redux-dynamic-module with currently ~17,000 weekly downloads, last update 3 years ago) doesn't have an obvious way to integrate with RTK Query. The newest one, redux-eggs, is still quite small (~3,000 downloads per week) and doesn't yet address e.g. injecting apis.
  5. It might be necessary to slightly tweak RTK Query and other parts of the core library to avoid hacky workarounds (not sure about this yet, could also work without).

Can you clarify what you mean by "redux-eggs requires an explicit dependency on the thing it injects into"?

Instead of using a native method like dispatching actions to set up modules, you need access to the actual store. So you need to explicitly have the store available, which you might not have in all the places where you have dispatch available. I don't think it is a really big thing, but it is different from the existing method used for listeners, and it makes some things a bit more cumbersome than what they would need to be.
It is also important to note that redux eggs does not actually support RTK Query yet (though I do think it would be possible to also write an extension for eggs to support it).

But given that Lenz and I already have our hands full and then some with all the existing issues and concepts around RTK/RTKQ, I don't think either of us have the bandwidth to design new APIs for RTK around this set of use cases.

I would offer to take the main work of building this and contributing it to the project with nice test coverage and documentation. I tend towards more and shorter files than what I've seen in this project. You can find a small example project of mine here. I am currently using RTK at work extensively enough to justify spending even a bit of work time on it.
I also know that this might change, and that you also don't know me well enough to be able to trust this support, especially since "creating the initial version" and "supporting it long-term" are different things.

Conclusion

Overall, I see three main questions:

  1. Is dynamic modules a topic that is as needed by others as I perceive it to be, or is it less important? If it is less important, it should not be part of RTK.
  2. Do you agree that this is a problem that should be solved centrally as an opinionated standard? If not, it should not be part of RTK.
  3. Is there enough resources available to maintain an additional module like this inside redux toolkit? I offer my support, but in the end you are the ones that would need to feel comfortable to maintain it. If there are not enough resources, dynamic modules should not be part of RTK.

I'll anyways put it on my list to experiment a bit with the API and maybe create an independent package, but I still believe that it would be much more valuable if it was part of the core.

@markerikson
Copy link
Collaborator

markerikson commented Feb 5, 2023

@dbartholomae : yeah, the notion of dynamic loading and code splitting has certainly been on my mind for 2.0 as a thing to look at.

I think a good first step would be doing some sketching out of the problem space and use cases:

  • What are kinds of things people want to do in terms of lazy-loading Redux code?
    • types of code: slice reducers, middleware, listeners, sagas, RTKQ APIs, etc
  • how does the loading get triggered?
    • What do the userland APIs for initiating loading look like?
    • How does that interact with code splitting overall?
    • How much of this is UI-agnostic, and how much might be React-specific? Similar to the RTKQ hooks, what could we add as a React-specific layer on top of the UI-agnostic core?
  • How does this affect the TS typing? How do we define static types for dynamically-loaded code?
  • What prior art exists?
  • What specific problems are we ultimately trying to solve? What problems do we not want to solve?
  • How does this work in conjunction with existing Redux logic?
    • How would you set up normal slice reducers and dynamic reducers?
    • What happens if the user manually called combineReducers and passed their own rootReducer to configureStore, instead of passing the object full of slice reducers?
  • Given the sets of functionality we settle on:
    • how feasible is it to implement some of these concepts?
    • eventually, how does this end up affecting bundle size?
    • How do we keep the API design "Redux-y", without turning RTK into something that looks and feels completely different?

Not saying we need to answer every single question before any code can be written, but sketching out scope and having an idea of what we want to build would be key.

In the run-up to publishing RTK 1.0, I wrote up a long "vision" statement laying out what I wanted RTK to do, and the constraints and things I didn't want to do:

Ironically, if you look at the things I said "I don't want to handle these"... we've ended up doing some of them ("data fetching" -> RTKQ, "encapsulated modules / plugins / code splitting" -> we're talking about this now, "React-Redux-specific functionality" -> RTKQ hooks). But saying no to those early on helped keep RTK's scope focused on what was critical for getting 1.0 out the door, and then we were able to build more features from there over time.

So. If you are willing to take point on driving most of the initial work for this, I'm open to providing oversight and guidance.

I know I had added a bunch of relevant libs to my "Redux Ecosystem Links" repo before I stopped updating it in 2018, and it looks like there's more libs since then that I haven't even seen before:

There's the three-ish major "Redux dynamic module loading" libs I can think of:

and then some of the major "Redux abstraction layer" libraries might have some stuff like this:

and a couple articles:

and then you've got the non-Redux libs:

Heh. Okay, yeah, so just reviewing some of those, it does seem like this is a reasonably common thing people want to do with Redux :)

Think you could start by filling out some answers to most of those questions, including trying to collate writeups on the ecosystem "prior art" reviews?

Tweeted a link at https://twitter.com/acemarke/status/1622040111440338945 asking for more feedback.

@markerikson
Copy link
Collaborator

I'm also gonna tag a couple people who may have related thoughts on this: @mq2thez and @Ephem

@Ephem
Copy link

Ephem commented Feb 5, 2023

While I've thought about this for 5+ years, it's been a while since it's last been top of mind so I don't think I have that much to add, but I'll note one thing.

The big problem I've always seen is where do you inject things? With client only this is simple (module scope), but with SSR it's trickier.

I've done a writeup about this problem in this discussion (I added a new comment today) that also links to some older RFCs I wrote (first one 2018 if you want to read some ancient history). I have never considered the more advanced cases much though, like module/middleware injection and the like, since I'm not sure it belongs in core, but RTK Toolkit is different and it might very well belong there. 😃

I've always argued that it would be very nice to have better support for at least the simpler cases like reducer code splitting in core, I've seen first hand the performance impact of not code splitting reducers in even medium sized projects and I think the reason people don't do it (according to that Twitter poll for example) is that it's tricky and not built in. So I'm very happy more people are thinking about this! 😄

@dbartholomae
Copy link
Author

Think you could start by filling out some answers to most of those questions, including trying to collate writeups on the ecosystem "prior art" reviews?

Sure, happy to do so! There are a few questions I would like to answer first, though:

Should this be in RTK?

What are roughly the questions that you need answered before being open to see this as part of RTK? The long list above already contains a lot of questions on the how, and there is certainly an influence (if we can't do it to a certain standard, we shouldn't include a half-baked solution), but let's not get preoccupied with whether we could and stop to think if we should just yet.

For me, this comes down to the three questions from my last post, but I would like to make sure that you see it the same that answering these questions is enough to start trying to solve it as part of RTK.

1. Is dynamic modules a topic that is as needed by others as I perceive it to be, or is it less important?

If there is uncertainty here, we should do more user research/outreach first. I'm happy to spread the question around a bit, but the amount of existing libraries maintained by others, we might already be convinced enough and able to skip to the next question.

2. Do you agree that this is a problem that should be solved centrally as an opinionated standard?

3. Is there enough resources available to maintain an additional module like this inside redux toolkit?

For both questions I made my argument and feel like it convinced you, but wanted to explicitly ask.

How to run the RFC process?

I don't see an existing process, but might be overlooking something. I would advise to use some kind of central document that we can comment, be it Google Doc, a PR with a markdown file against this repo, or something else. But having some kind of version control and line-by-line commentability in my experience makes the process easier. Otherwise, everyone who has input will need to read through all of the comments in this thread first, which will grow over time.

If we choose a central document, I would then also share the document for feedback to all of the maintainers for existing solutions to dynamic redux modules, which might help us to avoid some of the problems they might already have stumbled onto.

@phryneas
Copy link
Member

phryneas commented Feb 5, 2023

One thing to add:

It would be possible to rewrite the RTK Query middleware to a point where you just import { middleware } from '@reduxjs/toolkit/query', add that once to your store and that one middleware would handle all RTK Query api slices (internally keeping track of it "per api").

Maybe that would be worth exploring, as it could already remove a lot of friction - and adding integration to inject RTKQ slices via #2776 should be perfectly possible.

That would mean we had

  • injecting slices
  • no need to inject RTKQ middleware
  • a way of injecting middleware

From there, we can take a look if there is still a strong need to inject other middleware. Imho, that would split up into two categories of middlewares:

  • "reacting" middlewares: those middleware only trigger side effects from dispatched actions, but do not block actions going towards the store. They could already be latched onto the store via the listener middleware without any changes
  • "blocking" middleware and "returning" middleware. These middleware either block actions from reaching the store (maybe modifying actions on the way) or change the return value of dispatch by returning something different from next(action).
    "Blocking" middleware would have been possible with the first design I had up for the listener middleware back in the day, but we decided to keep that out. We could revisit that question. In the same way, we could add the ability to early-return to the listener middleware.
    Tbh., I don't like either of these two ideas, as they are not typesafe - they don't reflect in AppDispatch. But with this, as with any other way of writing an "injectable" middleware solution, we would need to build a new type safety story, so it might also be okay to do that there.

@markerikson
Copy link
Collaborator

@dbartholomae : at this point I'm operating under the assumption this is a thing worth at least exploring, and probably adding something around dynamic loading to RTK. The list of questions above is meant to be a guide for us to figure out what we want to build.

For content, maybe a shared Notion doc? Might have more flexibility in terms of what content gets added and written that way. And we can add a repo discussion thread that links that.

@dbartholomae
Copy link
Author

I think I would not go with Notion since it requires creating an account to be able to comment, and this might limit feedback from users. I saw that Ephem created a PR with a markdown document back then, and I would propose to use the same here.

@mq2thez
Copy link

mq2thez commented Feb 7, 2023

There's a whole lot of text in this thread; apologies for not reading everything. I know @markerikson tagged me because I built this for Etsy and have previously mentioned that it is very-custom and I'd love to migrate to an RTK-managed version.

@phryneas -- the system you're talking about where there's already a middleware pre-injected is essentially how I built this for us as I started working on how to managed RTKQ with each "subapp" injecting its own RTKQ api needs. There's a single middleware that would be added every time anyone creates a store, and each dynamically-loaded bundle would handle injecting its API-needs at the same time it injects its API endpoints. Our RTKQ usage is still only theoretical (I have a proof of concept but haven't been able to make forward progress on it for a few months due to other obligations.

However, we've also adopted the same pattern for listener middleware -- during store setup, we inject a singleton middleware in every store, and each "subapp" interacts with that to load the listeners it needs.

For our subapps, we have a Loader.ts file that handles the setup/bootstrapping:

import reducerRegistry from "path/to/this/helper";
import listener from "path/to/listener/singleton";
import reducer from "./reducer";
import { setupListeners } from "./listeners";
import App from "./App"

reducerRegistry.register({
  sliceName: reducer,
});

setupListeners(listener);

// Here's where we'd like also initialize RTKQ API endpoints as well

export default App;

The idea is that anything that wants to render a particular subapp can dynamically import the correct Loader file and all reducers/listeners for that subapp will get injected properly before first render of the component. This comes with awkwardness -- we have to derive the StateType and DispatchType somewhere, which currently lives in a Selector.ts file (along with subapp-typed hooks) for each subapp.

We have a small number of subapps that need multiple top-level reducers (both for legacy reasons and for code-sharing reasons); for that, the Loader file for the subapp might look like:

import reducerRegistry from "path/to/this/helper";
import listener from "path/to/listener/singleton";
import reducer1 from "./reducer";
import reducer2 from "../OtherSubapp/reducer";
import { setupListeners } from "./listeners";
import { setupListeners as setupOtherListeners } from "../OtherSubapp/listeners";
import App from "./App"

reducerRegistry.register({
  sliceName: reducer1,
  sliceName2: reducer2,
});

setupListeners(listener);
setupOtherListeners(listener);

export default App;

This ensures that any subapp which requires a shared piece of state can register it and its needs easily in a way that doesn't require an extra set of async steps (IE, we don't want to wind up with component trees that are 6 dynamic imports deep somewhere down the line).

In terms of our needs / things which have been hard for us:

  • We don't have SSR in the areas which utilize this stuff, so I'm not clear on what issues might arise from needing to account for that
  • injecting a reducer right now is an action which triggers a Redux state traversal, so injecting multiple reducers at once can be quite expensive (which is why reducerRegistry takes an object instead of a single name/reducer)
  • The code which manages the reducerRegistry requires a fair amount of custom behavior threaded through in different places, and there are only a few people at Etsy who could even begin to maintain it if something went wrong. I'd love to migrate from difficult-to-maintain custom code to library-provided functionality
  • Deriving types for State / Dispatch is a more-manual process than I would like
  • It's not currently possible to inject state anywhere except the top-level of the tree, because I couldn't figure out how to generalize that given the current tools available

Things our subapps currently need to share/derive/inject:

  • Injecting reducers
  • Injecting listeners
  • Injecting RTKQ APIs (at some point)
  • TS types for state/dispatch (with typed hooks) that include whatever was used for the reducerRegistry and also accounts for parts of state which are always present (IE, things part of the main bundle and not dynamically loaded)

@markerikson
Copy link
Collaborator

This is a mile-long thread, and I don't feel like re-reading the whole thing right now:

But we did just ship a createDynamicMiddleware API in 2.0 that might help here:

@mq2thez
Copy link

mq2thez commented Dec 6, 2023

Already scoping out the work to upgrade and then migrate over to it! Thank you so much.

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

5 participants