Add connected generic component example - #358
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a factory function pattern to connect generic React components with React-Redux's connect(), complete with documentation, implementation, and usage examples. The review feedback highlights two key areas for improvement: first, making the state type generic in the factory function to avoid unsafe type assertions in consumer components; second, introducing a keyExtractor prop to avoid using array indices as React keys, which is a known anti-pattern. The feedback also includes necessary updates to the usage examples and documentation to align with these improvements.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function connectGenericList<T>( | ||
| mapStateToProps: (state: RootState, ownProps: OwnProps<T>) => StateProps<T> | ||
| ) { | ||
| // We use a cast here because connect cannot directly handle generic components. | ||
| // The result is a connected component typed for the specific T. | ||
| return connect<StateProps<T>, {}, OwnProps<T>, RootState>(mapStateToProps)( | ||
| GenericList as new (props: GenericListProps<T>) => GenericList<T> | ||
| ); | ||
| } |
There was a problem hiding this comment.
The factory function connectGenericList is currently coupled to the global RootState type. This limits its reusability and forces consumers to use unsafe type assertions (like state as any as ExampleState in the usage example) if their local state or store slice differs. Making the state type generic (e.g., S = RootState) makes the factory function truly reusable and type-safe.
| export function connectGenericList<T>( | |
| mapStateToProps: (state: RootState, ownProps: OwnProps<T>) => StateProps<T> | |
| ) { | |
| // We use a cast here because connect cannot directly handle generic components. | |
| // The result is a connected component typed for the specific T. | |
| return connect<StateProps<T>, {}, OwnProps<T>, RootState>(mapStateToProps)( | |
| GenericList as new (props: GenericListProps<T>) => GenericList<T> | |
| ); | |
| } | |
| export function connectGenericList<T, S = RootState>( | |
| mapStateToProps: (state: S, ownProps: OwnProps<T>) => StateProps<T> | |
| ) { | |
| // We use a cast here because connect cannot directly handle generic components. | |
| // The result is a connected component typed for the specific T. | |
| return connect<StateProps<T>, {}, OwnProps<T>, S>(mapStateToProps)( | |
| GenericList as new (props: GenericListProps<T>) => GenericList<T> | |
| ); | |
| } |
| const mapStateToProps = (state: RootState) => ({ | ||
| items: (state as any as ExampleState).todos, | ||
| }); | ||
|
|
||
| // Create a connected generic list typed for Todo | ||
| export const ConnectedTodoList = connectGenericList<Todo>(mapStateToProps); |
There was a problem hiding this comment.
With the generic state parameter added to connectGenericList, we can type the state parameter in mapStateToProps directly as ExampleState and avoid the unsafe double type assertion (state as any as ExampleState).
| const mapStateToProps = (state: RootState) => ({ | |
| items: (state as any as ExampleState).todos, | |
| }); | |
| // Create a connected generic list typed for Todo | |
| export const ConnectedTodoList = connectGenericList<Todo>(mapStateToProps); | |
| // Map state to props for Todo items | |
| const mapStateToProps = (state: ExampleState) => ({ | |
| items: state.todos, | |
| }); | |
| // Create a connected generic list typed for Todo | |
| export const ConnectedTodoList = connectGenericList<Todo, ExampleState>(mapStateToProps); |
| export interface GenericListProps<T> { | ||
| items: T[]; | ||
| itemRenderer: (item: T) => JSX.Element; | ||
| } |
There was a problem hiding this comment.
Using the array index as a React key is an anti-pattern that can lead to rendering bugs and performance issues when the list is reordered or mutated. Since this is a generic component, we should allow the consumer to provide a custom key extractor function to uniquely identify each item.
| export interface GenericListProps<T> { | |
| items: T[]; | |
| itemRenderer: (item: T) => JSX.Element; | |
| } | |
| export interface GenericListProps<T> { | |
| items: T[]; | |
| itemRenderer: (item: T) => JSX.Element; | |
| keyExtractor?: (item: T, index: number) => string | number; | |
| } |
| function GenericList<T>(props: GenericListProps<T>) { | ||
| return ( | ||
| <ul> | ||
| {props.items.map((item, i) => ( | ||
| <li key={i}>{props.itemRenderer(item)}</li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Update the GenericList component to use the optional keyExtractor prop, falling back to the array index only if a custom extractor is not provided.
| function GenericList<T>(props: GenericListProps<T>) { | |
| return ( | |
| <ul> | |
| {props.items.map((item, i) => ( | |
| <li key={i}>{props.itemRenderer(item)}</li> | |
| ))} | |
| </ul> | |
| ); | |
| } | |
| function GenericList<T>(props: GenericListProps<T>) { | |
| const { items, itemRenderer, keyExtractor } = props; | |
| return ( | |
| <ul> | |
| {items.map((item, i) => { | |
| const key = keyExtractor ? keyExtractor(item, i) : i; | |
| return <li key={key}>{itemRenderer(item)}</li>; | |
| })} | |
| </ul> | |
| ); | |
| } |
| export interface OwnProps<T> { | ||
| itemRenderer: (item: T) => JSX.Element; | ||
| } |
There was a problem hiding this comment.
Add the optional keyExtractor prop to OwnProps<T> so that it can be passed through the connected component wrapper.
| export interface OwnProps<T> { | |
| itemRenderer: (item: T) => JSX.Element; | |
| } | |
| export interface OwnProps<T> { | |
| itemRenderer: (item: T) => JSX.Element; | |
| keyExtractor?: (item: T, index: number) => string | number; | |
| } |
| <ConnectedTodoList | ||
| itemRenderer={(todo: Todo) => ( | ||
| <span key={todo.id}>{todo.text}</span> | ||
| )} | ||
| /> |
There was a problem hiding this comment.
| export function connectGenericList<T>( | ||
| mapStateToProps: (state: RootState, ownProps: OwnProps<T>) => StateProps<T> | ||
| ) { | ||
| return connect<StateProps<T>, {}, OwnProps<T>, RootState>(mapStateToProps)( | ||
| GenericList as new (props: GenericListProps<T>) => GenericList<T> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Update the factory function definition in the documentation to match the generic state parameter improvement.
| export function connectGenericList<T>( | |
| mapStateToProps: (state: RootState, ownProps: OwnProps<T>) => StateProps<T> | |
| ) { | |
| return connect<StateProps<T>, {}, OwnProps<T>, RootState>(mapStateToProps)( | |
| GenericList as new (props: GenericListProps<T>) => GenericList<T> | |
| ); | |
| } | |
| export function connectGenericList<T, S = RootState>( | |
| mapStateToProps: (state: S, ownProps: OwnProps<T>) => StateProps<T> | |
| ) { | |
| return connect<StateProps<T>, {}, OwnProps<T>, S>(mapStateToProps)( | |
| GenericList as new (props: GenericListProps<T>) => GenericList<T> | |
| ); | |
| } |
Adds an example demonstrating how to combine generic components with React-Redux's connect(). This addresses the challenge of TypeScript's limitation with generic type parameters in connect() calls by using a wrapper function pattern. Closes #55