Sunday, September 25, 2022

The Deep State

— by Hasan Karahan, MSc ETH Zurich, @notexeditor

Today, I’d like to present a React mixing that allows you to update a component’s state recursively. It’s primarily meant for software developers who still use class based components and who would like to apply a more sophisticated method instead of the standard setState one, which only allows you to shallow update a component’s state.

The mixing below provides the update method which enables you to set a new state and which at its core facilitates the jQuery.extend function, that can deep merge multiple objects in a single invocation. It’s written in Typescript and offers full type safety while updating a component’s state:

import { DeepPartial } from 'redux';
import { Component } from 'react';
/**
 * returns a generic constructor type
 */
export type Constructor<U = unknown> = {
    new (...args: any[]): U;
}
/**
 * @returns the *inferred* property type from a component's constructor
 */
type PropsOf<Ctor>
    = Ctor extends Constructor<Component<infer P>> ? P : never;
/**
 * @returns the *inferred* state type from a component's constructor
 */
type StateOf<Ctor>
    = Ctor extends Constructor<Component<infer _, infer S>> ? S : never;
/**
 * @returns a mixin with the `update` method to *deep* set the state
 */
export function Updatable<
    TBase extends Constructor<Component<TProps, TState>>,
    TProps = PropsOf<TBase>, TState = StateOf<TBase>
>(
    Base: TBase
) {
    return class extends Base {
        protected update(
            next_state: DeepPartial<TState>,
            callback?: () => Promise<void>
        ) {
            return new Promise<void>((resolve) => {
                const state = $.extend(
                    true, {}, this.state, next_state
                );
                this.setState(state, async () => {
                    if (typeof callback === 'function') {
                        await callback();
                    }
                    resolve();
                });
            });
        }
    };
}

It’s usage is rather simple — by just wrapping the React component with the Updatable mixin you gain access to the update method:

type Props = {}
type State = {
  my:{ deep:{ state:{ x:number; y:number; }}}
}
export class App extends Updatable(
  React.Component<Props, State>
) {
  constructor(props: Props) {
    super(props); this.state = {
      my:{ deep:{ state:{ x:0,y:0 }}}
    };
  }
  set X(x: number) {
    // old y is *not* overwritten but kept:
    this.update(my:{ deep:{ state:{ x }}});
  }
  set Y(y: number) {
    // old x is *not* overwritten but kept:
    this.update(my:{ deep:{ state:{ y }}});
  }
}

As you can see, the X and Y setters are only updating the respective x and y states while not explicitly copying the other one — which is handled by the jQuery.extend function in the update method!

Further, it’s also possible to combine the update method with the async and await promises:

export class App extends Updatable(
  React.Component<Props, State>
) {
  constructor(props: Props) {
    super(props); this.state = {
      my:{ deep:{ state:{ x:0,y:0 }}}
    };
  }
  async setX(x: number) {
    await this.update(my:{ deep:{ state:{ x }}});
  }
  async setY(y: number) {
    await this.update(my:{ deep:{ state:{ y }}});
  }
}

The redux and jQuery dependencies are superficial and software developers who’d rather like to avoid them can replace the DeepPartial type (3 lines of code) and the jQuery.extend function with their own implementations.

No comments:

Post a Comment