0
votes

I am trying to use redux state store in a redux app using a rxjs obserable wrapper. Source tutorial

I first tried this approach when I switched from ngrx to redux in angular. Now I’m using this pattern in a react app. However, I have a bit of an issue. When I subscribe to some state store stream I use setState(foo) to store the value in the component. This in turn triggers a new render cycle. I’d like to have one render cycle per component instead of 2 or more. 

I’ve been trying to inhibit the initial rendering of the component in order to have it triggered first and only once by the state store subscription. When you have multiple nested components and multiple subscriptions they tend to create wasteful renderings just to do the app init. I know that React does a great job of optimising for multiple renderings but still I find that keeping an eye on the rendering cycles is healthy for avoiding subtle bugs.

Any recommendation on how to trigger the first rendering from the state store subscription?

app.module.tsx

private subscribeToAppSettings() {
    DEBUG.cmp && debug('Subscribe appSettings$');

    appSettings$().pipe(
        skip(1), // For REST api calls I skip the initial state
        takeUntil(this.destroyed$),
    )
        .subscribe(settings => {
            DEBUG.subscribe && debug('==> Observe appSettings$', [settings]);
            this.setState({ settings });
        });
}

As you can see AppModule and everything else is rendered twice because of this subscription. This is a filtered set of logs, showcasing when the app is running the render() methods. Just the init stage, no user interactions.

enter image description here

1
why are you calling setState? - azium
I believe I need to trigger a rendering cycle when the rest api returns. Since I don't use the redux connect() method I need a way to push this data in the rendering pipeline. If I just store it in some props on the class instance than nothing will change on the screen. Right? Basically, I'd like to get rid of the first rendering and keep the second one which uses state store data. - Adrian Moisa
Oh I see. What's the problem exactly? Why does it matter if render gets run one extra time? - azium
Somewhere down below in a nested component, I need to use componentDidUpdate() to process route params and query string all together in one shot. If I have multiple renderings, then this step needs additional logic to filter out excessive renderings. I am forced to use this life cycle method because if I use this.props.history.listen() then this.props.match.params.foo is lagging with 1 state behind. That's because the component has not updated the props yet. - Adrian Moisa
what about adding a renderedOnce set to false initially, together with a shouldComponentUpdate check? - azium

1 Answers

0
votes

After reviewing the entire architecture again I figured that I need to manually set the initial state in the components. Now, the initial rendering is doing the useful work, and the second rendering will be ignored by the react change detection.

I still have the extra rendering cycles. However, I see that this is the state of affairs with change detection. A lot of things trigger a second rendering: the init, the router, the event handlers, the observables. As long as React is using the virtual dom for change detection to weed out values that do not actually change, there should be no real impact on performance. As they say: I'm barking at the wrong tree.

state.service.tsx

/** Access state changes as an observable stream */
export const store$ = new Observable<AppState>(observer => {

    // All state store observable use `distinctUntilChanged()` operator.
    // Without this initial state, `distinctUntilChanged()` will be unable to compare previous and current state.
    // As a result, the webapi observable will miss the first response fron the server.
    observer.next(appInitialState);

    let appState: AppState;
    store.subscribe( () => {
        appState = store.getState();
        observer.next(appState);
    });

})

app.module.tsx

constructor(props: any) {
    super(props);
    DEBUG.construct && debug('Construct AppModule');

    this.state = {
        navigatorIsVisible: appInitialState.navigator.isVisible,
        searchOverlayIsVisible: appInitialState.search.isVisible
    } as State;

    getAppSettings();
}

search.overlay.smart.tsx

searchOverlayIsVisible$().pipe(
    takeUntil(this.destroyed$),
    skip(1), // Ignore init state
)
    .subscribe(searchOverlayIsVisible => {
        DEBUG.subscribe && debug('Observe searchOverlayVisiblity$', searchOverlayIsVisible);
        this.setState({ searchOverlayIsVisible });
        this.state.searchOverlayIsVisible
    });

search.overlay.service.tsx

export function toggleSearchOverlay(isVisible?: boolean) {
    if (DEBUG.service && DEBUG.verbose) debug('Toggle search overlay', isVisible);

    store.dispatch(
        searchActions.toggleSearch(isVisible)
    );

    return searchOverlayIsVisible$();
}

export const searchOverlayIsVisible$ = () => store$.pipe(
    map( state => SEARCH_VISIBILITY(state) ),
    distinctUntilChanged()
);

Conclusions

  • Pushing the initial state in the store$ observable is necessary because we need all the state store observables to recieve their first state. Without this initial state distinctUntilChanged() will not be able to run the comparison between previous and current state. If distictUntilChanged is blocking the obsevables then we end up blocking responses from the webapi. This means we see empty pages even if the state store received the first set of data.
  • Notice that we are using the component constructor to setup the initial state. Thus, we use the first rendering cycle for useful work. The second rendering will be inhibited by using skip(1) in all state store observables.
  • Even if we setup init state in constructor we still keep the initial state in reducers as well. All the TOGGLE actions need an initial state to start from.
  • Be aware that, a lot of processes trigger a second rendering: the init, the router, the event handlers, the observables. As long as React is using the virtual dom for change detection to weed out values that do not actually change, there should be no real impact on DOM rendering performance.
  • This means it is close to impossible to have just one componentDidUpdate call per route change in LessonsPage. This means we still need to filter out duplicate calls to handlRouteParams().