@forminator/react-wire v0.6.0
@forminator/react-wire
connect react components with wire
Motivation
- top-level components
- doesn't care about the value
- care about the connection between components
- detailed components
- only care about the value
- doesn't care about where the value came from or will be used
- controlled/uncontrolled components should be easy to write
- choosing the correct state location shouldn't be too hard
Install
yarn add @forminator/react-wire
Add proxy-polyfill to support ie browser. proxy support is more than 90% in browsers, more detail
Usage
With pure react, you use useState
to store state:
function FancyInput(props) {
const [value, setValue] = useState('');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
function FancyPage() {
return <FancyInput />;
}
Let's add a new feature, a filterable list that gets a string as a filter and only shows items containing that string. Now we need to read the state of input outside of the FancyInput
component; so we have to move the state to the parent component:
function FancyInput(props) {
- const [value, setValue] = useState('');
+ const { value, setValue } = props;
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
function FancyList(props) {
const [filter, setFilter] = useState('');
return (
<>
<FancyInput value={filter} setValue={setFilter} />
<List items={props.items} filter={filter} />
</>
);
}
- you need to refactor all the
FancyInput
usages, passvalue
, andsetValue
to make it work as expected - the top-level component (
FancyList
) re-renders each time thefilter
changes
Let see how react-wire helps you:
Same as above, but use useWireState
instead of useState
:
function FancyInput(props) {
const [value, setValue] = useWireState(null, '');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
Right now FancyInput
is uncontrolled. If you need to make it controllable, you should pass an up-link wire.
function FancyInput(props) {
- const [value, setValue] = useWireState(null, '');
+ const [value, setValue] = useWireState(props.value$, '');
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
And create a wire with the useWire
hook and pass it to components:
function FancyList(props) {
const filter$ = useWire(null, '');
return (
<>
<FancyInput value$={filter$} />
<List items={props.items} filter$={filter$} />
</>
);
}
With this new code:
- all the
FancyInput
uncontrolled usages are working as expected without any change - the Top-level component (
FancyList
) will not re-render each time the value changes - only detailed components (
FancyInput
,List
) will re-render each time the value changes FancyInput
is now controllable and can be used as a controlled or uncontrolled component by passing a wireFancyList
can be controllable if you want:
function FancyList(props) {
const filter$ = useWire(props.filter$, '');
/* ... */
}
react-wire has more advanced features:
- selectors to maintain calculated values
- fns to pass function calls over the wire
API
useWire
hook
useWire
hook creates and returns new wire. in the next renders, returns the same wire.
// without up-link wire and initial value
const wire = useWire<type>(null);
// with initial value
const wire = useWire(null, initialValue);
// with up-link wire
const wire = useWire(anotherWire);
// with up-link and initial value
const wire = useWire(anotherWire, initialValue);
- If up-link wire has value, initial value will be ignored
- If up-link wire has
undefined
value, initial value will be set on up-link wire
// Lazy initial value
const wire = useWire(anotherWire, () => initialValue);
useWireValue
hook
useWireValue
hook returns current wire value and subscribes for value updates
const value = useWireValue(wire);
// returns `defaultValue` if the wire is `null` or `undefined` or wire has an `undefined` value
const value = useWireValue(wire, defaultValue);
useWireState
hook
useWireState
behaves like react useState
, and keeps state and wire value in sync
// exactly same as useState
const [state, setState] = useWireState(null, initialValue);
// without initial value
const [state, setState] = useWireState(wire);
// with wire and initial value
const [state, setState] = useWireState(wire, initialValue);
- If the wire has value, the initial value will be ignored and respect wire value
- If the wire has
undefined
value, the initial value will be used and wire value will be updated
// Lazy initial value
const [state, setState] = useWireState(wire, () => initialValue);
// new value
setState(value);
// functional update
setState((value) => update(value));
note: If current state is undefined
, updater function won't be called! you can pass initialValue
to useWireState
or make sure the wire has value to avoid this behavior.
get/set wire value
// get value
const value = wire.getValue();
// set value
wire.setValue(someValue);
fns
object and useFn
hook
With fns
object and useFn
hook, you can transfer function calls over wires. Callback function should be memoized with useCallback
// subscribe for `sample` function call
useFn(
wire,
'sample',
useCallback((value) => {
console.log(value);
}, []),
);
// call `sample` function
wire.fns.sample(5);
you can define typing for fns
and useFn
.
type Value = number;
interface Fns {
sample: (n: number) => void;
}
const wire = useWire<Value, Fns>(null);
// code
wire.fns.sample();
error:
error TS2554: Expected 1 arguments, but got 0.
wire.fns.sample();
~~~~~~~~
sample: (n: number) => void;
~~~~~~~~~
An argument for 'n' was not provided.
// code
useFn(
wire,
'sample',
useCallback((n: string) => {}, []),
);
error:
error TS2345: Argument of type '(n: string) => void' is not assignable to parameter of type '(n: number) => void'.
Types of parameters 'n' and 'n' are incompatible.
Type 'number' is not assignable to type 'string'.
useFn(wire, 'sample', useCallback((n: string) => {}, []));
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
up-link wire should have an exact type for each function, but can have more functions.
interface AB {
a: (n: number) => void;
b: (n: string) => void;
}
interface A {
a: (n: number) => void;
}
interface B {
b: (n: string) => void;
}
interface C {
c: (n: string) => void;
}
const wireAB = useWire<Value, AB>(null);
const wireA = useWire<Value, A>(wireAB);
const wireB = useWire<Value, B>(wireAB);
const wireB = useWire<Value, C>(wireAB); // error
Advanced API
Global wire
If you want have a global wire you can createWire
function.
const themeWire = createWire<'light' | 'dark'>('light');
function SomeComponent() {
const theme = useWireValue(themeWire);
}
useSelector
hook
useSelector
creates and returns a new selector. a selector is a wire with the calculated value.
const wire = useWire(null, 4);
const selector = useSelector({
get: ({ get }) => get(wire) * 2,
});
selector.getValue(); // 8
You can create a writable selector with passing the set
option.
const wire = useWire(null, 4);
const selector = useSelector({
get: ({ get }) => get(wire) * 2,
set: ({ set }, value) => set(wire, value / 2),
});
selector.setValue(6);
wire.getValue(); // 3
You can define a dependencies list for dependencies of options.
const { n } = props;
const wire = useWire(null, 4);
const selector = useSelector(
{
get: ({ get }) => get(wire) * n,
set: ({ set }, value) => set(wire, value / n),
},
[n],
);
createSelector
function
createSelector
creates a new selector. It can be used outside of the react.
const themeWire = createWire<'light' | 'dark'>('light');
const textColor = createSelector({
get: ({ get }) => (get(themeWire) === 'light' ? '#000' : '#fff'),
});
function SomeComponent() {
const color = useWireValue(textColor);
}
useSubscribe
hook
Every time the wire value changes, the callback function would be called
// subscribe
useSubscribe(
wire,
useCallback((value) => {
/* ... */
}, []),
);
Subscribe to the wire
Every time the wire value changes, the callback function would be called
// subscribe
const unsubscribe = wire.subscribe((value) => {
/* ... */
});
// unsubscribe
unsubscribe();
useInterceptor
hook
in some rare use-cases you need changing wire behavior and interfering set value. useInterceptor
helps in these use-cases.
useInterceptor
hook gets wire and interceptor function and returns new wire. on every setValue
of returned wire, the interceptor function gets next value and previous value and returns a new value. the returned value of the interceptor will be set on the wire.
const wire = useInterceptor(anotherWire, (nextValue, preValue) => /* anotherValue */)
example:
const valueWire = useInterceptor(
props.valueWire,
useCallback(
(nextValue, preValue) =>
props.submittingWire.getValue() ? preValue : nextValue,
[props.submittingWire],
),
);
Notes
undefined
value vs null
value
undefined
value means the wire has no value and other wires may set its value during their initialization.null
value means the wire has value and value isnull
and other wires don't override its value during their initialization
Initial value
During initialization, wire value always has more priority.
- If the up-link wire has value, the initial value will be ignored and respect wire value
- If the up-link wire has
undefined
value, the initial value will be used and wire value will be updated
Examples:
const wire1 = useWire(null, 1);
const wire2 = useWire(wire1, 2);
wire1.getValue(); // => 1
wire2.getValue(); // => 1
const wire1 = useWire(null);
const wire2 = useWire(wire1, 2);
wire1.getValue(); // => 2
wire2.getValue(); // => 2
const wire = useWire(null, 1);
const [state] = useWireState(wire, 2);
wire.getValue(); // => 1
state; // => 1
const wire = useWire(null);
const [state] = useWireState(wire, 2);
wire.getValue(); // => 2
state; // => 2
Rewiring
Please avoid changing the wire variable. if wire argument changed, a warning will be shown.
// wrong, avoid.
const wire = useWire(condition ? wire1 : wire2);
const value = useWireValue(condition ? wire1 : wire2);
const [state, setState] = useWireState(condition ? wire1 : wire2);
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago