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

suggestion: connected generic component #55

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.

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

No branches or pull requests

10 participants