Skip to content

suggestion: connected generic component #55

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
bboxstart opened this issue Feb 9, 2018 · 11 comments
Open

suggestion: connected generic component #55

bboxstart opened this issue Feb 9, 2018 · 11 comments

Comments

@bboxstart
Copy link

The repository already contains nice examples of generic components (generic-list) and connected components (sfc-counter-connected), but I'm having problems with the correct declaration and usage of connected generic components.

I would like to be able to write something like:
export const ConnectedListExtended<T> = connect<GenericListProps<T>, {}, OwnProps>(mapStateToProps)(GenericList<T>);

An example of the combination of these two examples would be really helpfull.

Thanks in advance!

@Zummer
Copy link

Zummer commented Feb 14, 2018

Hi!
try it like this: function HelloContainer

export default function HelloContainer<T>() {
    return connect<StateFromProps, DispatchFromProps>(
        mapStateToProps, mapDispatchToProps
    )(Hello as new(props: ComponentProps<T>) => Hello<T>);
}
// src/containers/HelloContainer.tsx

import * as actions from '../actions/';
import { ComponentProps, DispatchFromProps, StateFromProps, StoreState } from '../types';
import { Dispatch } from 'react-redux';
import '../components/Hello.css';
import { connect } from 'react-redux';
import Hello from '../components/Hello';

function mapStateToProps({ enthusiasmLevel }: StoreState): StateFromProps {
    return {
        enthusiasmLevel,
    };
}

function mapDispatchToProps(
    dispatch: Dispatch<actions.EnthusiasmAction>
): DispatchFromProps {
    return {
        onIncrement: () => dispatch(actions.incrementEnthusiasm()),
        onDecrement: () => dispatch(actions.decrementEnthusiasm()),
    };
}

export default function HelloContainer<T>() {
    return connect<StateFromProps, DispatchFromProps>(
        mapStateToProps, mapDispatchToProps
    )(Hello as new(props: ComponentProps<T>) => Hello<T>);
}

use function HelloContainer

// src/index.tsx

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import registerServiceWorker from './registerServiceWorker';
import './index.css';
import { createStore } from 'redux';
import { enthusiasm } from './reducers';
import { StoreState } from './types';
import HelloContainer from './containers/HelloContainer';
import { Provider } from 'react-redux';

const store = createStore<StoreState>(enthusiasm, {
    enthusiasmLevel: 1,
});

// Assign a type
const HelloNumber = HelloContainer<number>();
const HelloString = HelloContainer<string>();

ReactDOM.render(
    <Provider store={store}>
        <div>
            <HelloNumber
                name={555}
            />
            <HelloString
                name={'TypeScript'}
            />
        </div>
    </Provider>,
    document.getElementById('root') as HTMLElement
);
registerServiceWorker();
// src/types/index.tsx

export interface StoreState {
    enthusiasmLevel: number;
}

export interface StateFromProps {
    enthusiasmLevel: number;
}

// merged type
export declare type ComponentProps<T> = StateFromProps & OwnProps<T> & DispatchFromProps;

// the type we want to make variable
export interface OwnProps<T> {
    name: T;
}

export interface DispatchFromProps {
    onIncrement: () => void;
    onDecrement: () => void;
}
// src/components/Hello.tsx

import * as React from 'react';
import './Hello.css';
import { ComponentProps } from '../types';

class Hello<T> extends React.Component<ComponentProps<T>> {
    constructor(props: ComponentProps<T>) {
        super(props);
    }
    render() {
        const { name, enthusiasmLevel = 1, onIncrement, onDecrement } = this.props;

        if (enthusiasmLevel <= 0) {
            throw new Error('You could be a little more enthusiastic. :D');
        }

        return (
            <div className="hello">
                <div className="greeting">
                    Hello {name + getExclamationMarks(enthusiasmLevel)}
                </div>
                <div>
                    <button onClick={onDecrement}>-</button>
                    <button onClick={onIncrement}>+</button>
                </div>
            </div>
        );
    }
}

export default Hello;

// helpers

function getExclamationMarks(numChars: number) {
    return Array(numChars + 1).join('!');
}

@bboxstart
Copy link
Author

The example above from @Zummer works like a charm. I'll try to create a pull request for this example.

@mellis481
Copy link

@Zummer Thanks for your great answer and also inspiring me to use less verbose interface names!

@rjdestigter
Copy link

rjdestigter commented Oct 3, 2018

What about this setup:

import * as React from 'react'
import { connect } from 'react-redux'

const mapStateToProps = (storeSate: any) => {
  return {
    foo: 144
  }
}

const container = connect(mapStateToProps)

interface TInjectedProps {
  foo: number
}

export function hoc1<TRequiredProps extends TInjectedProps>(Component: React.ComponentType<TRequiredProps>) {
  const connected = container(Component)
}

export function hoc2<TRequiredProps>(Component: React.ComponentType<TRequiredProps & TInjectedProps>) {
  const connected = container(Component)
}

export function hoc3<TRequiredProps extends {}>(Component: React.ComponentType<TRequiredProps & TInjectedProps>) {
  const connected = container(Component)
}

In all three cases I get the error:

Type 'TInjectedProps[P]' is not assignable to type
    'P extends "foo" | "dispatch"
        ? ({ foo: number; } & DispatchProp<AnyAction>)[P] extends TRequiredProps[P]

            ? TRequiredProps[P]
            : ({ foo: number; } & DispatchProp<AnyAction>)[P]
        
        : TRequiredProps[P]'.
@types/react: ^16.4.14
@types/react-dom: ^16.0.8
@types/react-redux: ^6.0.9
typescript: 3.1.1

@zhukevgeniy
Copy link

zhukevgeniy commented Oct 16, 2018

+1

import React from "react";
import { Subtract } from "utility-types";
import { connect } from "react-redux";
import { rangeVisibilitySelector } from "./date-range.selectors";

interface IInjectedProps {
  visible: boolean;
}

interface IMappedProps {
  isVisible: boolean;
}

const withIsVisibleRange = <T extends IInjectedProps>(
  Component: React.ComponentType<T>
) => {
  const WrappedComponent: React.SFC<
    Subtract<T, IInjectedProps> & IMappedProps
  > = ({ isVisible, ...rest }: IMappedProps) => {
    return <Component {...rest} visible={isVisible} />;
  };

  const mapStateToProps = (state: ApplicationState) => ({
    isVisible: rangeVisibilitySelector(state)
  });

  return connect(
    mapStateToProps,
    null
  )(WrappedComponent);
};

export default withIsVisibleRange;

In this case I get:

Error:(30, 5) TS2345: Argument of type 'StatelessComponent<Pick<T, SetDifference<keyof T, "visible">> & IMappedProps>' is not assignable to parameter of type 'ComponentType<Matching<{ isVisible: boolean; } & null, Pick<T, SetDifference<keyof T, "visible">> & IMappedProps>>'. Type 'StatelessComponent<Pick<T, SetDifference<keyof T, "visible">> & IMappedProps>' is not assignable to type 'StatelessComponent<Matching<{ isVisible: boolean; } & null, Pick<T, SetDifference<keyof T, "visible">> & IMappedProps>>'. Type 'Pick<T, SetDifference<keyof T, "visible">> & IMappedProps' is not assignable to type 'Matching<{ isVisible: boolean; } & null, Pick<T, SetDifference<keyof T, "visible">> & IMappedProps>'. Type '(Pick<T, SetDifference<keyof T, "visible">> & IMappedProps)[P]' is not assignable to type 'P extends "isVisible" ? ({ isVisible: boolean; } & null)[P] extends (Pick<T, SetDifference<keyof T, "visible">> & IMappedProps)[P] ? (Pick<T, SetDifference<keyof T, "visible">> & IMappedProps)[P] : ({ ...; } & null)[P] : (Pick<...> & IMappedProps)[P]'.

@IssueHuntBot
Copy link

@IssueHunt has funded $50.00 to this issue.


@yonigibbs
Copy link

@Zummer: is there an equivalent to your suggestion above, but for use with functional components rather than class components?

@yonigibbs
Copy link

yonigibbs commented Jul 22, 2019

Actually, scratch that, think I found it:

type Props<T> = {
    ...
}

const MyComponent = <T extends {}>(props: Props<T>) : JSX.Element => {
    // render something
}

export default function MyConnectedComponent<T>() {
    return connect(mapStateToProps, mapDispatchToProps)(
        MyComponent as (props: Props<T>) => JSX.Element)
}

Seems to work. Anyone got any thoughts on whether this is/isn't a good approach?

One thing I wondered was what is the best thing to return from the MyComponent function (and the call to it in connect): should I return JSX.Element or React.ReactElement<Props<T>>. JSX.Element extends React.ReactElement<any, any> so by returning JSX.Element we seem to be losing the generic type definition, but I'm not sure if that will actually affect anything.

Thanks for the original workaround, @Zummer. Very helpful!

@pbn04001
Copy link

Still can't get this working. I have this per comments, and no typescript errors, but the component doesn't render anything.
export function CMSContent<T>() { return connect<StateFromProps>(mapStateToProps)(ContentComponent as (props: Props<T>) => JSX.Element); }

My thoughts is it would need to be something like this
export function CMSContent<T>(props:Props<T>) { return connect<StateFromProps>(mapStateToProps)(ContentComponent as (props: Props<T>) => JSX.Element); }
But I don't know what to do with props to pass them down into the connected component and have it render

@danielrsantana-sastrix
Copy link

This is how I am doing now a days... hope it helps.

typescript class with redux

export class MyViewClass extends React.Component<RoutedProps<BaseProps>, AppState<BaseState>> {
 // my view class implementation
}

export const mapStoreToProps = (store: MyStore): BaseProps => ({
  isAuthenticated: store.authentication.isAuthenticated,
  user: store.authentication.user,
  isLoading: store.company.isLoading,
});

const mapDispatchToProps = (dispatch: any): DispatchProps => ({
  logout: () => dispatch(logout()),
  loadCompany: (companyId: number) => dispatch(loadCompany(companyId)),
});

export const MyView = withRouter(connect(mapStoreToProps, mapDispatchToProps)(MyViewClass));

Hooks same way... just change the name actually

React Hooks With Typescript

export const MyHookFC: React.FC<NoResultFeatureProps> = (props: NoResultFeatureProps) => {
  // my hook implementation
}

export const mapStoreToProps = (store: MyStore): BaseProps => ({
  isAuthenticated: store.authentication.isAuthenticated,
  user: store.authentication.user,
  isLoading: store.company.isLoading,
});

const mapDispatchToProps = (dispatch: any): DispatchProps => ({
  logout: () => dispatch(logout()),
  loadCompany: (companyId: number) => dispatch(loadCompany(companyId)),
});

export const MyHook = withRouter(connect(mapStoreToProps, mapDispatchToProps)(MyHookFC));

@pbn04001
Copy link

pbn04001 commented Feb 1, 2021

This is how I am doing now a days... hope it helps.

typescript class with redux

export class MyViewClass extends React.Component<RoutedProps<BaseProps>, AppState<BaseState>> {
 // my view class implementation
}

export const mapStoreToProps = (store: MyStore): BaseProps => ({
  isAuthenticated: store.authentication.isAuthenticated,
  user: store.authentication.user,
  isLoading: store.company.isLoading,
});

const mapDispatchToProps = (dispatch: any): DispatchProps => ({
  logout: () => dispatch(logout()),
  loadCompany: (companyId: number) => dispatch(loadCompany(companyId)),
});

export const MyView = withRouter(connect(mapStoreToProps, mapDispatchToProps)(MyViewClass));

Hooks same way... just change the name actually

React Hooks With Typescript

export const MyHookFC: React.FC<NoResultFeatureProps> = (props: NoResultFeatureProps) => {
  // my hook implementation
}

export const mapStoreToProps = (store: MyStore): BaseProps => ({
  isAuthenticated: store.authentication.isAuthenticated,
  user: store.authentication.user,
  isLoading: store.company.isLoading,
});

const mapDispatchToProps = (dispatch: any): DispatchProps => ({
  logout: () => dispatch(logout()),
  loadCompany: (companyId: number) => dispatch(loadCompany(companyId)),
});

export const MyHook = withRouter(connect(mapStoreToProps, mapDispatchToProps)(MyHookFC));

This still doesn't fix the issue of using generics on your class properties. Here is my class code.

interface Props<T> {
  view: CmsView,
  children: (content: T) => ReactNode,
  contentKey: keyof HomeQuery,
}

class ContentComponent<T> extends Component<Props<T>> {
  render() {
    const cms = getCMS(store.getState());
    const { view, contentKey, children } = this.props;
    let content: T | undefined;
    if (view === CmsView.HOME) {
      content = cms?.home?.[contentKey] as T;
    }
    if (content) {
      return <>{children(content)}</>;
    }
    return null;
  }
}

export const CMSContent = connect(mapStateToProps)(ContentComponent);

But I still get this warning when I try to use the component.

<CMSContent<HomepagePromoBannerCollection>
  contentKey="homepagePromoBannerCollection"
  view={CmsView.HOME}
>

TS2558: Expected 0 type arguments, but got 1.

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

10 participants