{"id":49453644,"url":"https://github.com/theKashey/use-sidecar","last_synced_at":"2026-06-02T05:00:42.904Z","repository":{"id":34909225,"uuid":"188824749","full_name":"theKashey/use-sidecar","owner":"theKashey","description":"Another way to code splitting","archived":false,"fork":false,"pushed_at":"2024-12-15T01:49:31.000Z","size":565,"stargazers_count":107,"open_issues_count":7,"forks_count":11,"subscribers_count":3,"default_branch":"master","last_synced_at":"2026-04-28T11:55:42.771Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/theKashey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-05-27T10:45:01.000Z","updated_at":"2026-03-05T15:39:58.000Z","dependencies_parsed_at":"2025-01-08T11:00:24.417Z","dependency_job_id":"96ffde28-4f7d-4ea2-9c1e-7a98fc2aeb4d","html_url":"https://github.com/theKashey/use-sidecar","commit_stats":{"total_commits":46,"total_committers":5,"mean_commits":9.2,"dds":"0.13043478260869568","last_synced_commit":"d48b1454c4632368cca22f186fdd9470cd7ec76d"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/theKashey/use-sidecar","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Fuse-sidecar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Fuse-sidecar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Fuse-sidecar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Fuse-sidecar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/theKashey","download_url":"https://codeload.github.com/theKashey/use-sidecar/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Fuse-sidecar/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33806987,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-30T04:01:03.702Z","updated_at":"2026-06-02T05:00:42.898Z","avatar_url":"https://github.com/theKashey.png","language":"TypeScript","funding_links":[],"categories":["📦 Legacy \u0026 Inactive Projects"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003e🏎 side car\u003c/h1\u003e\n  \u003cbr/\u003e\n   Alternative way to code splitting\n  \u003cbr/\u003e\n  \n  \u003ca href=\"https://www.npmjs.com/package/use-sidecar\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/use-sidecar.svg?style=flat-square\" /\u003e\n  \u003c/a\u003e\n    \n  \u003ca href=\"https://travis-ci.org/theKashey/use-sidecar\"\u003e\n   \u003cimg src=\"https://img.shields.io/travis/theKashey/use-sidecar.svg?style=flat-square\" alt=\"Build status\"\u003e\n  \u003c/a\u003e \n\n  \u003ca href=\"https://www.npmjs.com/package/use-sidecar\"\u003e\n   \u003cimg src=\"https://img.shields.io/npm/dm/use-sidecar.svg\" alt=\"npm downloads\"\u003e\n  \u003c/a\u003e \n\n  \u003ca href=\"https://bundlephobia.com/result?p=use-sidecar\"\u003e\n   \u003cimg src=\"https://img.shields.io/bundlephobia/minzip/use-sidecar.svg\" alt=\"bundle size\"\u003e\n  \u003c/a\u003e   \n  \u003cbr/\u003e\n\u003c/div\u003e\n\nUI/Effects code splitting pattern\n - [read the original article](https://dev.to/thekashey/sidecar-for-a-code-splitting-1o8g) to understand concepts behind.\n - [read how Google](https://medium.com/@cramforce/designing-very-large-javascript-applications-6e013a3291a3) split view and logic.\n - [watch how Facebook](https://developers.facebook.com/videos/2019/building-the-new-facebookcom-with-react-graphql-and-relay/) defers \"interactivity\" effects. \n\n## Terminology: \n- `sidecar` - non UI component, which may carry effects for a paired UI component.\n- `UI` - UI component, which interactivity is moved to a `sidecar`.\n\n`UI` is a _view_, `sidecar` is the _logic_ for it. Like Batman(UI) and his sidekick Robin(effects). \n\n## Concept\n- a `package` exposes __3 entry points__ using a [nested `package.json` format](https://github.com/theKashey/multiple-entry-points-example):\n  - default aka `combination`, and lets hope tree shaking will save you\n  - `UI`, with only UI part\n  - `sidecar`, with all the logic\n  - \u003e `UI` + `sidecar` === `combination`. The size of `UI+sidecar` might a bit bigger than size of their `combination`.\n  Use [size-limit](https://github.com/ai/size-limit) to control their size independently. \n  \n\n- package uses a `medium` to talk with own sidecar, breaking explicit dependency.\n \n- if package depends on another _sidecar_ package:\n  - it shall export dependency side car among own sidecar.\n  - package imports own sidecar via `medium`, thus able to export multiple sidecars via one export. \n\n- final consumer uses `sidecar` or `useSidecar` to combine pieces together.  \n\n## Rules\n- `UI` components might use/import any other `UI` components\n- `sidecar` could use/import any other `sidecar`\n\nThat would form two different code branches, you may load separately - UI first, and effect sidecar later.\nThat also leads to a obvious consequence - __one sidecar may export all sidecars__.\n- to decouple `sidecars` from module exports, and be able to pick \"the right\" one at any point\nyou have to use `exportSidecar(medium, component)` to export it, and use the same `medium` to import it back.\n- this limitation is for __libraries only__, as long as in the usercode you might \ndynamically import whatever and whenever you want. \n\n- `useMedium` is always async - action would be executed in a next tick, or on the logic load.\n- `sidecar` is always async - is does not matter have you loaded logic or not - component would be \nrendered at least in the next tick.\n\n\u003e except `medium.read`, which synchronously read the data from a medium, \nand `medium.assingSyncMedium` which changes `useMedium` to be sync. \n\n## SSR and usage tracking\nSidecar pattern is clear:\n- you dont need to use/render any `sidecars` on server.\n- you dont have to load `sidecars` prior main render.\n\nThus - no usage tracking, and literally no SSR. It's just skipped.\n\n\n# API\n\n## createMedium()\n- Type: Util. Creates shared effect medium for algebraic effect.\n- Goal: To decouple modules from each other.\n- Usage: `use` in UI side, and `assign` from side-car. All effects would be executed.\n- Analog: WeakMap, React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED\n```js\nconst medium = createMedium(defaultValue);\nconst cancelCb = medium.useMedium(someData);\n\n// like\nuseEffect(() =\u003e medium.useMedium(someData), []);\n\nmedium.assignMedium(someDataProcessor)\n\n// createSidecarMedium is a helper for createMedium to create a \"sidecar\" symbol\nconst effectCar = createSidecarMedium();\n```\n\n\u003e ! For consistence `useMedium` is async - sidecar load status should not affect function behavior,\nthus effect would be always executed at least in the \"next tick\". You may alter\nthis behavior by using `medium.assingSyncMedium`.\n\n\n## exportSidecar(medium, component)\n- Type: HOC\n- Goal: store `component` inside `medium` and return external wrapper\n- Solving: decoupling module exports to support exporting multiple sidecars via a single entry point.\n- Usage: use to export a `sidecar`\n- Analog: WeakMap\n```js\nimport {effectCar} from './medium';\nimport {EffectComponent} from './Effect';\n// !!! - to prevent Effect from being imported\n// `effectCar` medium __have__ to be defined in another file\n// const effectCar = createSidecarMedium();\nexport default exportSidecar(effectCar, EffectComponent);\n```\n\n## sidecar(importer)\n- Type: HOC\n- Goal: React.lazy analog for code splitting, but does not require `Suspense`, might provide error failback.\n- Usage: like React.lazy to load a side-car component.\n- Analog: React.Lazy\n```js\nimport {sidecar} from \"use-sidecar\";\nconst Sidecar =  sidecar(() =\u003e import('./sidecar'), \u003cspan\u003eon fail\u003c/span\u003e);\n\n\u003c\u003e\n \u003cSidecar /\u003e\n \u003cUI /\u003e\n\u003c/\u003e \n```\n### Importing `exportedSidecar`\nWould require additional prop to be set - ```\u003cSidecar sideCar={effectCar} /\u003e```\n\n## useSidecar(importer)\n- Type: hook, loads a `sideCar` using provided `importer` which shall follow React.lazy API\n- Goal: to load a side car without displaying any \"spinners\".\n- Usage: load side car for a component\n- Analog: none\n```js\nimport {useSidecar} from 'use-sidecar';\n\nconst [Car, error] = useSidecar(() =\u003e import('./sideCar'));\nreturn (\n  \u003c\u003e\n    {Car ? \u003cCar {...props} /\u003e : null}\n    \u003cUIComponent {...props}\u003e\n  \u003c/\u003e\n); \n```\n### Importing `exportedSideCar`\nYou have to specify __effect medium__ to read data from, as long as __export itself is empty__.\n```js\nimport {useSidecar} from 'use-sidecar';\n\n/* medium.js: */ export const effectCar = useMedium({});\n/* sideCar.js: */export default exportSidecar(effectCar, EffectComponent);\n\nconst [Car, error] = useSidecar(() =\u003e import('./sideCar'), effectCar); \nreturn (\n  \u003c\u003e\n    {Car ? \u003cCar {...props} /\u003e : null}\n    \u003cUIComponent {...props}\u003e\n  \u003c/\u003e\n);\n```\n\n## renderCar(Component)\n- Type: HOC, moves renderProp component to a side channel\n- Goal: Provide render prop support, ie defer component loading keeping tree untouched.\n- Usage: Provide `defaults` and use them until sidecar is loaded letting you code split (non visual) render-prop component\n- Analog: - Analog: code split library like [react-imported-library](https://github.com/theKashey/react-imported-library) or [@loadable/lib](https://www.smooth-code.com/open-source/loadable-components/docs/library-splitting/).\n```js\nimport {renderCar, sidecar} from \"use-sidecar\";\nconst RenderCar = renderCar(\n  // will move side car to a side channel\n  sidecar(() =\u003e import('react-powerplug').then(imports =\u003e imports.Value)),\n  // default render props\n  [{value: 0}]  \n);\n\n\u003cRenderCar\u003e\n  {({value}) =\u003e \u003cspan\u003e{value}\u003c/span\u003e}\n\u003c/RenderCar\u003e\n```\n\n## setConfig(config)\n```js\nsetConfig({\n  onError, // sets default error handler\n});\n```\n\n# Examples\n## Deferred effect\nLet's imagine - on element focus you have to do \"something\", for example focus anther element\n\n#### Original code\n```js\nonFocus = event =\u003e {\n  if (event.currentTarget === event.target) {\n    document.querySelectorAll('button', event.currentTarget)\n  }\n}\n```\n\n#### Sidecar code\n\n3. Use medium (yes, .3)\n```js\n// we are calling medium with an original event as an argument\nconst onFocus = event =\u003e focusMedium.useMedium(event);\n```\n2. Define reaction\n```js\n// in a sidecar\n\n// we are setting handler for the effect medium\n// effect is complicated - we are skipping event \"bubbling\", \n// and focusing some button inside a parent\nfocusMedium.assignMedium(event =\u003e {\n  if (event.currentTarget === event.target) {\n    document.querySelectorAll('button', event.currentTarget)\n  }\n});\n\n```\n1. Create medium\nHaving these constrains - we have to clone `event`, as long as React would eventually reuse SyntheticEvent, thus not\npreserve `target` and `currentTarget`. \n```js\n// \nconst focusMedium = createMedium(null, event =\u003e ({...event}));\n```\nNow medium side effect is ok to be async\n\n__Example__: [Effect for react-focus-lock](https://github.com/theKashey/react-focus-lock/blob/8c69c644ecfeed2ec9dc0dc4b5b30e896a366738/src/Lock.js#L48) - 1kb UI, 4kb sidecar\n\n### Medium callback\nLike a library level code splitting\n\n#### Original code\n```js\nimport {x, y} from './utils';\n\nuseEffect(() =\u003e {\n  if (x()) {\n    y()\n  }\n}, []);\n```\n\n#### Sidecar code\n\n```js\n// medium\nconst utilMedium = createMedium();\n\n// utils\nconst x = () =\u003e { /* ... */};\nconst y = () =\u003e { /* ... */};\n\n// medium will callback with exports exposed\nutilMedium.assignMedium(cb =\u003e cb({\n x, y\n}));\n\n\n// UI\n// not importing x and y from the module system, but would be given via callback\nuseEffect(() =\u003e {\n  utilMedium.useMedium(({x,y}) =\u003e {\n      if (x()) {\n        y()\n      }\n  })\n}, []);\n```\n\n- Hint: there is a easy way to type it\n```js\nconst utilMedium = createMedium\u003c(cb: typeof import('./utils')) =\u003e void\u003e();\n``` \n\n__Example__: [Callback API for react-focus-lock](https://github.com/theKashey/react-focus-lock/blob/8c69c644ecfeed2ec9dc0dc4b5b30e896a366738/src/MoveFocusInside.js#L12) \n\n### Split effects\nLets take an example from a Google - Calendar app, with view and logic separated.\nTo be honest - it's not easy to extract logic from application like calendar - usually it's tight coupled.\n\n#### Original code\n```js\nconst CalendarUI = () =\u003e { \n  const [date, setDate] = useState();\n  const onButtonClick = useCallback(() =\u003e setDate(Date.now), []);\n  \n  return (\n    \u003c\u003e\n     \u003cinput type=\"date\" onChange={setDate} value={date} /\u003e\n     \u003cinput type=\"button\" onClick={onButtonClick}\u003eSet Today\u003c/button\u003e\n    \u003c/\u003e\n  )\n}\n```\n#### Sidecar code\n\n```js\nconst CalendarUI = () =\u003e {\n  const [events, setEvents] = useState({});\n  const [date, setDate] = useState();\n  \n  return (\n    \u003c\u003e\n     \u003cSidecar setDate={setDate} setEvents={setEvents}/\u003e\n     \u003cUILayout {...events} date={date}/\u003e\n    \u003c/\u003e\n  )\n}\n\nconst UILayout = ({onDateChange, onButtonClick, date}) =\u003e (\n  \u003c\u003e\n      \u003cinput type=\"date\" onChange={onDateChange} value={date} /\u003e\n      \u003cinput type=\"button\" onClick={onButtonClick}\u003eSet Today\u003c/button\u003e\n  \u003c/\u003e\n);\n\n// in a sidecar\n// we are providing callbacks back to UI\nconst Sidecar = ({setDate, setEvents}) =\u003e {\n  useEffect(() =\u003e setEvents({\n      onDateChange:setDate,\n      onButtonClick: () =\u003e setDate(Date.now),\n  }), []);\n  \n  return null;\n}\n```  \n\nWhile in this example this looks a bit, you know, strange - there are 3 times more code\nthat in the original example - that would make a sense for a real Calendar, especially\nif some helper library, like `moment`, has been used.\n\n__Example__: [Effect for react-remove-scroll](https://github.com/theKashey/react-remove-scroll/blob/666472d5c77fb6c4e5beffdde87c53ae63ef42c5/src/SideEffect.tsx#L166) - 300b UI, 2kb sidecar\n\n# Licence\n\nMIT\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FtheKashey%2Fuse-sidecar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FtheKashey%2Fuse-sidecar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FtheKashey%2Fuse-sidecar/lists"}