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

code splitting strategy #4112

Closed
aNyMoRe0505 opened this issue Jan 23, 2024 · 4 comments
Closed

code splitting strategy #4112

aNyMoRe0505 opened this issue Jan 23, 2024 · 4 comments

Comments

@aNyMoRe0505
Copy link

aNyMoRe0505 commented Jan 23, 2024

@reduxjs/toolkit: 2.0.1
react-redux: 9.1.0
next: 14.1.0

Hi, recently I've been trying to use the new feature of createSlice in RTK V2 for code splitting, I encountered some uncertainties and errors. I reviewed the documentation but wasn't entirely sure, so I created this issue for clarification.

I created this repository to illustrate my questions, which include three branches: main, approach-2, and approach-3

main branch

In this branch, I injected the reducer on pages that require optionalSlice (index.tsx, other.tsx), and used listenerMiddleware to startListening for the required listeners

rootReducer.inject(optionalSlice);
listenerMiddleware.startListening({ actionCreator: optionalIncrement, effect: onOptionalIncrementEffect });
listenerMiddleware.startListening({ actionCreator: optionalDecrement, effect: onOptionalDecrementEffect });

Questions

  1. Is it okay to use listenerMiddleware.startListening for dynamically injecting listener effects, considering that the documentation indicates we should use addListener?
  2. Is it okay to write the same logic in two page components (index.tsx, other.tsx)? I am not sure if this is a proper approach. I am concerned it might cause some side effects. Although I tested it myself and it seems fine.
  3. I encountered some TypeScript issues when using startListening, but I'm not sure where I went wrong
截圖 2024-01-23 16 01 29

approach-2 branch

In this branch, not much has been changed. In a real-world app, adding logic to every required page might be impractical. Therefore, I wrote a CheckRequiredResource.tsx file, which wraps any component that meets the required conditions with InjectRoute.

Questions

  1. Just want to check if this approach is okay or not

Note

this approach has no effect on code splitting because CheckRequiredResource still references optionSlice.
you can ignore, I need to find another way

approach-3 branch

In this branch, I updated the InjectRoute. I used useEffect to inject the reducer and corresponding listener. Additionally, I added a state isInitializeDone to ensure that the children have the corresponding listener and reducer when rendering.
It is evident that this approach has an impact on SEO.

  useEffect(() => {
    const unsubscribeFn: any[] = [];

    // some condition needed
    rootReducer.inject(optionalSlice);

    unsubscribeFn.push(dispatch(addListener({ actionCreator: optionalIncrement, effect: onOptionalIncrementEffect  })))
    unsubscribeFn.push(dispatch(addListener({ actionCreator: optionalDecrement, effect: onOptionalDecrementEffect  })))

    setIsInitializeDone(true);

    return () => {
      unsubscribeFn.forEach(fn => fn());
    };
  }, []);

Questions

  1. I am uncertain whether it is ok to repeatedly inject the same reducer and listener. If it is allowed, does that mean there is no need to unsubscribe listener and inject the reducer based on conditions (like state is undefined or not)?
  2. I encountered some TypeScript issues when using addListener, but I'm not sure where I went wrong
截圖 2024-01-23 15 32 36

Note

this approach has no effect on code splitting because CheckRequiredResource still references optionSlice.
you can ignore, I need to find another way

I'm not sure if it's appropriate to post it here. Thank you for your help.

@EskiMojo14
Copy link
Collaborator

Hi! Just some general comments:

  1. we're working on getting more example and documentation for these, see Add example of lazy loading slices with combineSlices, and middleware with createDynamicMiddleware #4064 and Add combineSlices section to Code Splitting page redux#4665
  2. you're getting a non-serializable error because the listener middleware needs to be before the serializeable middleware:
export const store = configureStore({
  reducer: rootReducer,
-  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(listenerMiddleware.middleware),
+  middleware: getDefaultMiddleware => getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})
  1. With regards to your Typescript issues, you need to create typed versions of startListening and addListener:
import type { TypedStartListening, TypedAddListener } from "@reduxjs/toolkit"
import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'
import type { RootState, AppDispatch } from './store'

export const listenerMiddleware = createListenerMiddleware()

export const startAppListening = listenerMiddleware.startListening as TypedStartListening<RootState, AppDispatch>

export const addAppListener = addListener as TypedAddListener<RootState, AppDispatch>

then you can call these with effects that require the correct RootState and AppDispatch types.

We don't have any examples of lazy loading slices with NextJS, but I think the same approach applies - call .inject in the slice file, and lazy load the component that imports the slice.

I think your example repo is difficult to show the lazy loading because you're always injecting the slice - meaning you may as well have it as a static reducer.

In comparison, see the example in #4064, where slices are only loaded once needed (once the tab is opened and an action is dispatched)

@aNyMoRe0505
Copy link
Author

aNyMoRe0505 commented Jan 24, 2024

@EskiMojo14
Hello, thank you very much!!
Because Next.js performs code splitting under the pages directory (e.g., index.tsx, other.tsx), it only loads the JavaScript for a specific page when the user navigates to that page (e.g., /index, /other).

That's why I was thinking about whether it's possible to inject the required reducer on individual pages (although I'm not entirely sure if this approach is ok). It's somewhat similar to the example you provide, but in the example, Suspense is used for lazy importing the page component

My goal is to reduce the first load JS shared by all page, as we don't want to load all reducers during the initial loading
The bundle analysis results initially look like this when nothing has been done:
before

The result for the main branch is as follows:
after

The above result can be found by using npm run build

It seems like I've successfully reduced the size of _app, but I'm not entirely sure if this approach is correct or if it might cause other side effects. What do you think? (Perhaps the next step is to extract optionalSlice individually into a chunk for shared usage across pages.)

In the counterSlice.ts file under the example directory, I noticed these lines:

// we can call both inject and injectInto, because the reducer reference is the same - injection only happens once regardless
const withCounterSlice = rootReducer.inject(counterSlice)
const injectedCounterSlice = counterSlice.injectInto(rootReducer)

Does this mean that repeatedly injecting the same reducer won't have any impact, and it's safe to inject without any concerns? As for listenerMiddleware.startListening, is it safe to repeatedly start listening if the action and effect are the same?

Thank you again for your help; I truly appreciate it.


updated

Approach 2 & Approach 3 has no effect on code splitting because CheckRequiredResource still references optionSlice

@aNyMoRe0505
Copy link
Author

Sorry, I think I've figured it out.
I've updated approach 4.
If you have time, I would appreciate it if you could take a look for me
Is this what you mean by "call .inject in the slice file, and lazy load the component that imports the slice"?

Thank you again for your help

@EskiMojo14
Copy link
Collaborator

sorry for the late response - yes, that looks right to me.

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

2 participants