Skip to content

[Enhancement] Add a nested HOCs example with connect #5

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

Closed
estaub opened this issue Jul 12, 2017 · 13 comments · Fixed by #193
Closed

[Enhancement] Add a nested HOCs example with connect #5

estaub opened this issue Jul 12, 2017 · 13 comments · Fixed by #193
Assignees
Labels
enhancement 🎁 Rewarded on Issuehunt This issue has been rewarded on Issuehunt IssueHunt
Milestone

Comments

@estaub
Copy link

estaub commented Jul 12, 2017

Issuehunt badges

You might consider writing up techniques for dealing with prop types on components with multiple nested HOCs. For example, I frequently have components with three layers of HOCs: React-Router, Redux, and Apollo-React (graphql client).

I have a handle on how to deal, but recent changes in both Typescript and various declarations have made these stricter and harder to get right. I'm not at all confident that my design pattern for this is the best, but I'll share if you've nothing.


IssueHunt Summary

piotrwitek piotrwitek has been rewarded.

Backers (Total: $50.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

@piotrwitek
Copy link
Owner

Hello,
You could definitely share some simple generic example of the use case to provide the better context of your pattern.

PS: If I ever use any part of your work I will give due credits.

@piotrwitek
Copy link
Owner

piotrwitek commented Jul 12, 2017

Also recently I wanted to rewrite and update entire HOC section, so this could give me some fresh ideas

Related: #100

@piotrwitek piotrwitek self-assigned this Aug 28, 2017
@piotrwitek piotrwitek added this to the Next milestone Dec 23, 2017
@IssueHuntBot
Copy link

@BoostIO funded this issue with $20. Visit this issue on Issuehunt

@IssueHuntBot
Copy link

@IssueHunt has funded $30.00 to this issue.


@piotrwitek piotrwitek changed the title [Enhancement] Deeper docs for nested HOCs? [Enhancement] Add a nested HOCs example with connect Nov 3, 2019
@piotrwitek
Copy link
Owner

I have added a new section with an example of nested HOC with connect (latest TS and react-redux types):

import { RootState } from 'MyTypes';
import React from 'react';
import { connect } from 'react-redux';
import { Diff } from 'utility-types';
import { countersActions, countersSelectors } from '../features/counters';

// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () => void;
}

export const withConnectedCount = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  type HocProps = Diff<BaseProps, InjectedProps> & {
    // here you can extend hoc with new props
    initialCount?: number;
  };

  const mapStateToProps = (state: RootState) => ({
    count: countersSelectors.getReduxCounter(state.counters),
  });

  const dispatchProps = {
    onIncrement: countersActions.increment,
  };

  class Hoc extends React.Component<InjectedProps> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withConnectedCount(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;

    render() {
      const { count, onIncrement, ...restProps } = this.props;

      return (
        <BaseComponent
          count={count} // injected
          onIncrement={onIncrement} // injected
          {...(restProps as BaseProps)}
        />
      );
    }
  }

  const ConnectedHoc = connect<
    ReturnType<typeof mapStateToProps>,
    typeof dispatchProps,
    HocProps,
    RootState
  >(
    mapStateToProps,
    dispatchProps
  )(Hoc);

  return ConnectedHoc;
};

piotrwitek added a commit that referenced this issue Nov 3, 2019
piotrwitek added a commit that referenced this issue Nov 3, 2019
… recent TypeScript 3.7 and React & Redux type definitions. (#193)

* Updated deps

* Updated deps and refactored code to fix breaking changes

* Fixed issue with HOC Fixed #111

* Added an example of nested HOC with connect. Fixed #5

* Updated readme Intro & TOC

* Updated PR template

* Added new section with Nested HOC - wrapping a component, injecting props and connecting to redux #5

* Updated dev deps
@issuehunt-oss
Copy link

issuehunt-oss bot commented Nov 3, 2019

@piotrwitek has rewarded $35.00 to @piotrwitek. See it on IssueHunt

  • 💰 Total deposit: $50.00
  • 🎉 Repository reward(20%): $10.00
  • 🔧 Service fee(10%): $5.00

@issuehunt-oss issuehunt-oss bot added the 🎁 Rewarded on Issuehunt This issue has been rewarded on Issuehunt label Nov 3, 2019
@zdila
Copy link

zdila commented Nov 4, 2019

@piotrwitek thanks. Unfortunately I still have a type problem:

Argument of type 'typeof Hoc' is not assignable to parameter of type 'ComponentType<Matching<{ languageCounter: number; } & DispatchProp<AnyAction>, Pick<BaseProps, SetDifference<keyof BaseProps, "t">>>>'.
  Type 'typeof Hoc' is not assignable to type 'ComponentClass<Matching<{ languageCounter: number; } & DispatchProp<AnyAction>, Pick<BaseProps, SetDifference<keyof BaseProps, "t">>>, any>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'Matching<{ languageCounter: number; } & DispatchProp<AnyAction>, Pick<BaseProps, SetDifference<keyof BaseProps, "t">>>' is not assignable to type 'Readonly<Pick<BaseProps, SetDifference<keyof BaseProps, "t">>>'.
        Type 'P extends "languageCounter" | "dispatch" ? ({ languageCounter: number; } & DispatchProp<AnyAction>)[P] extends Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[P] ? Pick<BaseProps, SetDifference<...>>[P] : ({ ...; } & DispatchProp<...>)[P] : Pick<...>[P]' is not assignable to type 'BaseProps[P]'.
          Type 'Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[P] | (({ languageCounter: number; } & DispatchProp<AnyAction>)[P] extends Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[P] ? Pick<...>[P] : ({ ...; } & DispatchProp<...>)[P])' is not assignable to type 'BaseProps[P]'.
            Type '({ languageCounter: number; } & DispatchProp<AnyAction>)[P] extends Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[P] ? Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[P] : ({ ...; } & DispatchProp<...>)[P]' is not assignable to type 'BaseProps[P]'.
              Type '({ languageCounter: number; } & DispatchProp<AnyAction>)[P] | Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[P]' is not assignable to type 'BaseProps[P]'.
                Type '({ languageCounter: number; } & DispatchProp<AnyAction>)[P]' is not assignable to type 'BaseProps[P]'.
                  Type '{ languageCounter: number; } & DispatchProp<AnyAction>' is not assignable to type 'BaseProps'.
                    Type 'SetDifference<keyof BaseProps, "t"> extends "languageCounter" | "dispatch" ? ({ languageCounter: number; } & DispatchProp<AnyAction>)[("languageCounter" & SetDifference<keyof BaseProps, "t">) | ("dispatch" & SetDifference<...>)] extends Pick<...>[("languageCounter" & SetDifference<...>) | ("dispatch" & SetDifference...' is not assignable to type 'BaseProps[P]'.
                      Type '(({ languageCounter: number; } & DispatchProp<AnyAction>)[("languageCounter" & SetDifference<keyof BaseProps, "t">) | ("dispatch" & SetDifference<keyof BaseProps, "t">)] extends Pick<...>[("languageCounter" & SetDifference<...>) | ("dispatch" & SetDifference<...>)] ? Pick<...>[("languageCounter" & SetDifference<...>...' is not assignable to type 'BaseProps[P]'.
                        Type '({ languageCounter: number; } & DispatchProp<AnyAction>)[("languageCounter" & SetDifference<keyof BaseProps, "t">) | ("dispatch" & SetDifference<keyof BaseProps, "t">)] extends Pick<...>[("languageCounter" & SetDifference<...>) | ("dispatch" & SetDifference<...>)] ? Pick<...>[("languageCounter" & SetDifference<...>)...' is not assignable to type 'BaseProps[P]'.
                          Type '({ languageCounter: number; } & DispatchProp<AnyAction>)[("languageCounter" & SetDifference<keyof BaseProps, "t">) | ("dispatch" & SetDifference<keyof BaseProps, "t">)] | Pick<...>[("languageCounter" & SetDifference<...>) | ("dispatch" & SetDifference<...>)]' is not assignable to type 'BaseProps[P]'.
                            Type '({ languageCounter: number; }["languageCounter" & SetDifference<keyof BaseProps, "t">] & DispatchProp<AnyAction>["languageCounter" & SetDifference<keyof BaseProps, "t">]) | ({ ...; }["dispatch" & SetDifference<...>] & DispatchProp<...>["dispatch" & SetDifference<...>])' is not assignable to type 'BaseProps[P]'.
                              Type '{ languageCounter: number; }["languageCounter" & SetDifference<keyof BaseProps, "t">] & DispatchProp<AnyAction>["languageCounter" & SetDifference<keyof BaseProps, "t">]' is not assignable to type 'BaseProps[P]'.
                                Type 'Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[string] | Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[number] | Pick<BaseProps, SetDifference<keyof BaseProps, "t">>[symbol]' is not assignable to type 'BaseProps[P]'.
                                  Type 'BaseProps[string]' is not assignable to type 'BaseProps[P]'.
                                    Type 'string' is not assignable to type 'P'.
                                      'string' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'string | number | symbol'.

Could you please help me with that? In my case I don't use dispatchProps.

My code:

import * as React from 'react';
import { translate, splitAndSubstitute } from 'fm3/stringUtils';
import { Diff } from 'utility-types';
import { connect } from 'react-redux';
import { RootState } from './storeCreator';

function tx(key: string, params: { [key: string]: any } = {}, dflt = '') {
  const t = translate(window.translations, key, dflt);
  return typeof t === 'function' ? t(params) : splitAndSubstitute(t, params);
}

export type Translator = typeof tx;

interface InjectedProps {
  t: Translator;
}

export const withTranslator = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>,
) => {
  type HocProps = Diff<BaseProps, InjectedProps>;

  class Hoc extends React.Component<HocProps> {
    static displayName = `injectL10n(${BaseComponent.name})`;
    static readonly WrappedComponent = BaseComponent;

    render() {
      const { ...restProps } = this.props;
      return <BaseComponent t={tx} {...(restProps as BaseProps)} />;
    }
  }

  const mapStateToProps = (state: RootState) => ({
    languageCounter: state.l10n.counter, // force applying english language on load
  });

  return connect<
    ReturnType<typeof mapStateToProps>,
    undefined,
    HocProps,
    RootState
  >(mapStateToProps)(Hoc);
};

@piotrwitek
Copy link
Owner

class Hoc extends React.Component<HocProps> { <= InjectedProps not HocProps

@zdila
Copy link

zdila commented Nov 4, 2019

Thank you. Now HOC with connect compiles fine, but its usage fails:

type Props = {
  t: Translator;
}

const FooInt: React.FC<Props> = ({ t }) => {
 // ...
}

const Foo = withTranslator(FooInt);

...

<Foo />
// ^-- Property 't' is missing in type '{}' but required in type 'Pick<InjectedProps, "t">'.

@piotrwitek
Copy link
Owner

@zdila It should work just fine. Did you change anything else except the single line I have pointed out?

Could you please copy your entire file again?

@zdila
Copy link

zdila commented Nov 5, 2019

Piotr, you can see it at https://github.com/FreemapSlovakia/freemap-v3-react/blob/46ae01320e276ed9eb55bc56af4f85d9a813a8bb/src/l10nInjector.tsx

If you like, you can also clone the project (branch hoc-types) and run npm i && npm run livereload.

@piotrwitek
Copy link
Owner

@zdila Thanks for branch, I have figured out your problem.

The problem was that Hoc component was receiving incorrect type. It should be the return type of mapStateToProps, NOT an InjectedProps definition.

Thanks for providing your use case, I have improved my example to better convey this distinction for other people.

Here is a corrected code:

import * as React from 'react';
import { translate, splitAndSubstitute } from 'fm3/stringUtils';
import { Diff } from 'utility-types';
import { connect } from 'react-redux';
import { RootState } from './storeCreator';

function tx(key: string, params: { [key: string]: any } = {}, dflt = '') {
  const t = translate(window.translations, key, dflt);
  return typeof t === 'function' ? t(params) : splitAndSubstitute(t, params);
}

export type Translator = typeof tx;

interface InjectedProps {
  t: Translator;
}

export const withTranslator = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>,
) => {
  const mapStateToProps = (state: RootState) => ({
    languageCounter: state.l10n.counter, // force applying english language on load
  });

  type HocProps = ReturnType<typeof mapStateToProps>;

  class Hoc extends React.Component<HocProps> {
    static displayName = `injectL10n(${BaseComponent.name})`;
    static readonly WrappedComponent = BaseComponent;

    render() {
      const { languageCounter, ...restProps } = this.props;

      // TODO: do something with languageCounter

      return <BaseComponent t={tx} {...(restProps as BaseProps)} />;
    }
  }

  type OwnProps = Diff<BaseProps, InjectedProps>;

  return connect<HocProps, undefined, OwnProps, RootState>(mapStateToProps)(
    Hoc,
  );
};

type Props = {
  t: Translator;
};

declare const FooInt: React.FC<Props>;

const Foo = withTranslator(FooInt);

<Foo />;

@zdila
Copy link

zdila commented Nov 5, 2019

Thanks! Now it works for me 👍.

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