Typescript + React + Recompose + Reselect

I am currently using recompose and reselect a lot with react and I wanted to explore what it would be like to add typescript into the mix. Overall my impression has been fantastic. It allows strong typing of the props object as it is manipulated by recompose, ensures the final object is exactly what the container requires, and gives great IDE support. As an example I’ll use a simple todo list that fetches data from the server, adds new todos, and has an input to filter the todos list.

So we have a TodosList that takes a few props, and a sub-component TodosSearch that takes its own props that get passed through.

interface State {
todoText: string;
}

export class TodosList extends PureComponent<TodosListProps,State> {
state = {
todoText: ''
};

render() {
const {todos, search} = this.props;

return (
<div>
<h2>Todos Recompose</h2>
<button onClick={this.addTodo}>add</button>
<input type='text'
value={this.state.todoText}
onChange={this.updateTodoText}/>
<TodosSearch {...search}/>
<ol className={styles.list}>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ol>
</div>
);
}

updateTodoText: FormEventHandler<HTMLInputElement> = e =>
this.setState({todoText: e.currentTarget.value});

addTodo = () => {
if (this.state.todoText.length) {
this.props.addTodo({text: this.state.todoText});
this.setState({todoText: ''});
}
}
}

We know from the organizing react props post that we want our selectors to reflect the hierarchy of the props below. With typescript we can now use types to ensure those selectors are actually fulfilling the contract set by the component props interface.

interface TodosListProps {
todos: TodoEntity[];
addTodo: (todo: TodoEntity) => void;
search: TodosSearchProps;
}
interface TodosSearchProps {
searchText: string;
setSearchText: (text: string) => void;
}

recompose & reselect

With recompose with build up a props object with all the data we need to create the props for our container (in this case TodosList). So let’s walk through each higher order component we will need to compose to get all the data we need.

First let’s fetch the todos list from the server. In this case our withPromise hoc takes the response of the promise and wraps it in the TodosFindResponse interface.

interface TodosFindResponse {
promiseData?: TodoEntity[];
promiseError?: object;
loaded: boolean;
}
withPromise<TodoEntity[]>(() => TodosService.find())

Next when a new todo is added to the list we will store it in the client after posting to the server so we can display the original fetched todos along with the added ones.

interface AddedTodosState {
addedTodos: TodoEntity[];
addTodoToState: (todo: TodoEntity) => void;
}
withState<AddedTodosState>('addedTodos', 'addTodoToState', [])

We also want to store the search text so we can filter the todos.

interface SearchFilterState {
searchText: string;
setSearchText: (text: string) => void;
}
withState<SearchFilterState>('searchText', 'setSearchText', '')

Now that we have all our state we can create a union type that represents all the data received from our higher order components we are composing.

type TodosListDataProps = TodosFindResponse & 
UrlState &
AddedTodosState;
// we will map from TodosListDataProps -> TodosListProps

Next we have a selector that will create the todos list we want to render. We now have strong typing for each selector so it’s easier to keep track of the inputs along with the IDE support for the props.

const selectFilteredTodos = createSelector<TodosListDataProps, TodoEntity[], string, TodoEntity[], TodoEntity[]>(
props => props.promiseData || [],
props => props.searchText || '',
props => props.addedTodos,
(todos, searchText, addedTodos) => (
(searchText.length > 0)
? todos.filter(todo => todo.text.includes(searchText))
: todos
).concat(addedTodos)
);

We also have a strongly typed selector that will be the props for the TodosSearch component.

// map from TodosListDataProps to TodosSearchProps
const selectSearchProps = (props: TodosListDataProps): TodosSearchProps => ({
searchText: props.searchText,
setSearchText: props.setSearchText
});

Finally we create our last selector that fulfills the TodosListProps.

// map from TodosListDataProps to TodosListProps
const TodosListPage = compose<TodosListProps, {}>(
withPromise<TodoEntity[]>(() => TodosService.find()),
withState<AddedTodosState>('addedTodos', 'addTodoToState', []),
withState<SearchFilterState>('searchText', 'setSearchText', ''),
mapProps<TodosListProps, TodosListDataProps>(props => ({
addTodo: (todo: TodoEntity) => TodosService.create(todo)
.then(addedTodo => props.addTodoToState(addedTodo)),
todos: selectFilteredTodos(props),
search: selectSearchProps(props)
})
)(TodosList);

In the end we have a strongly typed props object that we can use to map to the props interface that our container requires, all with great IDE support and type checking.

Here is the end result:

interface TodosFindResponse {
promiseData?: TodoEntity[];
promiseError?: object;
loaded: boolean;
}
interface AddedTodosState {
addedTodos: TodoEntity[];
addTodoToState: (todo: TodoEntity) => void;
}
interface SearchFilterState {
searchText: string;
setSearchText: (text: string) => void;
}
type TodosListDataProps = TodosFindResponse & 
UrlState &
AddedTodosState;
const selectFilteredTodos = createSelector<TodosListDataProps, TodoEntity[], string, TodoEntity[], TodoEntity[]>(
props => props.promiseData || [],
props => props.searchText || '',
props => props.addedTodos,
(todos, searchText, addedTodos) => (
(searchText.length > 0)
? todos.filter(todo => todo.text.includes(searchText))
: todos
).concat(addedTodos)
);
// map from TodosListDataProps to TodosSearchProps
const selectSearchProps = (props: TodosListDataProps): TodosSearchProps => ({
searchText: props.searchText,
setSearchText: props.setSearchText
});
// map from TodosListDataProps to TodosListProps
const TodosListPage = compose<TodosListProps, {}>(
withPromise<TodoEntity[]>(() => TodosService.find()),
withState<AddedTodosState>('addedTodos', 'addTodoToState', []),
withState<SearchFilterState>('searchText', 'setSearchText', ''),
mapProps<TodosListProps, TodosListDataProps>(props => ({
addTodo: (todo: TodoEntity) => TodosService.create(todo)
.then(addedTodo => props.addTodoToState(addedTodo)),
todos: selectFilteredTodos(props),
search: selectSearchProps(props)
})
)(TodosList);

To run code similar to this I have built an example that syncs the search filter to the url using router5 and rxjs-router5.

Like what you read? Give Scott Lively a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.