Oxymora
Making React components 100% pure.
oxymoron ŏk″sē-môr′ŏn″
noun A rhetorical figure in which incongruous or contradictory terms are combined, as in a 'deafening silence' and a 'mournful optimist'.
Oxymora is the plural of oxymoron, and this library has that name since it has functions named pureStatefulComponent
and usePureStatefulCallback
, yet "pure stateful" is an oxymoron.
Why Use Oxymora?
Oxymora components are:
- Quick to write:
- Easy to reason about / debug.
- Simple to test.
- Always composable.
- Quicker to feedback on broken functionality.
- Robust.
The Anatomy of an Oxymora Component
Oxymora components start with a TypeScript state-spec; this defines the component's state, input-props and output-props. Here's an example of a state-spec for a Counter
component:
type CounterStateSpec = {
State: number;
InputProps: {
incrementBy?: number;
};
OutputProps: {
onCounterChange: number;
};
};
The props for this component are defined like this:
type CounterProps = Props<CounterStateSpec>;
This is equivalent to manually writing the following:
// NOTE: you don't need to write this because this is what Props<CounterStateSpec> gives you:
type CounterProps = {
// `InputProps`:
incrementBy?: number;
// `OutputProps`:
onCounterChange?: (number) => void;
// `State`:
state: number;
onStateChange?: (number) => void;
};
Although the State
related props are part of the pure-stateful component, they will be removed from the stateful component that's subsequently created using makeStateful
. Having the pure-stateful component available to us is helpful for testing however, plus it can also be useful when composition isn't possible using the stateful component.
Here's how you might implement PureStatefulCounter
:
export const PureStatefulCounter = pureStatefulComponent<CounterStateSpec>(
1, // initial state
({ state }) => {
const onIncrementCounter = usePureStatefulCallback<CounterStateSpec>(
({ state, incrementBy = 1 }) => {
const newState = state + incrementBy;
return {
state: newState,
onCounterChange: newState,
};
}
);
return (
<Button
colorScheme="orange"
leftIcon={<GrFormAdd />}
onClick={onIncrementCounter}
>
{state}
</Button>
);
}
);
Notice the declarative nature of the onIncrementCounter
event handler; it signals both a state update and a callback declaratively. Event handlers act on behalf of the closest pure-stateful ancestor component, so this event handler would continue to work even if it was moved into a child component (no callback prop-drilling required).
Finally, the stateful version of the component (i.e. not having state
and onStateChange
props) can be created like this:
export const StatefulCounter =
makeStateful<CounterStateSpec>(PureStatefulCounter);
Try a Demo!
We have a live StackBlitz development environment that includes the simple Counter
example above, but also an Oxymora implementation of TodoMVC. Use this to play with the components, inspect the code, and test code changes live.