Skip to content

Repository files navigation

@forminator/react-wire

NPM versionNPM downloadsBuild Statuscodecov

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:

functionFancyInput(props){const[value,setValue]=useState('');return<inputvalue={value}onChange={(e)=>setValue(e.target.value)}/>;}
functionFancyPage(){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)} />;
}
functionFancyList(props){const[filter,setFilter]=useState('');return(<><FancyInputvalue={filter}setValue={setFilter}/><Listitems={props.items}filter={filter}/></>);}
  • you need to refactor all the FancyInput usages, pass value, and setValue to make it work as expected
  • the top-level component (FancyList) re-renders each time the filter changes

Let see how react-wire helps you:

Same as above, but use useWireState instead of useState:

functionFancyInput(props){const[value,setValue]=useWireState(null,'');return<inputvalue={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:

functionFancyList(props){constfilter$=useWire(null,'');return(<><FancyInputvalue$={filter$}/><Listitems={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 wire
  • FancyList can be controllable if you want:
functionFancyList(props){constfilter$=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 valueconstwire=useWire<type>(null);
// with initial valueconstwire=useWire(null,initialValue);
// with up-link wireconstwire=useWire(anotherWire);
// with up-link and initial valueconstwire=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 valueconstwire=useWire(anotherWire,()=>initialValue);

useWireValue hook

useWireValue hook returns current wire value and subscribes for value updates

constvalue=useWireValue(wire);
// returns `defaultValue` if the wire is `null` or `undefined` or wire has an `undefined` valueconstvalue=useWireValue(wire,defaultValue);

useWireState hook

useWireState behaves like react useState, and keeps state and wire value in sync

// exactly same as useStateconst[state,setState]=useWireState(null,initialValue);
// without initial valueconst[state,setState]=useWireState(wire);
// with wire and initial valueconst[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 valueconst[state,setState]=useWireState(wire,()=>initialValue);
// new valuesetState(value);
// functional updatesetState((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 valueconstvalue=wire.getValue();
// set valuewire.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 calluseFn(wire,'sample',useCallback((value)=>{console.log(value);},[]),);// call `sample` functionwire.fns.sample(5);

you can define typing for fns and useFn.

typeValue=number;interfaceFns{sample: (n: number)=>void;}constwire=useWire<Value,Fns>(null);
// codewire.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.
// codeuseFn(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.

interfaceAB{a: (n: number)=>void;b: (n: string)=>void;}interfaceA{a: (n: number)=>void;}interfaceB{b: (n: string)=>void;}interfaceC{c: (n: string)=>void;}constwireAB=useWire<Value,AB>(null);constwireA=useWire<Value,A>(wireAB);constwireB=useWire<Value,B>(wireAB);constwireB=useWire<Value,C>(wireAB);// error

Advanced API

Global wire

If you want have a global wire you can createWire function.

constthemeWire=createWire<'light'|'dark'>('light');functionSomeComponent(){consttheme=useWireValue(themeWire);}

useSelector hook

useSelector creates and returns a new selector. a selector is a wire with the calculated value.

constwire=useWire(null,4);constselector=useSelector({get: ({ get })=>get(wire)*2,});selector.getValue();// 8

You can create a writable selector with passing the set option.

constwire=useWire(null,4);constselector=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;constwire=useWire(null,4);constselector=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.

constthemeWire=createWire<'light'|'dark'>('light');consttextColor=createSelector({get: ({ get })=>(get(themeWire)==='light' ? '#000' : '#fff'),});functionSomeComponent(){constcolor=useWireValue(textColor);}

useSubscribe hook

Every time the wire value changes, the callback function would be called

// subscribeuseSubscribe(wire,useCallback((value)=>{/* ... */},[]),);

Subscribe to the wire

Every time the wire value changes, the callback function would be called

more detail
// subscribeconstunsubscribe=wire.subscribe((value)=>{/* ... */});// unsubscribeunsubscribe();

useInterceptor hook

in some rare use-cases you need changing wire behavior and interfering set value. useInterceptor helps in these use-cases.

more detail

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.

constwire=useInterceptor(anotherWire,(nextValue,preValue)=>/* anotherValue */)

example:

constvalueWire=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 is null 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 up-link wire value
  • In CSR, If the up-link wire has undefined value, the initial value will be used and up-link wire value will be updated
  • In SSR, If the up-link wire has undefined value, the initial value will be used and a warning will be shown

Examples:

constwire1=useWire(null,1);constwire2=useWire(wire1,2);wire1.getValue();// => 1wire2.getValue();// => 1
constwire1=useWire(null);constwire2=useWire(wire1,2);wire1.getValue();// => 2wire2.getValue();// => 2
constwire=useWire(null,1);const[state]=useWireState(wire,2);wire.getValue();// => 1state;// => 1
constwire=useWire(null);const[state]=useWireState(wire,2);wire.getValue();// => 2state;// => 2
// ssrconstwire1=useWire(null);constwire2=useWire(wire1,2);// warning: 'upLink value is undefined. uplink without value is not supported in server side rendering'wire1.getValue();// => undefinedwire2.getValue();// => 2
// ssrconstwire1=useWire(null);constwire2=useWire(wire1);wire1.getValue();// => undefinedwire2.getValue();// => undefined

Server Side Rendering

In SSR, up-link wire without value is not supported. if you use up-link wire without value, a warning will be shown.

Rewiring

Please avoid changing the wire variable. if wire argument changed, a warning will be shown.

// wrong, avoid.constwire=useWire(condition ? wire1 : wire2);constvalue=useWireValue(condition ? wire1 : wire2);const[state,setState]=useWireState(condition ? wire1 : wire2);

About

connect react components with wire, end controlled/uncontrolled fight with controllable states

Resources

Stars

20 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages