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 oftypeof
in the definition ofIncrementAction
ensures thattype
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 itscase
branch. That is, even thoughMyActionTypes
may beFoo | Bar
, within its matching case branch TS knows thataction
isFooAction
orBarAction
. - 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 simplyMyState
. You react to the undefined state by configuring the initial state. You're going to get confusing type errors when you try to callcreateStore
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 inreact-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
tellsreact-redux
that the propcounter
, accessible withinApp2
asthis.props.counter
, should reflect the value of the propertycounter
in the store. (The fact that they have the same name is incidental.) - The object
mapDispatchToProps
just says: withinApp2
, the function-valued propincrement
will call the action creatorincrementActionCreator
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 interfaceAppProps
. I'm not sure that the type ofincrement
is correct, but it's marginally better than theany
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 asmapState
. 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.