I know next-to-nothing about any of these, except for Typescript which I know a bit about. The goal is to produce the most minimal example possible of a JS app with state, i.e. a counter, the classic demo.

Set up the project with npx create-react-app myapp --typescript.

First off: You want both pieces, redux and react-redux. You will use imports from both of these.

To get data into and out of your components, you'll need a store. The store is constructed with the createStore function from redux.

You always need a reducer to construct a store. A reducer is a function from a state and an action to the next state.

This should be the contents of index.tsx:

interface MyState {
    counter: number;
}

const INCREMENT = 'INCREMENT';

interface IncrementAction {
    type: typeof INCREMENT
}

type MyActionTypes = IncrementAction;


function myReducer(state: MyState | undefined, action: MyActionTypes): MyState {
    console.log("Reducer being called.");
    if (state === undefined) {
        return { counter: 0 };
    }

    switch (action.type) {
        case INCREMENT:
            return Object.assign({}, state, { counter: state.counter + 1 });
        default:
            return state;
    }
}

const store = createStore(myReducer);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

Points to note here:

  • MyActionTypes is a sum type with only one member.
  • Each action has a mandatory property, type. The use of typeof in the definition of IncrementAction ensures that type receives a string literal type. This in turn enables the switch below to be type safe.
  • The structure of the switch will make sure that action is inferred to the correct type within its case branch. That is, even though MyActionTypes may be Foo | Bar, within its matching case branch TS knows that action is FooAction or BarAction.
  • Provider is a wrapper component that will wire up the store to all components that are beneath it in the component tree.
  • The type of the first argument must be MyState | undefined NOT simply MyState. You react to the undefined state by configuring the initial state. You're going to get confusing type errors when you try to call createStore if you type this wrongly.

This should be the contents of App.tsx. Note that you also need the type definitions from above, I recommend extracting them to a file.

function mapStateToProps(state: MyState) {
    return {
        counter: state.counter
    };
}

function incrementActionCreator(): IncrementAction {
    return {
        type: INCREMENT
    };
}

const mapDispatchToProps = {
    increment: incrementActionCreator
};

interface AppProps {
    counter: number;
    increment: () => void;
}


class App2 extends React.Component<AppProps> {
    render() {
        const { counter, increment } = this.props;

        return (
            <div className="App">
                <header className="App-header">
                    <img src={logo} className="App-logo" alt="logo" />

                    <p>Counter value: {counter}</p>

                    <button onClick={increment}>Increment</button>

                    <a
                        className="App-link"
                        href="https://reactjs.org"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        Learn React
                </a>
                </header>
            </div>
        );
    }
}


export default connect(
    mapStateToProps, mapDispatchToProps
)(App2);

Things to note here:

  • The connect function lives in react-redux.
  • App2 needs to be converted to the class-based component syntax, it won't work with the functional component syntax, as far as I can see.
  • An action creator is a function returning an action, which in this case just means that it returns an object that has a type property.
  • The argument to mapStateToProps tells react-redux that the prop counter, accessible within App2 as this.props.counter, should reflect the value of the property counter in the store. (The fact that they have the same name is incidental.)
  • The object mapDispatchToProps just says: within App2, the function-valued prop increment will call the action creator incrementActionCreator and dispatch the resulting action... which will end up calling the reducer.
  • To get these props typed, we have to explicitly declare them by making our component a class that extends React.Component, and declare an interface AppProps. I'm not sure that the type of increment is correct, but it's marginally better than the any type, I would suppose.
  • Arguably the declaration of AppProps should be unnecessarily as it should be able to be inferred; here, React is in a rather similar position to the Vuex helper methods such as mapState. Neither provide automatic typing of mapped store props, I was rather hoping that React might have been better here -- but see this question.
  • Note that we have to destructure the mapped stuff from this.props. This is a little bit dangerous because if you used the same name for the mapped prop as the action creator (which you've probably imported into your scope from Some other module), you're shadowing that with the mapped-prop version, meaning that if you FORGET to destructure the props and just end up calling the parent-scope version then you'll get no behaviour and no errors.