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

feat(ts): allow custom ui state and route state in routing #4816

Merged
merged 6 commits into from
Aug 4, 2021

Conversation

Haroenv
Copy link
Contributor

@Haroenv Haroenv commented Jul 28, 2021

InstantSearch generic:

import algoliasearch from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js/es';
import { history } from 'instantsearch.js/es/lib/routers';
import { StateMapping, UiState } from 'instantsearch.js/es/types';

type SwagIndexUiState = { swag: boolean };
type SwagUiState = { [indexName: string]: SwagIndexUiState };

// can also be written inline, no need for the type then
const stateMapping: StateMapping<SwagUiState, SwagUiState> = {
  stateToRoute(uiState) {
    return Object.keys(uiState).reduce(
      (state, indexId) => ({
        ...state,
        [indexId]: { swag: uiState[indexId].swag },
      }),
      {}
    );
  },

  routeToState(routeState = {}) {
    return Object.keys(routeState).reduce(
      (state, indexId) => ({
        ...state,
        [indexId]: routeState[indexId],
      }),
      {}
    );
  },
};

instantsearch<SwagUiState, SwagUiState>({
  indexName: '',
  searchClient: algoliasearch('', ''),
  routing: {
    router: history(),
    stateMapping,
  },
});

generic middleware

import instantsearch from 'instantsearch.js/es'
import { history } from 'instantsearch.js/es/lib/routers';
import { createRouterMiddleware } from 'instantsearch.js/es/middlewares';
import { StateMapping, UiState } from 'instantsearch.js/es/types';

type SwagIndexUiState = { swag: boolean };
type SwagUiState = { [indexName: string]: SwagIndexUiState };

const stateMapping: StateMapping<SwagUiState, SwagUiState> = {
  stateToRoute(uiState) {
    return Object.keys(uiState).reduce(
      (state, indexId) => ({
        ...state,
        [indexId]: { swag: uiState[indexId].swag },
      }),
      {}
    );
  },

  routeToState(routeState = {}) {
    return Object.keys(routeState).reduce(
      (state, indexId) => ({
        ...state,
        [indexId]: routeState[indexId],
      }),
      {}
    );
  },
};

const search = instantsearch();

search.use(
  createRouterMiddleware<SwagUiState, SwagUiState>({
    router: history(),
    stateMapping,
  })
);
search.addWidgets([instantsearch.widgets.hits({ container })]);

references

fixes DX-2298

While I couldn't find a way to make InstantSearch itself generic (this gets passed to many places, which then loses generic), using the routing middleware directly is possible like this now

```ts
import instantsearch from 'instantsearch.js/es'
import { history } from 'instantsearch.js/es/lib/routers';
import { createRouterMiddleware } from 'instantsearch.js/es/middlewares';
import { StateMapping, UiState } from 'instantsearch.js/es/types';

type SwagIndexUiState = { swag: boolean };
type SwagUiState = { [indexName: string]: SwagIndexUiState };

const stateMapping: StateMapping<UiState & SwagUiState, SwagUiState> = {
  stateToRoute(uiState) {
    return Object.keys(uiState).reduce(
      (state, indexId) => ({
        ...state,
        [indexId]: { swag: uiState[indexId].swag },
      }),
      {}
    );
  },

  routeToState(routeState = {}) {
    return Object.keys(routeState).reduce(
      (state, indexId) => ({
        ...state,
        [indexId]: routeState[indexId],
      }),
      {}
    );
  },
};

const search = instantsearch();

search.use(
  createRouterMiddleware<UiState & SwagUiState, SwagUiState>({
    router: history(),
    stateMapping,
  })
);
search.addWidgets([instantsearch.widgets.hits({ container })]);
```
@Haroenv Haroenv requested review from a team, brob, tkrugg and francoischalifour and removed request for a team and brob July 28, 2021 14:53
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jul 28, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit cc02f67:

Sandbox Source
InstantSearch.js Configuration

Copy link
Member

@francoischalifour francoischalifour left a comment

Choose a reason for hiding this comment

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

Can we let users not have to provide UiState, but we augment it on our side?

Copy link
Contributor Author

@Haroenv Haroenv left a comment

Choose a reason for hiding this comment

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

is this what you expect @francoischalifour ?

router = historyRouter(),
stateMapping = simpleStateMapping(),
router = historyRouter<TRouteState>(),
// technically this is wrong, as without stateMapping parameter given, the routeState *must* be UiState
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain a bit more?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

simpleStateMapping is a StateMapping<UiState, UiState>, and createRouterMiddleware can be called with a different generic than UiState, so the types don't really match. The only thing I can think of otherwise is only conditionally allowing stateMapping to be conditional, but I couldn't find how

Copy link
Contributor Author

@Haroenv Haroenv Jul 30, 2021

Choose a reason for hiding this comment

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

I tried things with branding, but still could not get anything working as soon as simpleStatemapping is actually used (as it uses UiState (const stateMapping: StateMapping<UiState, TRouteState> | StateMapping<TRouteState, TRouteState> becomes a union, and even though in that case both types are the same, typescript doesn't seem to be able to know that)

things I have tried
export type RouterProps<
  TUiState extends UiState = UiState,
  TRouteState = UiState & { ___default: true }
> = TUiState extends { ___default: true }
  ? {
      router?: Router<TRouteState>;
      stateMapping?: never;
    }
  : {
      router?: Router<TRouteState>;
      stateMapping: StateMapping<TUiState, TRouteState>;
    };

export const createRouterMiddleware = <
  TUiState extends UiState = UiState,
  TRouteState = UiState & { ___default: true }
>(
  props: RouterProps<TUiState, TRouteState> = {} as RouterProps<
    TUiState,
    TRouteState
  >
): InternalMiddleware<TUiState> => {
  const {
    router = historyRouter<TRouteState>(),
    stateMapping = simpleStateMapping<TRouteState>(),
  } = props;

and variations of it

Comment on lines +30 to +33
// We have to cast simpleStateMapping as a StateMapping<TUiState, TRouteState>.
// this is needed because simpleStateMapping is StateMapping<TUiState, TUiState>.
// While it's only used when UiState and RouteState are the same, unfortunately
// TypeScript still considers them separate types.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

new comment @francoischalifour

Comment on lines +5 to +6
export type MiddlewareDefinition<TUiState extends UiState = UiState> = {
onStateChange(options: { uiState: TUiState }): void;
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it supposed to be this here too?

Suggested change
export type MiddlewareDefinition<TUiState extends UiState = UiState> = {
onStateChange(options: { uiState: TUiState }): void;
export type MiddlewareDefinition<TUiState = Record<string, unknown>> = {
onStateChange(options: { uiState: UiState & TUiState }): void;

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 was thinking every place except the root only needs to deal with UiState and likely no extensions (not really user code), so all except the entry point expects the consumer to make the mix

Copy link
Member

Choose a reason for hiding this comment

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

Right, that looks fair like that!

Copy link
Member

Choose a reason for hiding this comment

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

Right, that looks fair like that!

Comment on lines +5 to +6
export type MiddlewareDefinition<TUiState extends UiState = UiState> = {
onStateChange(options: { uiState: TUiState }): void;
Copy link
Member

Choose a reason for hiding this comment

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

Right, that looks fair like that!

Comment on lines +5 to +6
export type MiddlewareDefinition<TUiState extends UiState = UiState> = {
onStateChange(options: { uiState: TUiState }): void;
Copy link
Member

Choose a reason for hiding this comment

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

Right, that looks fair like that!

@Haroenv Haroenv merged commit 5f8ba5d into master Aug 4, 2021
@Haroenv Haroenv deleted the feat/custom-widget-routing branch August 4, 2021 13:20
@Haroenv
Copy link
Contributor Author

Haroenv commented Aug 4, 2021

Right, that looks fair like that!

1 similar comment
@francoischalifour
Copy link
Member

Right, that looks fair like that!

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.

2 participants