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:
MyActionTypesis a sum type with only one member.- Each action has a mandatory property,
type. The use oftypeofin the definition ofIncrementActionensures thattypereceives a string literal type. This in turn enables the switch below to be type safe. - The structure of the switch will make sure that
actionis inferred to the correct type within itscasebranch. That is, even thoughMyActionTypesmay beFoo | Bar, within its matching case branch TS knows thatactionisFooActionorBarAction. - 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 | undefinedNOT simplyMyState. You react to the undefined state by configuring the initial state. You're going to get confusing type errors when you try to callcreateStoreif 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
connectfunction 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
typeproperty. - The argument to
mapStateToPropstellsreact-reduxthat the propcounter, accessible withinApp2asthis.props.counter, should reflect the value of the propertycounterin the store. (The fact that they have the same name is incidental.) - The object
mapDispatchToPropsjust says: withinApp2, the function-valued propincrementwill call the action creatorincrementActionCreatorand 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 ofincrementis correct, but it's marginally better than theanytype, I would suppose. - Arguably the declaration of
AppPropsshould 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.