react-redux-typescript-guide
react-redux-typescript-guide copied to clipboard
suggestion: connected generic component
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!
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('!');
}
The example above from @Zummer works like a charm. I'll try to create a pull request for this example.
@Zummer Thanks for your great answer and also inspiring me to use less verbose interface names!
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
+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]'.
@issuehunt has funded $50.00 to this issue.
- Submit pull request via IssueHunt to receive this reward.
- Want to contribute? Chip in to this issue via IssueHunt.
- Checkout the IssueHunt Issue Explorer to see more funded issues.
- Need help from developers? Add your repository on IssueHunt to raise funds.
@Zummer: is there an equivalent to your suggestion above, but for use with functional components rather than class components?
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!
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
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 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.