Jonathan Tuzman
Jonathan Tuzman

Reputation: 13262

Incompatible props: void not assignable to ThunkAction

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?

actions/index.ts

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:

App.tsx


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;

AppContainer.tsx


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

Answers (3)

David Bradshaw
David Bradshaw

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

Jonathan Tuzman
Jonathan Tuzman

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

markerikson
markerikson

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 on mapState and mapDispatch.

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, whereas typeof mapDispatch does not.

More details:

Upvotes: 3

Related Questions