Home

Awesome

<div align="center"> <h1>🏎 side car</h1> <br/> Alternative way to code splitting <br/> <a href="https://www.npmjs.com/package/use-sidecar"> <img src="https://img.shields.io/npm/v/use-sidecar.svg?style=flat-square" /> </a> <a href="https://travis-ci.org/theKashey/use-sidecar"> <img src="https://img.shields.io/travis/theKashey/use-sidecar.svg?style=flat-square" alt="Build status"> </a> <a href="https://www.npmjs.com/package/use-sidecar"> <img src="https://img.shields.io/npm/dm/use-sidecar.svg" alt="npm downloads"> </a> <a href="https://bundlephobia.com/result?p=use-sidecar"> <img src="https://img.shields.io/bundlephobia/minzip/use-sidecar.svg" alt="bundle size"> </a> <br/> </div>

UI/Effects code splitting pattern

Terminology:

UI is a view, sidecar is the logic for it. Like Batman(UI) and his sidekick Robin(effects).

Concept

Rules

That would form two different code branches, you may load separately - UI first, and effect sidecar later. That also leads to a obvious consequence - one sidecar may export all sidecars.

except medium.read, which synchronously read the data from a medium, and medium.assingSyncMedium which changes useMedium to be sync.

SSR and usage tracking

Sidecar pattern is clear:

Thus - no usage tracking, and literally no SSR. It's just skipped.

API

createMedium()

const medium = createMedium(defaultValue);
const cancelCb = medium.useMedium(someData);

// like
useEffect(() => medium.useMedium(someData), []);

medium.assignMedium(someDataProcessor)

// createSidecarMedium is a helper for createMedium to create a "sidecar" symbol
const effectCar = createSidecarMedium();

! For consistence useMedium is async - sidecar load status should not affect function behavior, thus effect would be always executed at least in the "next tick". You may alter this behavior by using medium.assingSyncMedium.

exportSidecar(medium, component)

import {effectCar} from './medium';
import {EffectComponent} from './Effect';
// !!! - to prevent Effect from being imported
// `effectCar` medium __have__ to be defined in another file
// const effectCar = createSidecarMedium();
export default exportSidecar(effectCar, EffectComponent);

sidecar(importer)

import {sidecar} from "use-sidecar";
const Sidecar =  sidecar(() => import('./sidecar'), <span>on fail</span>);

<>
 <Sidecar />
 <UI />
</> 

Importing exportedSidecar

Would require additional prop to be set - <Sidecar sideCar={effectCar} />

useSidecar(importer)

import {useSidecar} from 'use-sidecar';

const [Car, error] = useSidecar(() => import('./sideCar'));
return (
  <>
    {Car ? <Car {...props} /> : null}
    <UIComponent {...props}>
  </>
); 

Importing exportedSideCar

You have to specify effect medium to read data from, as long as export itself is empty.

import {useSidecar} from 'use-sidecar';

/* medium.js: */ export const effectCar = useMedium({});
/* sideCar.js: */export default exportSidecar(effectCar, EffectComponent);

const [Car, error] = useSidecar(() => import('./sideCar'), effectCar); 
return (
  <>
    {Car ? <Car {...props} /> : null}
    <UIComponent {...props}>
  </>
);

renderCar(Component)

import {renderCar, sidecar} from "use-sidecar";
const RenderCar = renderCar(
  // will move side car to a side channel
  sidecar(() => import('react-powerplug').then(imports => imports.Value)),
  // default render props
  [{value: 0}]  
);

<RenderCar>
  {({value}) => <span>{value}</span>}
</RenderCar>

setConfig(config)

setConfig({
  onError, // sets default error handler
});

Examples

Deferred effect

Let's imagine - on element focus you have to do "something", for example focus anther element

Original code

onFocus = event => {
  if (event.currentTarget === event.target) {
    document.querySelectorAll('button', event.currentTarget)
  }
}

Sidecar code

  1. Use medium (yes, .3)
// we are calling medium with an original event as an argument
const onFocus = event => focusMedium.useMedium(event);
  1. Define reaction
// in a sidecar

// we are setting handler for the effect medium
// effect is complicated - we are skipping event "bubbling", 
// and focusing some button inside a parent
focusMedium.assignMedium(event => {
  if (event.currentTarget === event.target) {
    document.querySelectorAll('button', event.currentTarget)
  }
});

  1. Create medium Having these constrains - we have to clone event, as long as React would eventually reuse SyntheticEvent, thus not preserve target and currentTarget.
// 
const focusMedium = createMedium(null, event => ({...event}));

Now medium side effect is ok to be async

Example: Effect for react-focus-lock - 1kb UI, 4kb sidecar

Medium callback

Like a library level code splitting

Original code

import {x, y} from './utils';

useEffect(() => {
  if (x()) {
    y()
  }
}, []);

Sidecar code

// medium
const utilMedium = createMedium();

// utils
const x = () => { /* ... */};
const y = () => { /* ... */};

// medium will callback with exports exposed
utilMedium.assignMedium(cb => cb({
 x, y
}));


// UI
// not importing x and y from the module system, but would be given via callback
useEffect(() => {
  utilMedium.useMedium(({x,y}) => {
      if (x()) {
        y()
      }
  })
}, []);
const utilMedium = createMedium<(cb: typeof import('./utils')) => void>();

Example: Callback API for react-focus-lock

Split effects

Lets take an example from a Google - Calendar app, with view and logic separated. To be honest - it's not easy to extract logic from application like calendar - usually it's tight coupled.

Original code

const CalendarUI = () => { 
  const [date, setDate] = useState();
  const onButtonClick = useCallback(() => setDate(Date.now), []);
  
  return (
    <>
     <input type="date" onChange={setDate} value={date} />
     <input type="button" onClick={onButtonClick}>Set Today</button>
    </>
  )
}

Sidecar code

const CalendarUI = () => {
  const [events, setEvents] = useState({});
  const [date, setDate] = useState();
  
  return (
    <>
     <Sidecar setDate={setDate} setEvents={setEvents}/>
     <UILayout {...events} date={date}/>
    </>
  )
}

const UILayout = ({onDateChange, onButtonClick, date}) => (
  <>
      <input type="date" onChange={onDateChange} value={date} />
      <input type="button" onClick={onButtonClick}>Set Today</button>
  </>
);

// in a sidecar
// we are providing callbacks back to UI
const Sidecar = ({setDate, setEvents}) => {
  useEffect(() => setEvents({
      onDateChange:setDate,
      onButtonClick: () => setDate(Date.now),
  }), []);
  
  return null;
}

While in this example this looks a bit, you know, strange - there are 3 times more code that in the original example - that would make a sense for a real Calendar, especially if some helper library, like moment, has been used.

Example: Effect for react-remove-scroll - 300b UI, 2kb sidecar

Licence

MIT