React + Typescript + RxJS by@scott.lively

React + Typescript + RxJS

image
Scott Lively Hacker Noon profile picture

Scott Lively

Engineer Lead & Product Owner

Lately I’ve been trying out React, Typescript, and RxJS and so far I really like the result. The basic premise was to have a single observable library and connect that to React to update components. Then push all state and business logic to the observables. This comes from spending a lot of time with Redux (with added middleware), Recompose, and Reselect, then being frustrated in the large amount of libraries and boilerplate required to simply connect components to an observable and calculate derived state. I had the pleasure years ago to work with a very smart dev (shout out to Isaiah) on an Angular 1 project that used a simple home grown observable library for state management and everything worked remarkably well without using tons of libraries. This example is meant to recreate that concept with more modern tools. It is built around a todos list that displays the list of todos fetched from a server and allows users to add new todos to the list.

Disclaimer: This approach breaks the Redux principle of keeping all state in a single store. If you are a strong advocate of that approach, then you won’t like this, and that’s cool. I personally have had good success and find it easier to structure a project with multiple state stores that interact with each other.

To integrate Typescript, React, and RxJS for a simple todos list container I have created 4 files.

TodosListComponent.ts

This file exports the ‘dumb’ component which just render the props as JSX.

import * as React from 'react';
import {TodosListComponentProps} from './TodosListComponentProps';
import {FormEventHandler, KeyboardEventHandler} from 'react';
import {TodoEntity} from 'service-entities/todos';
import {Key} from 'ts-keycode-enum';

interface TodosListState {
todoText: string;
}

export class TodosListComponent extends React.PureComponent<TodosListComponentProps, TodosListState> {
 state = {
todoText: ''
};

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

return (
<div>
<button onClick={this.addTodo}>add</button>
<input type='text'
value={this.state.todoText}
onChange={this.updateTodoText}
onKeyPress={this.handleEnter}/>
<ol>
{
todos.map((todo: TodoEntity) =>
<li key={todo.id}>{todo.text}</li>
)
}
</ol>
</div>
);
}

componentDidMount() {
this.props.refresh();
}

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

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

handleEnter: KeyboardEventHandler<HTMLInputElement> = e => {
if (e.which === Key.Enter) {
this.addTodo();
}
}
}

TodosListComponentProps.ts

This file exports the interface that defines the props for the TodosListComponent, which RxJS will need to fulfill to create the TodosList container.

import {TodoEntity, CreateTodoEntity} from 'service-entities/todos';

export interface TodosListComponentProps {
todos: TodoEntity[];
refresh: () => void;
addTodo: (todo: CreateTodoEntity) => void;
}

TodosListStore.ts

This is our store that will implement all the properties and methods defined by the TodosListComponentProps. To store the list of todos from the server we use a BehaviorSubject from RxJS. When storing application state a BehaviorSubject gives us two great features. We can get the current value of the observable stream using the `value` property and any changes to the stream will be multi-cast to all subscribers (like subscribing to redux). Notice we export the class not just for type information, but also so unit testing the store is easy via basic dependency injection. Also, if we need multiple TodosListStore objects we can easily create them by injecting different dependencies.

import {todosService} from 'todos/TodosService';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {CreateTodoEntity, TodoEntity} from 'service-entities/todos';

export class TodosListStore {

constructor(
readonly todos = new BehaviorSubject<TodoEntity[]>([]),
private service = todosService
) {}
 // fetch todos from the server and update the value of todos
readonly refresh = () =>
this.service.find().then((todos: TodoEntity[]) =>
this.todos.next(todos)
);
  // add the todo to the server and prepend it to our todos list
readonly addTodo = (todo: CreateTodoEntity) =>
this.service.create(todo).then(
(addedTodo: TodoEntity) =>
this.todos.next([addedTodo, ...this.todos])
);
}

export const todosListStore = new TodosListStore();

TodosList.ts

This file exports the final container component <TodosList/>. To connect our TodosListStore to the TodosListComponent, we need to do two things.

First, create an observable for all props that the component needs. We do this using a function called combineLatestObj, which is based off the RxJS function combineLatest. The combineLatest operator simply takes a list of observables and when any one of them changes it fires the subscriber with the latest values from all the observables as arguments.

// combineLatest Example
const observer1 = Observable.of('a');
const observer2 = Observable.of(1);
combineLatest(
observer1,
observer2,
(value1, value2) => value1 + value2
)
.subscribe(combinedValue => console.log(combinedValue));
observer1.next('b');
observer2.next(2);
// a1
// b1
// b2

Our function combineLatestObj is the same idea, but we want to give it a map of keys to observables and have it resolve the map of keys to latest values of each observable. It has an added feature of passing static values through so we don’t need to create observables for values that don’t change (like functions or constants defined outside the component).

NOTE: this can be composed to create nested objects as well

// combineLatestObj Example
const observer1 = Observable.of('a');
const observer2 = Observable.of(1);
const constantValue = 'no changes';
combineLatest({
value1: observer1,
value2: observer2,
constantValue
})
.subscribe(latestObject => console.log(latestObject));
observer1.next('b');
observer2.next(2);
// { value1: a, value2: 1, constantValue: 'no changes' }
// { value1: b, value2: 1, constantValue: 'no changes' }
// { value1: b, value2: 2, constantValue: 'no changes' }

With combineLatestObj it is easy to create a single observable that satisfies the TodosListComponentProps with the todosListStore and is strongly typed:

combineLatestObj<TodosListComponentProps>(todosListStore);

Second we need to connect our new observable to the TodosListComponent to create a container, just like you would with the react-redux connect higher order component. To do this we have a similar higher order component called withObservable that will subscribe to the given observable and any time there is an update, the value of the observable (and nothing else) will be passed to the given component. The function is used like any other higher order component and provides strong typing to ensure the observable and component both satisfy the given interface:

withObservable<TodosListComponentProps>(TodosListObservable)(TodosListComponent);

Now exporting our container is as simple as exporting the result of these two steps:

import {TodosListComponent} from 'todos/TodosList/TodosListComponent';
import {TodosListComponentProps} from 'todos/TodosList/TodosListComponentProps';
import {todosListStore} from 'todos/TodosList/TodosListStore';
import {withObservable} from 'lib/withObservable';
import combineLatestObj from 'lib/combineLatestObj';

const TodosListObservable = combineLatestObj<TodosListComponentProps>(todosListStore);

export const TodosList = withObservable<TodosListComponentProps>(TodosListObservable)(TodosListComponent);

I like this approach not only because I’ve used a similar pattern in the past with great results, but by using vanilla classes, observable streams and a single higher order component we’ve replaced redux, redux-thunk, reselect, and recompose. This post is based off a more involved example that demonstrates the interaction of two stores by adding the ability to filter the todo list.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.