reactjs-util v1.0.2
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-utilModules
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.
SideBarwith no app logic - controllers, which have no styles, but do have logic, e.g.
SideBarControllerand only return one thing: their namesake equivalent presentational component, e.g.SideBar
- dumb (props-only) presentational components, e.g.
- 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:
useRxStateunwrapsmyNumber$-->number(i.e.Observable<number>--->number),useAtomgoes the other way and wrapsmyNumber-->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.