Writing Redux-Aware, Functional React Components in Typescript
In the past few years, ReactJS has begun to dominate the landscape of client-side web frameworks, in part for its blazingly fast performance and ease of use. Redux is a helpful tool to deploy alongside React, as it allows web applications to manage state more effectively via a single set of logic (called "Reducers").
There are a whole host of posts on the web outlining just what makes these features so powerful, but in this post, I'd like to outline a single design pattern useful for creating state-aware ReactJS functional components in Typescript.
As an intro to the Javascript patterns we will explore here, I recommend you first see the Redux documentation on integrating with React here.
Without further ado, let's take a look at an example of a component in Javascript.
The Javascript Pattern
As a reminder, [Typescript]() builds on Javascript with typings, so it's important to understand what we're building on in this case.
When writing Redux-aware React components, we implement a function called mapStateToProps
which takes an existing redux state and maps it to the properties of our component. You can see this example in my annotated version of a todo list app from the Redux docs:
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
onTodoClick: PropTypes.func.isRequired
}
const TodoListInternal = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
))}
</ul>
)
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const TodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)
export default VisibleTodoList
A few things worth noting are happening in the above. At the very top, we declare an initial property definition. However, our actual definitions for those properties are not declared in the initial declaration of that component. For example, you would not do the following:
// not needed -- props set by Redux
<TodoList todos={[{...}]} onTodoClick={() => {...}} />
This is because Redux is actually handling the initialization of some of these properties. However, what if we also had properties that were not set by Redux? What if, for example, we had a combination of three things:
- Props from the component itself.
- Props set by Redux
- A dispatch method from Redux.
In Javascript, we might do something like the following:
TodoList.propTypes = {
color: PropTypes.string.isRequired, // designed to be passed to the component itself
todos: PropTypes.arrayOf( // set by Redux
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
}
const TodoListInternal = (props) => (
<ul>
{props.todos.map((todo, index) => (
<Todo key={todo.id} {...todo} onClick={() =>
// dispatch is on our props thanks to the connect function
props.dispatch(toggleTodo(todo.id));
/>
))}
</ul>
)
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const TodoList = connect(mapStateToProps)(TodoList)
export default TodoList
Unlike the case above where all of our props were being set by React, we now have three different prop sources for properties to be passed directly on the component, props set by Redux, and the dispatch
method added to our props from Redux.
This means we could do something like the following:
<TodoList color="#fff" />
The remainder of the props would be set for us by Redux.
In typescript, however, this code becomes even clearer.
The Typescript Pattern
In typescript, the distinctions between the different types of props used above are far more apparent. This is thanks to Typescript's (you guessed it) typing system!
This allows us to draw the distinction between what exists from Redux, our component itself, and the dispatch method more directly for better props readability.
To start, let's just compare our props. In Javascript, they looked like this:
There are a few shortcomings here:
- We rely on special React code to support typechecking (i.e.
PropTypes.string
). Using this requires knowledge of the semantics of the tool. - We have no way to distinguish between props set by Redux and props designed to be passed in manually.
- There's no clear signal that these props should contain the dispatch method on them unless we're familiar with the semantics of Redux's
connect
function.
Typescript clears up all three of these shortcomings with a much simpler prop definition:
export namespace TodoList {
interface OwnProps {
color: string;
}
interface StoreProps {
todos: ITodo[];
}
interface ITodo {
id: number;
completed: boolean;
text: string;
}
export type Props = OwnProps & DispatchProp & StoreProps;
}
The above goes a long way to simplifying the compressed code, and it makes it a lot clearer that the props that we use are actually a combination of three different props. It also means our use of the props is now linked with what we've declared them to be.
Putting it all together, we end up with the following:
export namespace TodoList {
interface OwnProps {
color: string;
}
interface StoreProps {
todos: ITodo[];
}
interface ITodo {
id: number;
completed: boolean;
text: string;
}
export type Props = OwnProps & DispatchProp & StoreProps;
}
const TodoListInternal: FunctionComponent<TodoList.Props> = (props) => (
<ul>
{props.todos.map((todo, index) => (
<Todo key={todo.id} {...todo} onClick={() =>
// dispatch is on our props thanks to the connect function
props.dispatch(toggleTodo(todo.id));
/>
))}
</ul>
)
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
export const TodoList = connect(mapStateToProps)(TodoList)
Just like that, we have a more clear definition of what props actually live on our component.