Image is from Pixabay user kordi_vahle
React functional component and hooks pose a slight different model of operation then the standard component lifecycle model.
In order to use RxJS effectively in React functional components with hooks, we need to adjust some of the pattern we use in regular components to the new paradigm.
This is part 1 of the series. It deals with RxJS patterns for React Class components.
Part 2 will handle React functional components patterns and what is the logic behind them.
RxJS in Regular Class Components
Component Lifecycles
In regular components frameworks, there is an on init
lifecycle that runs once, on change
lifecycle that runs on each component's input change, and on destroy
lifecycle that get called when the component is removed and discarded.
The analog to those lifecycles in React are:
-
componentDidMount for
onInit
functionality, -
componentDidUpdate for
onChange
functionality, and -
componentWillUnmount for
onDestroy
functionality.
class Calculator {
constructor(props) {
// ...
}
componentDidMount() {
// ...
}
componentDidUpdate(prevProps, prevState, snapshot) {
// ...
}
componentWillUnmount() {
// ...
}
}
Using RxJS with Class Component Lifecycles
Initialization
The componentDidMount
callback and the constructor
allow to create the main streams that will guide the component logic and bring them to life - to subscribe to them.
class Calculator {
constructor(props) {
super(props);
/*
* Initialization of callbacks fucntions
* to send stream events
*/
this.onNumber = (num) => this.numAction$.next(num);
this.onMathOperation = (operation) => this.mathOperation$.next(operation);
/*
* Initialize reflected state of the main streams values.
* To be used in the view layer.
*/
this.state = {
result: "",
expr: []
};
}
componentDidMount() {
/*
* Setting up the event Rx subjects. Will be used in
* the main streams.
*/
this.onUnmount$ = new Subject();
this.numAction$ = new Subject();
this.mathOperation$ = new Subject();
/*
* Setting up the main logic streams.
*/
this.mathExpression$ = this.streamMathExpression();
this.result$ = this.streamResult();
/*
* 1. Reflecting the main streams values into
* regular state to be used in the view.
* 2. Subscribing to the main streams to bring
* them to life for the duration of the
* component.
*/
merge(
this.result$.pipe(
tap((result) => this.setState((state) => ({ ...state, result })))
),
this.mathExpression$.pipe(
tap((expr) => this.setState((state) => ({ ...state, expr })))
)
)
.pipe(takeUntil(this.onUnmount$))
.subscribe();
}
/*
* Main stream expression
*/
streamMathExpression() {
return pipe(
() =>
merge(
this.numAction$.pipe(
map((num) => ({ action: "number", value: num }))
),
this.mathOperation$.pipe(
map((op) => ({ action: "operation", value: op }))
)
),
scan((expr, { action, value }) => {
// reducer logic for the math expression
}, [])
)(null);
}
}
Here, in the init
phase of the component, several things happening:
- Main observables values reflection into React component state.
- Creating Rx Subjects to be used and notify the main streams of changes and events.
- Creating and wiring up the main streams that handle the component behavior and state management.
Using Values in the View
This is needed for using the observable values in the view jsx
layer. In Angular for instance, this is done with the async pipe that subscribe and provide the observable value automatically in Angular HTML template expressions.
Initialization of React component state in the constructor:
constructor() {
this.state = {
result: "",
expr: []
};
Reflecting and updating the main observables values into the React component states. This is done with subscribing to the observables and updating the React component state.
componentDidMount() {
merge(
this.result$.pipe(
tap((result) => this.setState((state) => ({ ...state, result })))
),
this.mathExpression$.pipe(
tap((expr) => this.setState((state) => ({ ...state, expr })))
)
)
.pipe(takeUntil(this.onUnmount$))
.subscribe();
}
Creating Rx Subjects to be used and notify the main streams of changes and events
The main streams subscribe to the subjects that are used in the view or in other stream to notify of changes or events such as click and timer events.
constructor(props) {
super(props);
this.onNumber = (num) => this.numAction$.next(num);
this.onMathOperation = (operation) => this.mathOperation$.next(operation);
}
componentDidMount() {
this.onUnmount$ = new Subject();
this.numAction$ = new Subject();
this.mathOperation$ = new Subject();
}
Main Streams Logic
The main streams of the component. Those needs to be created and brought to life - subscribed to - only once and live for the rest of the component life until it destroyed.
componentDidMount() {
this.mathExpression$ = this.streamMathExpression();
this.result$ = this.streamResult();
merge(
this.result$.pipe(
// ...
),
this.mathExpression$.pipe(
// ...
)
)
.pipe(takeUntil(this.onUnmount$))
.subscribe();
}
Input Change Detection
The onChange
callback allows to push detected input changes into RxJS subjects. The main streams of the component then can listen on those subject and react to component input changes.
Gathering the changes events:
componentDidMount() {
this.propsChanges$ = new Subject();
}
componentDidUpdate(prevProps) {
this.propsChanges$.next({ prev: prevProps, current: this.props });
}
Listening and notifying relevant prop changes:
componentDidMount() {
this.inputMode$ = handlePropChange("mode", this.propsChanges$);
merge(
this.inputMode$,
)
.pipe(takeUntil(this.onUnmount$))
.subscribe();
}
function handlePropChange(propName, propsChanges$) {
return propsChanges$.pipe(
filter(({ prev, current }) => prev[propName] !== current[propName]),
map(({ current }) => current[propName]),
shareReplay({ bufferSize: 1, refCount: true })
);
}
Cleanup
The componentWillUnmount callback allows to cleanup subscriptions of the main logic streams so no dangling subscriptions will be left after the component is destroyed.
This is done through another subject - onUnmount
- that all other main streams subscription depends on so when it emit and complete, the subscriptions of the main streams complete as well.
The takeUntil operator subscribe to the unMount$
subject and complete the main subscriptions when unMount
complete.
componentDidMount() {
this.onUnmount$ = new Subject();
this.mathExpression$ = this.streamMathExpression();
this.result$ = this.streamResult();
merge(
this.result$.pipe(
// ...
),
this.mathExpression$.pipe(
// ...
)
)
.pipe(takeUntil(this.onUnmount$))
.subscribe();
}
componentWillUnmount() {
this.onUnmount$.next();
this.onUnmount$.complete();
}
Full example code can be found on github;
Conclusion
This part dealt with RxJS patterns for React class components. Specifically we saw:
- How to create main streams for the duration of the component life.
- Use the streams values in the view layer.
- Listen on component input props changes.
- Cleanup subscriptions when the component is destroyed.
Next part will show how to update those patterns to React functional components with hooks and the logic behind those patterns.
Top comments (0)