Project WebApp — Taking control over state-changes
As already said in last session, besides handling the state itself it is very important to take care of the state changes. An extremely powerful tool for this is RXJS. It works with the concept of observables, which are sources of data you can subscribe on.
The rxjs lib provides many defaults to create useful observables for example the interval function creates an observable triggered every time the given amount of milliseconds passes.
import { interval } from "rxjs";
// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000);
// Subscribe to begin publishing values
secondsCounter.subscribe((n) =>
console.log(
`It's been ${n} seconds since subscribing!`
)
);
Important to know here is, that the observable is only active as long as there is an active subscription. So you can declare the complete dataflow and trigger it by subscribing. This is also meaningful for ajax requests, the request is only triggered when there is someone who listens for the result.
import { ajax } from "rxjs/ajax";
const requestLuke = ajax(
"https://swapi.co/api/people/1"
);
// nothing happens here
import { ajax } from "rxjs/ajax";
const requestLuke = ajax(
"https://swapi.co/api/people/1"
);
const sub = requestLuke.subscribe((res) => {
console.log(res.status, res.response);
});
// now the actual api call is fired
The real power of rxjs lies in its operators which
can transform the data coming from an observable
and build a new observable providing the
transformed data. The most important one is the
map-operator, which returns a new value for each
value passing through. In general, it can be seen
as a stream of information passing through a pipe
with all sorts of valves/filters/transformers...
and so the function to work with the operators has
the fitting name pipe
. A simple map
example is
to transform the ajax-response object to the
actual response object.
import { ajax } from "rxjs/ajax";
import { map } from "rxjs/operators";
const requestLuke = ajax(
"https://swapi.co/api/people/1"
).pipe(map((res) => res.response));
const sub = requestLuke.subscribe((response) => {
console.log(response);
});
And to catch possible errors (handling with
external services can always cause errors) use the
catchError
operator.
import { ajax } from "rxjs/ajax";
import { catchError, map } from "rxjs/operators";
import { empty } from "rxjs";
const requestLuke = ajax(
"https://swapi.co/api/people/1"
).pipe(
catchError((e) => {
console.error(e);
// simple fallback of returning an empty observable
// (swallows error -> bad error catching)
return empty();
}),
map((res) => res.response)
);
const sub = requestLuke.subscribe((response) => {
console.log(response);
});
A list of all operators and observable factories can be found on the official docs site reactivex.io/rxjs.
Besides the given Observable factories there are
also "Subjects", a subject in the rxjs context is
an observable, with the addition of the .next()
method. By calling next with a value you can push
this value in the observable pipeline. But
remember, the provided values are only digested
when there is a subscriber. All calls to next
before there is an active subscription are simply
forgotten.
import { Subject } from "rxjs";
const eventsProvider = new Subject();
eventsProvider.next("first event"); // this is forgotten
const sub = eventsProvider.subscribe(
(response) => {
console.log(response);
}
);
eventsProvider.next("second event"); // gets logged to console
For the chat app from the last session this means you can declare all changes to the state with observables and user inputs with subjects.
import { Subject, from } from "rxjs";
import {
Subject,
merge,
scan,
} from "rxjs/operators";
const messageInputSubject = new Subject();
const initialMessages = from(messagesJson);
const messageStream = messageInputSubject.pipe(
merge(messagesJson)
);
// observable providing an array of all messages
const messageState = messageStream.pipe(
// scan acts like reduce, but over time.
// it always provides the last calculated value
scan(
(state, message) => {
return state.concat(message);
},
// empty array as initial value
[]
)
);
To connect this state with the react world a
subscription on the relevant data is needed in one
component which transfers the rxjs-state into the
react-world. When building a subscription it is
important to unsubscribe, when it's not used
anymore to prevent memory-leaks. You can either
use a direct .unsubscribe()
call on the
subscription object, or use the
takeUntil(someObservable)
operator. The
subscribe
call should be in the
componentDidMount
react lifecycle method, and
end of the subscription should be in the
componentWillUnmount
lifecycle method. More
information about the react-component lifecycle
can be found
here.
export class App extends React.Component {
// initial state
state = {
/* ✂️ */
};
// subject which emits on componentWillUnmount
unMounted = new Subject();
// react lifecycle method called when this
// component got mounted into the DOM
componentDidMount() {
stateObservable
.pipe(
/* other transforamtions ✂️ */
takeUntil(this.unMounted)
)
.subscribe((stateUpdate) => {
this.setState(stateUpdate);
});
}
// react lifecycle method called when this
// component will be removed from the DOM
componentWillUnmount() {
this.unMounted.next();
}
render() {
/* ✂️ */
}
}
Tasks
- clone the chat-app-2 repository
- install the rxjs dependency
npm install rxjs
- create a folder "state" in the project to separate state-handling code
- create the message- and user-observables and subjects needed to manage all data hold in the state
- connect the app with the newly create state-logic
- extend the state with an observable to calculate the message count
- (bonus) add second input field to select a user and filter the messages to show only messages from this user