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

chore(headless): dynamically load reducers #709

Merged
merged 10 commits into from
Apr 15, 2021
Merged

chore(headless): dynamically load reducers #709

merged 10 commits into from
Apr 15, 2021

Conversation

samisayegh
Copy link
Contributor

The goal of this PR is to simplify Headless configuration. Rather than asking a developer to specify reducers, they would be added dynamically when instantiating a controller. Some benefits of the approach are:

  • Less configuration for Typescript users. The reducers option when instantiating an engine would eventually become optional.
  • Reduces the chance of errors for javascript users. The state of the engine argument on controllers can be empty.
  • Smaller bundles. Only the instantiated controllers and their corresponding reducers will be included in the bundle. At the moment, all the reducers in searchAppReducers are included, whether or not the feature set is being used.

Inspiration: https://redux.js.org/recipes/code-splitting#using-a-reducer-manager

@github-actions
Copy link

github-actions bot commented Apr 13, 2021

Pull Request Report

PR Title

✅ Title follows the conventional commit spec.

Bundle Size

File Compression Old (kb) New (kb) Change (%)
dist/browser/headless.js bundled 456.3 457.7 0.3
minified 192.3 192.9 0.3
gzipped 55.4 55.5 0.3
dist/browser/headless.esm.js bundled 442.8 444.1 0.3
minified 243.6 244.4 0.4
gzipped 60.8 61.1 0.4
../atomic/src/external-builds/headless.esm.js bundled 442.8 444.1 0.3
minified 243.6 244.4 0.4
gzipped 60.8 61.1 0.4

Copy link
Member

@olamothe olamothe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clean !

@@ -281,6 +291,11 @@ export class HeadlessEngine<Reducers extends ReducersMapObject>
}
}

public addReducers(reducers: ReducersMapObject) {
this.reducerManager.add(reducers);
this.reduxStore.replaceReducer(this.reducerManager.reducer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick, but shouldn't the reducer manager be in charge of calling replaceReducer internally on the redux store, when add is executed ?

Seems natural to me that a manager would also manage that part.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see ReducerManager's role as taking care of holding, adding and building the final reducer function.

I feel giving it the role of replacing the reducer on the store would be a tad too much. The reduxStore instance is part of this class. I could pass it into ReducerManager, but I feel it would be clearer to not do that,

@@ -330,7 +338,7 @@ export function buildFacet(
const request = getRequest();
const response = getResponse();

const isLoading = engine.state.search.isLoading;
const isLoading = getIsLoading();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change needed or just an improvement you saw as you were working on this ?

If it's needed, I'd like to understand why ;)

Copy link
Contributor Author

@samisayegh samisayegh Apr 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I made this change to resolve a typescript error. Inside getters only, typescript doesn't correctly infer the state of the engine. It sees it as unknown.

Everywhere else in the controller, ts understands the correct type via the typeguard.

buildSomeController(
engine: Engine<unknown>
) {
    // engine state is "unknown"
    if (!loadReducers(engine)) {
        throw Error
    }

    // if we are here, typescript understands that the engine state is that specified by the loadReducers type guard

    return {
        doSomeAction() {
          // inferred engine state is still correct
       },
        get state() {
           // here, typescript goes back to thinking the engine state is "unknown" for some reason.
       }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative would be to do this, and update references to use resolvedEngine inside the controller:

export function buildFacet(engine: Engine<unknown>, props: FacetProps): Facet {
  if (!loadFacetReducers(engine)) {
    throw loadReducerError;
  }

  const resolvedEngine = engine;

Down-side is one would be able to use engine or resolvedEngine, except inside getters where only resolvedEngine would work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a report for the TS team: microsoft/TypeScript#43676

Copy link
Contributor

@nathanlb nathanlb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good but I'm still a bit confused at what this translates to on the ui library side. When instantiating the engine no reducers are required now I guess. I'm probably just not searching right but I don't see where they're loaded dynamically based on registered components.

@@ -356,13 +371,17 @@ export class HeadlessEngine<Reducers extends ReducersMapObject>
});
}

private initReducerManger() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private initReducerManger() {
private initReducerManager() {


constructor(private options: HeadlessOptions<Reducers>) {
this.initLogger();
this.validateConfiguration(options);
this.initReducerManger();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.initReducerManger();
this.initReducerManager();

/**
* Adds the specified reducers to the store.
*/
addReducers(reducerMap: ReducersMapObject): void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument is called reducerMap here and reducers on line 294. For consistency maybe they should be the same.

const reducers = {...initialReducers};

return {
get reducer() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be more explicit to name this combinedReducer

@samisayegh
Copy link
Contributor Author

This looks good but I'm still a bit confused at what this translates to on the ui library side. When instantiating the engine no reducers are required now I guess. I'm probably just not searching right but I don't see where they're loaded dynamically based on registered components.

Yea, once we convert all the controllers to the new approach, reducers would not need to be specified. The dynamic load happens at the start of the controller, where it adds the necessary reducers to the passed engine.

@nathanlb
Copy link
Contributor

Oh I see so this is added to the controllers. Sounds good 👍 thanks!

@samisayegh samisayegh merged commit 922d113 into master Apr 15, 2021
@samisayegh samisayegh deleted the KIT-264 branch April 15, 2021 20:21
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

Successfully merging this pull request may close these issues.

4 participants