1.0.2 • Published 2 years ago

reactjs-util v1.0.2

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

JS Utils libs

Fully inspired by Mark E's @react-utils

Helpers for building React apps. Also offers React utilities based on RxJS

Install

// Via NPM
$ npm install --save reactjs-util

// Via Yarn
$ yarn add reactjs-util

Modules

It's often desirable to create self-contained React components called "modules", which from the outside look like a dumb React component, but inside includes a whole sub-tree of interacting components, and can be thought of as a complete mini-app in one component.

An obvious candidate for this is a "page" (e.g. a show page, with its own component tree and state shared between the whole page).

A "module" has the following characteristics:

  • from the outside, it looks just like a dumb presentational component, whose interface is only props (no context)
  • it has its own subtree of components, which are either
    • dumb (props-only) presentational components, e.g. SideBar with no app logic
    • controllers, which have no styles, but do have logic, e.g. SideBarController and only return one thing: their namesake equivalent presentational component, e.g. SideBar
  • it has its own list of stores, shared between all its descendant controllers for maintaining state
  • it has its own list of services (e.g. an api client, a remote control button listener), shared between all its descendant controllers
  • its props can be accessed by its descendant controllers

  • createModule create a module, whose descendants can access its stores, services and props

Hooks

useAtom

Wraps a changing value in a React component into a ReadonlyAtom. The opposite of useRxState Just as:

  • useRxState unwraps myNumber$ --> number (i.e. Observable<number> ---> number),

  • useAtom goes the other way and wraps myNumber --> myNumber$ (i.e. number ---> ReadonlyAtom<number>).

import { useAtom } from "reactjs-util";
const ScoreCard = ({ score }: { score: number }) => {
  const score$ = useAtom(score);
  // score$ will refer to the same ReadonlyAtom that you can use how you wish!
  // For example...
  useSubscribe(score$.pipe(withPrevious), ([newScore, oldScore]) => {
    console.log("Score changed from", oldScore, "to", newScore);
  });
};

useRxState

unwraps a stateful Rx Observable. The opposite of useAtom. Automatically update from one or more Atoms or (synchronously emitting) Rx Observables.

import { useRxState } from "reactjs-util";

const ScoreCard = () => {
  const [name, score] = useRxState([name$, score$]);

  return (
    <div>
      {name} has {score} points!
    </div>
  );
};

You can either pass a single observable

const name = useRxState(name$);

...or a tuple...

const [name, score] = useRxState([name$, score$]);

...or a lookup...

const { name, theScore } = useRxState({ name: name$, theScore: score$ });

...in each case the returned values are correctly typed.

Also, you can pass a function (that returns a single observable / tuple / lookup as above) to avoid creating unnecessary obvservables on every render; the function will only get evaluated once on initialize.

const ScoreCard = () => {
  const name = useRxState(() => user$.map((u) => u.name));
  return <div>My name is {name}</div>;
};

useSubscribe

Subscribe to any Rx Observable, (and clean up by correctly unsubscribing when unmounted).

import { useSubscribe } from "reactjs-util";

const HelloController = () => {
  useSubscribe(click$, (click) => {
    // do something with the click
  });

  // ...
};

The second argument is the same callback you'd use if you were subscribing directly, e.g. if you were doing click$.subscribe((click) => { /* do something */ }).

The hook manages unsubscribing for you, and also behaves as expected regarding enclosed variables (which isn't a given), i.e.

  const someVar = ...;

  useSubscribe(click$, (click) => {
    // do something with the someVar - it'll be the "correct" one, not the one from a previous render
  });

useModule

access stores, services and module props from the containing module. See createModule

createModule

createModule

A "module" is a self-contained React component that has its own lookup of stores and services, and interacts with the outside world only via props.

A module is initialised with stores, services and props, then deeply-nested child controller components can use useModule to access these as needed.

Usage:

src/myModule/stores.ts

export const createStores = () => ({
  timer: new TimerStore(),
  // ...
});

src/myModule/services.ts

export const createServices = () => ({
  exampleService: new ExampleService(),
  // ...
});

src/myModule/index.ts (the module component itself)

Initialize the stores, services and module props in the module root component.

import { createModule } from "reactjs-util";
import { createStores } from "./stores";
import { createServices } from "./services";
import { RootController } from "./components/RootController";

export type Props = {
  onClick: () => void;
  initialTime: number;
  //...
};

export const [MyModule, useModule] = createModule<
  Props,
  typeof createStores,
  typeof createServices
>({
  createStores,
  createServices,
  root: RootController, // RootController is a controller component you've defined yourself
});

The exported component MyModule is usable as you would any React component, with its props as the interface:

<MyModule initialTime={time} onClick={handleClick} />

src/myModule/components/ClockController.ts (some child component)

Then consume the stores / services as needed in deeply-nested child controllers

import { useModule } from "./src/utils/create-module";

export function ClockController() {
  const { stores, services } = useModule();

  // Use them ...

  useEffect(() => {
    services.exampleService.doStuff();
  });

  const currentTime = useRxState(stores.timer.time$);

  return <Clock time={time} />;
}

Using module props

You normally won't need to, but the props passed in to the module component from outside will be available...

... to controllers ...

import { useModule } from "./src/utils/create-module";

export function ClockController() {
  const { moduleProps$ } = useModule();

  function handleClick() {
    moduleProps$.get().onClick(); // gets the "onClick" prop passed in to the module itself and calls it
  }

  // ...
}

... and to createServices and createStores if you need them

import { ReadonlyAtom } from "reactjs-util";
import { Props } from "./src/utils/create-module";

export const createStores = (moduleProps$: ReadonlyAtom<Props>) => ({
  timer: new TimerStore(moduleProps$.get().initialTime),
  // ...
});

Semantic release

Release management is automated using semantic-release.

License

The MIT License