React HoC usage with Typescript - javascript

I have a react HoC where I have define few states and I am passing that to wrapped component. But the wrapped component itself has some props.
HoC.tsx
const HOC = (Component: React.ComponentType<T>) => {
const [someState, setSomeState] = useState()
const WrappedComponent = (props: T) =>
return(
<Component {(...props) as T} someState={someState}/>
)
return WrappedComponent
}
Hoc Usage in a component which needs other props
interfae NewComponentProps {
x: number
}
const NewComponent: React.FC<NewComponentProps> = (props) => {
let {x, someState} = props
//Here I am not able to access someState prop which is coming from HOC, typescript is giving error
return ( ... )
}
export default HoC(NewComponent)
How to handle such case and if I add someState in NewComponentProps interface it will work but I have to pass someState prop when I will call the NewComponent anywhere
So what should be the props type of new component to access both props??

Here is a small example on how you might type it to make it work
type WrappedProps = {
b: string;
};
// Here you type the child component as generic T combined with
// your Wrapped props
const Wrapped = <T,>(Comp: ComponentType<T & WrappedProps>) => {
return (props: T) => {
return <Comp {...props} b="World" />;
};
};
// ============================
type CompProps = {
a: string;
};
// You need to manually pass the props of your component to the Wrapped
// function, because it can't infer the type
const Comp = Wrapped<CompProps>((props) => {
// props now has the type CompProps & WrappedProps
return (
<p>
A: {props.a}
<br />
B: {props.b}
</p>
);
});
// ============================
export default function App() {
return <Comp a="Hello" />;
}

Related

How to create higher order function in react functional component

I am trying to create a HOC using react functional component that will take a component and some props, but I think I am missing something I did not get the props value in the component which I passed. I am also using typescript
My higher-order component:
interface EditChannelInfo {
Component: any;
setIsCollapsed: Function;
isCollapsed: boolean;
}
const EditChannelInfo = (props: EditChannelInfo): ReactElement => {
const {isCollapsed, setIsCollapsed, Component} = props;
const {data: gamesList} = useGamesList();
const games = gamesList.games.map((list: GamesList) => ({
value: list.gameId,
label: list.gameName,
}));
return <Component {...props} />;
};
export default EditChannelInfo;
From here I am passing the component to the higher-order component
import EditChannelInfoWrapper from '../EditChannelInfoWrapper';
const Dashboard: NextPage = (): ReactElement => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
return (
<div>
<EditChannelInfo
Component={EditChannelInfoWrapper}
setIsCollapsed={setIsCollapsed}
isCollapsed={isCollapsed}
/>
</div>
);
};
export default Dashboard;
I am getting games undefined
interface EditChannelInfoWrapper {
games: any;
}
const EditChannelInfoWrapper = (
props: EditChannelInfoWrapper,
): ReactElement => {
const {
games,
} = props;
console.log(games);
return ()
}
It looks like you're not passing your games prop to the Component here: <Component {...props} />.
Add in your games prop and it should work as expected <Component {...props} games={games} />

How can I properly migrate a connected component from class based to functional component on TS?

I am attempting to migrate from a class based component to functional component. It is a connected component using mapState.
This is what I had:
import { connect } from 'react-redux'
import { fetchArticles } from '../shared/actions/articleActions';
import { AppState } from '../shared/types/genericTypes';
import Article from '../shared/models/Article.model';
type Props = {
articles?: Article[]
fetchArticles?: any,
};
const mapState = (state: AppState, props) => ({
articles: state.articleReducers.articles,
...props
});
const actionCreators = {
fetchArticles,
};
class NewsArticles extends Component<Props> {
componentDidMount() {
if (!this.props.articles || !this.props.articles.length) {
this.props.fetchArticles();
}
}
render() {
return (...);
}
}
export default connect(mapState, actionCreators)(NewsArticles)
Here is what I have now:
// same imports except for FC and useEffec from react.
type Props = {
articles?: Article[];
fetchArticles?: any;
};
const mapState = (state: AppState, props: Props) => ({
articles: state.articleReducers.articles,
...props,
});
const actionCreators = {
fetchArticles,
};
const NewsArticles: FC<Props> = ({ articles, fetchArticles }) => {
useEffect(() => {
if (!articles || !articles.length) {
fetchArticles();
}
}, []);
return (...);
};
export default connect(mapState, actionCreators)(NewsArticles);
The main concern I have is the props.
Before they were like this
const mapState = (state: AppState, props) => ({
articles: state.articleReducers.articles,
...props
});
And used like this:
componentDidMount() {
if (!this.props.articles || !this.props.articles.length) {
this.props.fetchArticles();
}
}
Now that I have a functional component, I am getting this
const mapState = (state: AppState, props: Props) => ({
articles: state.articleReducers.articles,
...props,
});
And used like this:
useEffect(() => {
if (!articles || !articles.length) {
fetchArticles();
}
}, []);
So how props will work now that articles and fetchArticles are not called like this.props.articles and only articles so, does make any sense to spread props …props on mapState?
There's no need to spread props in mapState.
This:
const mapState = (state: AppState, props: Props) => ({
articles: state.articleReducers.articles,
...props,
});
is equivalent to this:
const mapState = (state: AppState) => ({
articles: state.articleReducers.articles,
});
Any extra props will get passed into your component in addition to the props from mapState and mapDispatch (which you've called actionCreators).
Your code is compilable but is not fully correct with connect function signature. Lets look on signature
<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = {}>(
mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>
): InferableComponentEnhancerWithProps<TStateProps & TDispatchProps, TOwnProps>;
It has 4 types,
TSTateProps is type of props, derived from Redux state. In your sample they are
type StateProps = {
articles: Article[];
}
Only articles are derived from Redux state.
TDispatchProps is type of props containing actions your component will dispatch. As you passing actionCreators object to connect TDispatchProps should equal to typeof actionCreators (we should get type of object actionCreators).
TOwnProps is type of props your component get from parent (not from Redux). You not use props from parent, so TOwnProps will equal to {}.
TState is type of state in Redux. It is AppState.
So to be fully correct with Redux, you should do
type StateProps = {
articles: Article[];
};
const mapState = (state: AppState): StateProps => ({
articles: state.articleReducers.articles
});
const actionCreators = {
fetchArticles,
};
type Props = StateProps & typeof actionCreators; // These is correct props for your component
const NewsArticles: FC<Props> = ({ articles, fetchArticles }) => {
And in case you'll later add props from parent, just intersect them with Props.
type Props = StateProps & typeof actionCreators & OwnProps;
Your code worked as you added ...props to mapState. And props contained member fetchAction. So you finally got the same Props like I showed in my answer, but slightly incorrect.

Type of Generic Stateless Component React? OR Extending generic function interface in typescript to have a further generic?

Problem: The interface of Stateless Functional Component is given as
interface SFC<P = {}> {
(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
propTypes?: ValidationMap<P>;
}
The prop type of my component is also generic as:
interface Prop<V>{
num: V;
}
How to properly define my component? as:
const myCom: <T>SFC<Prop<T>> = <T>(props: Prop<T>)=> <div>test</div>
gives an error at character 27 that Cannot find name 'T'
Here is :Typescript Playground of modified example
MyFindings:
1:Typescript 2.9.1 support Stateful Generic Component: http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#generic-type-arguments-in-jsx-elements
class myCom<T> extends React.Component<Prop<T>, any> {
render() {
return <div>test</div>;
}
}
2: Extending SFC to make a new interface as mentioned in following answer would make component's prop type as any:
Typescript React stateless function with generic parameter/return types which I don't want. I want to give proper type for my prop
You can't use generics like this:
const myCom: <T>SFC<Prop<T>> = <T>(props: Prop<T>)=> <div>test</div>
The TypeScript spec states:
A construct of the form
< T > ( ... ) => { ... }
could be parsed as an arrow function expression with a type parameter or a type assertion applied to an arrow function with no type parameter.
source; Microsoft/TypeScript spec.md
Your declaration doesn't match the pattern defined in the TypeScript spec, therefore it wont work.
You can however don't use the SFC interface and just declare it yourself.
interface Prop<V> {
num: V;
}
// normal function
function Abc<T extends string | number>(props: Prop<T>): React.ReactElement<Prop<T>> {
return <div />;
}
// const lambda function
const Abc: <T extends string | number>(p: Prop<T>) => React.ReactElement<Prop<T>> = (props) => {
return <div />
};
export default function App() {
return (
<React.Fragment>
<Abc<number> num={1} />
<Abc<string> num="abc" />
<Abc<string> num={1} /> // string expected but was number
</React.Fragment>
);
}
There's a pattern to mitigate this issue by declaring generic component type alias outside of component and then simply asserting it when you need it.
Not as pretty, but still reusable and strict.
interface IMyComponentProps<T> {
name: string
type: T
}
// instead of inline with component assignment
type MyComponentI<T = any> = React.FC<IMyComponentProps<T>>
const MyComponent: MyComponentI = props => <p {...props}>Hello</p>
const TypedComponent = MyComponent as MyComponentI<number>
Factory pattern:
import React, { SFC } from 'react';
export interface GridProps<T = unknown> {
data: T[];
renderItem: (props: { item: T }) => React.ReactChild;
}
export const GridFactory = <T extends any>(): SFC<GridProps<T>> => () => {
return (
<div>
...
</div>
);
};
const Grid = GridFactory<string>();
UPDATE 08/03/2021
To avoid rules of hooks errors you have to finesse the syntax like this:
import React, { FC } from 'react';
export interface GridProps<T = unknown> {
data: T[]
renderItem: (props: { item: T }) => React.ReactChild
}
export const GridFactory = <T extends any>() => {
const Instance: FC<GridProps<T>> = (props) => {
const [state, setState] = useState(props.data)
return <div>...</div>
}
return Instance
}
const Grid = GridFactory<string>()
I'm proposing a similar yet albeit slightly different solution (brainstormed with a friend). We were trying to create a Formik wrapper, and managed to get it working in the following fashion:
import React, { memo } from 'react';
export type FormDefaultProps<T> = {
initialValues: T;
onSubmit<T>(values: T, actions: FormikActions<T>): void;
validationSchema?: object;
};
// We extract React.PropsWithChildren from React.FunctionComponent or React.FC
function component<T>(props: React.PropsWithChildren<FormDefaultProps<T>>) {
// Do whatever you want with the props.
return(<div>{props.children}</div>
}
// the casting here is key. You can use as typeof component to
// create the typing automatically with the generic included..
export const FormDefault = memo(component) as typeof component;
And then, you use it like:
<FormDefault<PlanningCreateValues>
onSubmit={handleSubmit}
initialValues={PlanningCreateDefaultValues}
>
{/*Or any other child content in here */}
{pages[page]}
</FormDefault>
I haven't been able to achieve this with method expressions:
const a: React.FC<MyProp> = (prop) => (<>MyComponent</>);
The Factory pattern presented here by #chris is great but I can't use React Hooks with it.
So I'm using this one.
// Props
interface Props<T> {
a: T;
}
// Component
export const MyComponent: <T>(p: PropsWithChildren<Props<T>>) => React.ReactElement = props => {
return <div>Hello Typescript</div>;
};
If you don't need children you can remove PropsWithChildren part.
Props decomposition and hooks work as well.
export const MyComponent: <T>(p: Props<T>) => React.ReactElement = ({ a }) => {
const [myState, setMyState] = useState(false);
return <div>Hello Typescript</div>;
};
i have a way to do, but I'm not sure is perfect or not
interface ComponentProps<T> {
text: T;
}
export const Component= <T,>(props: ComponentProps<T>) => {
const { text } = props
const [s] = useState(0) // you can use hook
return (<div>yes</div>)
}
you can use component like this:
(
<>
<Component<string> text="some text" />
</>
)
If you want to have React's FunctionComponent type included in the type definition (to get the return type automatically typechecked, for example), you can use a construct like this if you don't mind repeating the types a bit:
interface MyProps<T> {
val: T
}
const Component: React.FC<MyProps<any>> = <T>({
val
}: MyProps<T>) => {
// Your code and hooks here
return <div>Heya</div>
}
The type of the const Component is React.FC<MyProps<any>>, meaning that you can use it with any valid value for the MyProps type parameter T, but since the implementation re-specifies the generic type parameter T inside the implementation code its type is T and not any, and thus it will be typechecked properly. T might have restricting specifiers like extends string, you just need to repeat them in both the interface specification and the function implementation signature.
An example of generic stateless component according to jmattheis's post.
MyGenericStatelessComponent.tsx
import React from "react";
type Prop<T> = {
example: T;
};
const MyGenericStatelessComponent: <T extends Record<string, number | string>>(props: Prop<T>) => JSX.Element = <
T extends Record<string, unknown>
>(
props: Prop<T>
): JSX.Element => {
return (
<div>
Example Prop id: {props.example.id}, Example Prop name: {props.example.name}
</div>
);
};
export default MyGenericStatelessComponent;
Usage:
<MyGenericStatelessComponent example={{ id: 1, name: "test01" }} />
Using T = any as #vadistic example works but you will not have any type checking. Use this code and you will have code completion and type checks.
interface IProps<TModel> extends RouteComponentProps {
headerText?: string | React.ReactNode;
collection: TModel[];
}
interface ICommonSortableType extends ISortableItem {
id: number;
isCorrectResponse: boolean;
}
interface ISortableItem {
sortableId: number;
}
type GenericFunctionalComponent<TModel> = React.FC<IProps<TModel>>;
const CommonSortableList: GenericFunctionalComponent<ICommonSortableType> = (props) => {
...
}
Can then be used like this:
class CommonSortableType {
public sortableId: number = -1;
public id: number = -1;
public isCorrectResponse: boolean = false;
}
<CommonSortableList
collection={item.commonSortableTypes} //Is CommonSortableType[]
headerText={<FormattedMessage id="item.list" />}
</CommonSortableList>
class ExtendedOptionDto extends OptionDto implements ICommonSortableType {
public sortableId: number = -1;
}
class OptionDto {
public id: number = -1;
public isCorrectResponse: boolean = false;
}
<CommonSortableList
collection={item.extendedOptionDtos} //Is ExtendedOptionDto[]
headerText={<FormattedMessage id="item.list" />}
</CommonSortableList>

React Context API and avoiding re-renders

I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB is the only component that sees any render changes and even though b is the only part of the state tree that changes. I've tried this with functional components and with PureComponent and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
Create your reducer
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
Create your ContextProvider component
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
Use your ContextProvider at top level of your App, or where you want it
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
Write components as pure functional component
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
Have a function to select props from context (like redux map...)
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
Write a connectToContext HOC
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
Put it all together
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
Demo that I did on other StackOverflow question
Demo on codesandbox
The re-render avoided
MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.
Other solutions
I suggest check this out Preventing rerenders with React.memo and useContext hook.
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.
Check it out below, or try the CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.
Here is a great link to improve performance, you can apply the same to the context API too

typescript + redux: exclude redux props in parent component

I am using redux and typescript for my current webapp.
What is the best practice to define the props of a component which receives redux-actions via #connect, but also props from parent?
Example:
// mychild.tsx
export namespace MyChildComponent {
export interface IProps {
propertyFromParent: string;
propertyFromRedux: string; // !!!! -> This is the problem
actionsPropertyFromRedux: typeof MyReduxActions; // !!!! -> And this
}
}
#connect(mapStateToProps, mapDispatchToProps)
export class MyChildComponent extends React.Component <MyChildComponent.IProps, any> {
... react stuff
}
function mapStateToProps(state: RootState) {
return {
propertyFromRedux: state.propertyFromRedux
};
}
function mapDispatchToProps(dispatch) {
return {
actionsPropertyFromRedux: bindActionCreators(MyReduxActions as any, dispatch)
};
}
// myparent.tsx
export class MyParentComponent extends React.Component <MyParentComponent.IProps, any> {
... react stuff
render(){
// typescript complains, because I am not passing `propertyFromRedux`!
return <div><MyChildComponent propertyFromParent="yay" /></div>;
}
}
As I see it I got 2 solutions.
Just pass the actions & state down through my whole app. But that would mean that my whole app gets re-rendered even when just some small child component would have to change. Or is it the redux way to listen in my top level component on all store changes? Then I would have to write a lot of logic inside shouldComponentUpdate for props which are no flat objects.
Set the param in IProps of MyChildComponent optional like this:
-
// mychild.tsx
export namespace MyChildComponent {
export interface IProps {
propertyFromParent: string;
propertyFromRedux?: typeof MyAction; // This is the problem
}
}
Is there another way? Both of the above ways seem too messy in my eyes.
You need to split up your props - you'll need a DispatchProps, StateProps, and an OwnProps type. You'll also have to use TypeScript's generics with connect
DispatchProps are your action creators.
StateProps are your state props (duh) - these come from mapStateToProps - the return type of that function should match this type.
OwnProps are props which are accepted (and perhaps expected) by your component. Optional props should be marked as optional in the interface.
The way I do it (without decorators, but i'm sure it applies here) is
interface ComponentDispatchProps {
doSomeAction: typeof someAction;
}
interface ComponentStateProps {
somethingFromState: any;
}
interface ComponentOwnProps {
somethingWhichIsRequiredInProps: any;
somethingWhichIsNotRequiredInProps?: any;
}
// not necessary to combine them into another type, but it cleans up the next line
type ComponentProps = ComponentStateProps & ComponentDispatchProps & ComponentOwnProps;
class Component extends React.Component<ComponentProps, {}> {...}
function mapStateToProps(state, props) {
return { somethingFromState };
}
export default connect<ComponentStateProps, ComponentDispatchProps, ComponentOwnProps>(
mapStateToProps,
mapDispatchToProps
)(Component);
I think you have to use #connect<StateProps, DispatchProps, OwnProps> which will decorate and return a class which accepts OwnProps.
If you look at connects implementation in TS
export declare function connect<TStateProps, TDispatchProps, TOwnProps>(...): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>
interface ComponentDecorator<TOriginalProps, TOwnProps> {
(component: ComponentClass<TOriginalProps> | StatelessComponent<TOriginalProps>): ComponentClass<TOwnProps>;
}
connect<...> returns a ComponentDecorator, which, when passed the component (in your case this is done transparently when transpiling the decorator out), regardless of StateProps, and DispatchProps returns a component which expects OwnProps.
connect (non-generic) returns InferableComponentDecorator
export interface InferableComponentDecorator {
<P, TComponentConstruct extends (ComponentClass<P> | StatelessComponent<P>)>(component: TComponentConstruct): TComponentConstruct;
}
which attempts to infer the props based on the props supplied to the component, which in your case is the combination of all props (OwnProps becomes ComponentProps from above).
Of course you can setup types manually. But much comfortable to use generated, that you actually get from connect. It helps to avoid annoying duplicates.
Example 1:
type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>
class MyComponent extends React.PureComponent<Props> {
...
}
const mapStateToProps = (state: ReduxState) => ({
me: state.me,
})
const mapDispatchToProps = (dispatch: ReduxDispatch) => ({
doSomething: () => dispatch(Dispatcher.doSomething()),
})
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
Now we get types directly from redux state and action/dispatch functions.
After some time we simplified this example to:
Example 2:
//// other file
import { InferableComponentEnhancerWithProps } from 'react-redux'
type ExtractConnectType<T> = T extends InferableComponentEnhancerWithProps<infer K, any> ? K : T
//// <= Save it somewhere and import
type Props = ExtractConnectType<typeof connectStore>
class MyComponent extends React.PureComponent<Props> {
...
}
const connectStore = connect(
(state: ReduxState) => ({
me: state.me,
}),
(dispatch) => ({
doSomething: () => dispatch(Dispatcher.doSomething()),
})
)
export default connectStore(MyComponent)
To put it simply,
a component should be clear on what props should be coming from a parent and connect (redux).
Now, connect() can issue redux state (your app state) or action as a prop to the component and the rest props of the component should be coming from the parent.
As suggested, it is better to break the component props into 3 parts (ComponentStateProps, ComponentDispatchProps & ComponentOwnProps) and then use them in connect(). And, join these 3 props to form ComponentProps.
I think the below code will give a better understanding.
// Your redux State
type SampleAppState = {
someState: any;
};
// State props received from redux
type ChildStateProps = {
propFromState: any;
};
// dispatch action received from redux (connect)
type ChildDispatchProps = {
actionAsProp: () => void;
};
// Props received from parent
type ChildOwnProps = {
propFromParent: any;
};
// All Props
type ChildProps = ChildStateProps & ChildDispatchProps & ChildOwnProps;
const ChildComponent = (props: ChildProps) => {
return <>{/*....*/}</>;
};
let ConnectedChildComponent = connect<
ChildStateProps,
ChildDispatchProps,
ChildOwnProps,
SampleAppState
>(
(appState: SampleAppState, ownProps: ChildOwnProps) => {
// Shape that matches ChildStateProps
return {
propFromState: appState.someState,
};
},
(dispatch, ownProps: ChildOwnProps) => {
return bindActionCreators(
// Shape that matches ChildDispatchProps
{
actionAsProp: () => {},
},
dispatch,
);
},
)(ChildComponent);
const ParentComponent = () => {
return (
<>
<ConnectedChildComponent propFromParent={'Prop Value'} />
</>
);
};

Categories