Reputation: 13262
I have this redux thunk action that is implemented in my component which is connected to redux via a container component. I am getting an error that the props of App
itself, and the props passed to connect
, are incompatible. It looks like it's expecting a function that returns a ThunkAction
but is receiving a function that returns void
.
C:/Users/jtuzman/dev/NOUS/front_end/src/containers/AppContainer.tsx
Type error: Argument of type 'typeof App' is not assignable to parameter of type 'ComponentType<Matching<{ settingsReceived: boolean; error: Error | null; } & { changeParameter: (parameter: ParameterName, value: any) => ChangeParameterAction; reportError: (msg: string, type?: ErrorType) => ErrorAction; updateValuesFromBackend: (updatedValues: any) => void; }, AppProps>>'.
Type 'typeof App' is not assignable to type 'ComponentClass<Matching<{ settingsReceived: boolean; error: Error | null; } & { changeParameter: (parameter: ParameterName, value: any) => ChangeParameterAction; reportError: (msg: string, type?: ErrorType) => ErrorAction; updateValuesFromBackend: (updatedValues: any) => void; }, AppProps>, any>'.
Types of parameters 'props' and 'props' are incompatible.
Type 'Matching<{ settingsReceived: boolean; error: Error | null; } & { changeParameter: (parameter: ParameterName, value: any) => ChangeParameterAction; reportError: (msg: string, type?: ErrorType) => ErrorAction; updateValuesFromBackend: (updatedValues: any) => void; }, AppProps>' is not assignable to type 'Readonly<AppProps>'.
The types returned by 'updateValuesFromBackend(...)' are incompatible between these types.
Type 'void' is not assignable to type 'ThunkAction<void, { meta: { ...; }; study: { ...; }; data: { ...; } | { ...; }; }, {}, MyAction>'. TS2345
18 | mapStateToProps,
19 | mapDispatchToProps,
> 20 | )(App);
| ^
21 |
I don't understand this, firstly because my function does indeed return a ThunkAction
(I think?) but mostly because the type is typeof updateValuesFromBackend
so there shouldn't ever possibly be a mismatch anyway, correct?
BTW ThunkAction
, which my action "should be" returning but "isn't", is indeed an alias for a function that returns void! (i.e., updateValuesFromBackend
returns a ThunkAction
which is indeed a function that returns void
)
When I switch tp class App extends Component<any>
I'm able to run the app, of course.
Anyone know how I can fix this?
export const updateValuesFromBackend = (updatedValues: any): MyThunkAction => {
return (
dispatch: ThunkDispatch<AppState, {}, MyAction>,
getState: () => AppState
) => {
const changes: any = {};
Object.entries(updatedValues).forEach(([key, value]) => {
const parameter = backEndSettingsKeys[key];
if (!parameter) return;
changes[parameter] = value;
});
// if we haven't received any settings yet, accept these settings
if (true || !getState().meta.settingsReceived) {
dispatch(setParameters(changes));
} else {
// Compare the received settings with the current settings.
// If they match, we consider this a "success" response
// after a settings update request.
// If they don't, we consider this an error.
}
};
};
It is implemented in my App
component which is connected to redux in my AppContainer
:
export type AppProps = {
settingsReceived: boolean,
error: Error | null,
changeParameter: typeof changeParameter,
updateValuesFromBackend: typeof updateValuesFromBackend,
reportError: typeof reportError
}
// class App extends Component<any> {
class App extends Component<AppProps> {
settingsSubscription: W3CWebSocket;
componentDidMount(): void {
this.settingsSubscription = this.subscribeToSettings(urls.SETTINGS_WS);
}
subscribeToSettings(url: string): W3CWebSocket {
let settingsSubscription = new W3CWebSocket(url);
settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
return settingsSubscription;
}
handleSettingsMessage(message: MessageEvent) {
const updatedValues = JSON.parse(message.data);
const { settingsReceived, reportError, updateValuesFromBackend } = this.props;
if (settingsReceived) return reportError('Invalid settings received.');
console.log('updating settings');
updateValuesFromBackend(updatedValues);
}
render() {
return this.props.error
? <ErrorWrapper/>
: <InnerAppContainer/>
}
}
export default App;
const mapStateToProps = ({ meta }: AppState) => ({
settingsReceived: meta.settingsReceived,
error: meta.error
});
const mapDispatchToProps = ({
changeParameter, reportError, updateValuesFromBackend
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
Upvotes: 1
Views: 737
Reputation: 13077
Personally I think you would be better off moving all your connection logic out of your component and into either a Saga, or custom middleware. That way when you get the response from your API you can decide if you should dispatch an action or not.
Although it is REST based, rather than using WebSockets. I would suggest having a look at how redux-api-middleware
works as an example of this kind of approach.
Upvotes: 1
Reputation: 13262
Thanks very much to @markerikson for a great explanation. In my implementation, I want to keep the presentation component completely separate and ignorant of its container and its connection to the Redux store. Therefore, your specific (and excellent) suggestion of defining a connected type in the container, and importing that in my component, doesn't fit my architecture.
Your explanation of how connect
strips the dispatch section of the function signature was really the solution to my particular problem:
export type AppProps = {
settingsReceived: boolean,
error: Error | null,
changeParameter: typeof changeParameter,
updateValuesFromBackend: (updatedValues: any) => void,
reportError: typeof reportError
}
Upvotes: 1
Reputation: 67489
Yes, the problem is specifically the updateValuesFromBackend: typeof updateValuesFromBackend
part.
The actual type of that will be, roughly, () => thunkFunction => void
. However, due to the way that dispatch
and thunks work, the bound function that your own component gets will effectively look like () => void
.
The React-Redux types have some magic inside that tries to "resolve thunks", by figuring out what the thunk itself returns (basically stripping out the => thunkFunction
part). So, there is a mismatch between what you are declaring updateValuesFromBackend
to be, and what connect
will pass down.
I ran into this problem myself recently, and found a really neat solution that we will soon be officially recommending in the React-Redux docs. I described the approach over in this SO answer, but I'll paste it here for completeness:
There's a neat technique for inferring the type of the combined props that
connect
will pass to your component based onmapState
andmapDispatch
.There is a new
ConnectedProps<T>
type that is available in@types/[email protected]
. You can use it like this:
function mapStateToProps(state: MyState) {
return {
counter: state.counter
};
}
const mapDispatch = {increment};
// Do the first half of the `connect()` call separately,
// before declaring the component
const connector = connect(mapState, mapDispatch);
// Extract "the type of the props passed down by connect"
type PropsFromRedux = ConnectedProps<typeof connector>
// should be: {counter: number, increment: () => {type: "INCREMENT"}}, etc
// define combined props
type MyComponentProps = PropsFromRedux & PropsFromParent;
// Declare the component with the right props type
class MyComponent extends React.Component<MyComponentProps> {}
// Finish the connect call
export default connector(MyComponent)
Note that this correctly infers the type of thunk action creators included in
mapDispatch
if it's an object, whereastypeof mapDispatch
does not.More details:
Upvotes: 3