From c4db300bcdf3972d77b067e5b7471da005e9a52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 17:53:15 +0200 Subject: [PATCH 1/8] chore: update readme --- README.md | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdbd118..471f6d8 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,143 @@ # React Arven -A lightweight React context helper with **stable action references** and **selective re-renders**. +A lightweight, fully typed React context helper with **stable action references** and **subscribable context**. + +## Installation + +Install react-arven with npm, yarn or pnpm: + +```sh +npm i react-arven +``` + +## Usage + +### Create provider + +You create a provider component, you can use hooks there as in regular react component. Instead of rendering html, we return object with actions and state properties. + +```ts +import { createProvider } from "react-arven"; + +const [CounterProvider, useCounterActions, useCounterState] = createProvider(() => { + const [count, setCount] = useState(0); + + function increment() { // No useCallback necessary! + setCount(val => val + 1) + } + + return { + actions: { increment }, + state: { count }, + }; +}); +``` + +1. `actions` - is an object with functions, to modify the state, accessible through `useCounterActions` +2. `state` - is a subscribable state, which you can access through `useCounterState` + +You can name your provider and hooks whatever you like. I recommend convention similar to this one. + +Both hooks are typed automatically through type inferrence, if you use TypeScript. + +### Use provider in your app + +Use the provider component, to provide context to children. + +```tsx +function CounterApp() { + return ( + + + + ) +} +``` + +### Use state and actions + +Now you can use hooks returned from the `createProvider` to use state and actions. + +```tsx +function Counter() { + const count = useCounterState(s => s.count) // selecting only what is needed + const { increment } = useCounterActions() + return ( +
+
Count: {count}
+ +
+ ) +} +``` + +### Actions object + +This library allows you to avoid `useCallback` hook in the provider. The library creates a static object which follows the structure of your passed `actions`, this object contains stable functions, which will call your actual `action`. So even though you recreate the actions object on every render of the provider, you can safely use any action and your component won't rerender because of that. + +WARNING: actions object needs to have the same structure every time and is only intended for providing functions, not data. + +### State object + +State object is passed to children "as-is", however you are expected to only select what you need through the selector function. Library will only re-render when the returned value differ (based on `Object.is`). + +So this way you can have very big state, but still be performant and avoid unnecessary re-renders. + +### Early return + +If you know your state is not complete while you are waiting for some async data, you can return a fallback component instead of state and actions. + +```ts +import { createProvider } from "react-arven"; + +const [CounterProvider, useCounterActions, useCounterState] = createProvider(() => { + const { data, refetch } = useSomeFetchFunction(....); + + if (!data) { + return + } + + return { + actions: { refetch }, + state: { data }, + }; +}); +``` + +If you return react component from `createProvider` function, library will just render the component without providing context and rendering children. + +This way you can make sure your children will never recieve `data` as undefined. + +### Provider with props + +You can pass props to your provider the same way as to any other react component and then use them in the provider body, you can also pass them through context as well. + +```tsx + +type Props = { + itemId: number +} + +const [ItemDataProvider, ...] = createProvider(({ itemId }: Props) => { + const { data, refetch } = useSomeFetchFunction(`/api/item/${itemId}`); + + return { + actions: { refetch }, + state: { data, itemId }, + }; +}); + +// Usage: + +function MyApp() { + return ( + + + + ) +} +``` + + + From cdeee2509e6494fb7425350c7ea23906221e0b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 18:03:29 +0200 Subject: [PATCH 2/8] chore: update docs --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 471f6d8..3bbdda1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # React Arven -A lightweight, fully typed React context helper with **stable action references** and **subscribable context**. + A lightweight, fully typed React context helper with **stable action references** and **subscribable context**. ## Installation @@ -140,5 +140,27 @@ function MyApp() { } ``` +### Using `shallow` + +If you need to transform data in the state selector, use shallow function to perform comparison on object properties instead of top level object. + +```tsx +import { shallow } from 'react-arven' + +function Counter() { + const { count, label } = useCounterState( + s => ({ + count: s.count, + label: c.label + }), + shallow + ) + + ... +} +``` + +This way you'll make sure your component doesn't re-render every time. This functionality is inspired by zustand library. + From 5e0a2c571dd5755f61590fbd54f76cb9b739bfc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 18:07:24 +0200 Subject: [PATCH 3/8] chore: update docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3bbdda1..3c15c13 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ If you return react component from `createProvider` function, library will just This way you can make sure your children will never recieve `data` as undefined. +REMINDER: Don't use hooks after early return statement [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) still apply! + ### Provider with props You can pass props to your provider the same way as to any other react component and then use them in the provider body, you can also pass them through context as well. From a08ab8cf356202c0984fa95a4f1bbdc6a9333c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 18:40:44 +0200 Subject: [PATCH 4/8] chore: update docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c15c13..9719fce 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ npm i react-arven You create a provider component, you can use hooks there as in regular react component. Instead of rendering html, we return object with actions and state properties. -```ts +```tsx import { createProvider } from "react-arven"; const [CounterProvider, useCounterActions, useCounterState] = createProvider(() => { @@ -89,7 +89,7 @@ So this way you can have very big state, but still be performant and avoid unnec If you know your state is not complete while you are waiting for some async data, you can return a fallback component instead of state and actions. -```ts +```tsx import { createProvider } from "react-arven"; const [CounterProvider, useCounterActions, useCounterState] = createProvider(() => { From 211797c422ccd194d15b5431440b2550201b44bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 18:44:39 +0200 Subject: [PATCH 5/8] chore: english improvements --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9719fce..95a3c91 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ npm i react-arven ### Create provider -You create a provider component, you can use hooks there as in regular react component. Instead of rendering html, we return object with actions and state properties. +You create a provider component, you can use hooks there as in a regular React component. Instead of rendering HTML, we return an object with actions and state properties. ```tsx import { createProvider } from "react-arven"; @@ -40,7 +40,7 @@ const [CounterProvider, useCounterActions, useCounterState] = createProvider(() You can name your provider and hooks whatever you like. I recommend convention similar to this one. -Both hooks are typed automatically through type inferrence, if you use TypeScript. +Both hooks are typed automatically through type inference, if you use TypeScript. ### Use provider in your app @@ -75,13 +75,13 @@ function Counter() { ### Actions object -This library allows you to avoid `useCallback` hook in the provider. The library creates a static object which follows the structure of your passed `actions`, this object contains stable functions, which will call your actual `action`. So even though you recreate the actions object on every render of the provider, you can safely use any action and your component won't rerender because of that. +This library lets you avoid the `useCallback` hook in the provider. Internally, it creates a stable object that mirrors the structure of your `actions`, where each function delegates to your actual implementation. This means that even if the actions object is recreated on every render, components using it won't re-render unnecessarily. WARNING: actions object needs to have the same structure every time and is only intended for providing functions, not data. ### State object -State object is passed to children "as-is", however you are expected to only select what you need through the selector function. Library will only re-render when the returned value differ (based on `Object.is`). +State object is passed to children "as-is", however you are expected to only select what you need through the selector function. The library will only re-render when the returned value differs (based on `Object.is`). So this way you can have very big state, but still be performant and avoid unnecessary re-renders. @@ -106,15 +106,15 @@ const [CounterProvider, useCounterActions, useCounterState] = createProvider(() }); ``` -If you return react component from `createProvider` function, library will just render the component without providing context and rendering children. +If you return a React component from the `createProvider` function, the library will just render the component without providing context and rendering children. -This way you can make sure your children will never recieve `data` as undefined. +This way you can make sure your children will never receive `data` as undefined. REMINDER: Don't use hooks after early return statement [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) still apply! ### Provider with props -You can pass props to your provider the same way as to any other react component and then use them in the provider body, you can also pass them through context as well. +You can pass props to your provider the same way as to any other React component and then use them in the provider body, you can also pass them through context. ```tsx @@ -144,7 +144,7 @@ function MyApp() { ### Using `shallow` -If you need to transform data in the state selector, use shallow function to perform comparison on object properties instead of top level object. +If you need to transform data in the state selector, use the shallow function to perform comparison on object properties instead of the top-level object. ```tsx import { shallow } from 'react-arven' From 537eb8bec3bdb061a4ebf1784f57c1e1303a97d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 18:51:01 +0200 Subject: [PATCH 6/8] chore: add badges and lib comparison --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 95a3c91..a5d461a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,26 @@ # React Arven - A lightweight, fully typed React context helper with **stable action references** and **subscribable context**. +[![npm](https://img.shields.io/npm/v/react-arven)](https://www.npmjs.com/package/react-arven) +[![bundle size](https://img.shields.io/bundlephobia/minzip/react-arven)](https://bundlephobia.com/package/react-arven) +[![license](https://img.shields.io/npm/l/react-arven)](https://github.com/stepan662/react-arven/blob/main/LICENSE) + +A lightweight, fully typed React context helper with **stable action references** and **subscribable context**. + +## Why React Arven? + +Plain `useContext` re-renders every consumer on every state change, which pushes you toward splitting contexts, memoizing selectors, and wrapping every action in `useCallback`. Libraries like Zustand or Jotai solve this but live outside the React component tree — you lose the ability to use hooks naturally inside the store. + +React Arven sits in the middle: it gives you a hook-friendly provider body (just write hooks as you normally would), granular re-renders via selector subscriptions, and stable action references — without any extra boilerplate. + +| | React Arven | plain `useContext` | Zustand | constate | use-context-selector | +|---|---|---|---|---|---| +| Hooks inside store | Yes | Yes | No | Yes | Yes | +| Granular re-renders | Yes | No | Yes | Partial* | Yes | +| Stable action refs | Yes | Manual (`useCallback`) | Yes | Manual (`useCallback`) | Manual (`useCallback`) | +| Scoped to component tree | Yes | Yes | No | Yes | Yes | + +\* constate achieves granular re-renders by splitting your hook into multiple separate contexts — one per value. This works well but requires you to restructure your code around it. React Arven uses selectors on a single context instead. ## Installation @@ -73,11 +92,40 @@ function Counter() { } ``` +### TypeScript + +The hooks returned by `createProvider` are fully typed — no manual annotations needed. Types are inferred directly from what you return in the provider body: + +```tsx +const [CounterProvider, useCounterActions, useCounterState] = createProvider(() => { + const [count, setCount] = useState(0); + function increment() { setCount(val => val + 1) } + + return { + actions: { increment }, + state: { count }, + }; +}); + +// useCounterState: (selector: (state: { count: number }) => T) => T +// useCounterActions: () => { increment: () => void } +``` + +If you use `createProvider` with props, the provider component is typed accordingly: + +```tsx +type Props = { itemId: number } + +const [ItemDataProvider] = createProvider(({ itemId }: Props) => { ... }); + +// ItemDataProvider expects: { itemId: number, children: React.ReactNode } +``` + ### Actions object This library lets you avoid the `useCallback` hook in the provider. Internally, it creates a stable object that mirrors the structure of your `actions`, where each function delegates to your actual implementation. This means that even if the actions object is recreated on every render, components using it won't re-render unnecessarily. -WARNING: actions object needs to have the same structure every time and is only intended for providing functions, not data. +> **Note:** The actions object must have the same structure on every render and is intended for functions only — do not put data in it. ### State object @@ -110,14 +158,13 @@ If you return a React component from the `createProvider` function, the library This way you can make sure your children will never receive `data` as undefined. -REMINDER: Don't use hooks after early return statement [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) still apply! +> **Note:** Don't use hooks after early return statement — [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) still apply! ### Provider with props You can pass props to your provider the same way as to any other React component and then use them in the provider body, you can also pass them through context. ```tsx - type Props = { itemId: number } @@ -162,7 +209,4 @@ function Counter() { } ``` -This way you'll make sure your component doesn't re-render every time. This functionality is inspired by zustand library. - - - +This way you'll make sure your component doesn't re-render every time. This functionality is inspired by the Zustand library. From 39531cc09f2e051a7d8b0d2a3df128285622f90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 19:02:14 +0200 Subject: [PATCH 7/8] chore: remove tests from src folder --- README.md | 2 +- .../__tests__ => tests}/createProvider.test.tsx | 2 +- .../createStableActions.test.ts | 2 +- .../renderPerformance.test.tsx | 4 ++-- lib/{src/__tests__ => tests}/shallow.test.ts | 2 +- .../__tests__ => tests}/test-utils-react16.ts | 0 .../__tests__ => tests}/test-utils-react17.ts | 0 lib/{src/__tests__ => tests}/test-utils.ts | 0 lib/tsconfig.json | 4 ++-- lib/vitest.config.ts | 16 +++++++++------- lib/vitest.workspace.ts | 4 ++-- 11 files changed, 19 insertions(+), 17 deletions(-) rename lib/{src/__tests__ => tests}/createProvider.test.tsx (99%) rename lib/{src/__tests__ => tests}/createStableActions.test.ts (96%) rename lib/{src/__tests__ => tests}/renderPerformance.test.tsx (98%) rename lib/{src/__tests__ => tests}/shallow.test.ts (97%) rename lib/{src/__tests__ => tests}/test-utils-react16.ts (100%) rename lib/{src/__tests__ => tests}/test-utils-react17.ts (100%) rename lib/{src/__tests__ => tests}/test-utils.ts (100%) diff --git a/README.md b/README.md index a5d461a..c8f3036 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # React Arven [![npm](https://img.shields.io/npm/v/react-arven)](https://www.npmjs.com/package/react-arven) -[![bundle size](https://img.shields.io/bundlephobia/minzip/react-arven)](https://bundlephobia.com/package/react-arven) +[![bundle size](https://img.shields.io/npm/unpacked-size/react-arven)](https://www.npmjs.com/package/react-arven) [![license](https://img.shields.io/npm/l/react-arven)](https://github.com/stepan662/react-arven/blob/main/LICENSE) A lightweight, fully typed React context helper with **stable action references** and **subscribable context**. diff --git a/lib/src/__tests__/createProvider.test.tsx b/lib/tests/createProvider.test.tsx similarity index 99% rename from lib/src/__tests__/createProvider.test.tsx rename to lib/tests/createProvider.test.tsx index bd70ba2..82db914 100644 --- a/lib/src/__tests__/createProvider.test.tsx +++ b/lib/tests/createProvider.test.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { describe, test, expect, vi } from "vitest"; import { act, render, screen } from "@test-utils"; -import { createProvider } from "../createProvider"; +import { createProvider } from "../src/createProvider"; describe("createProvider", () => { test("renders provider and exposes state/actions", () => { diff --git a/lib/src/__tests__/createStableActions.test.ts b/lib/tests/createStableActions.test.ts similarity index 96% rename from lib/src/__tests__/createStableActions.test.ts rename to lib/tests/createStableActions.test.ts index 98cc3fc..5164560 100644 --- a/lib/src/__tests__/createStableActions.test.ts +++ b/lib/tests/createStableActions.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, vi } from "vitest"; -import { createStableActions, ActionMap } from "../useStableActions"; +import { createStableActions, ActionMap } from "../src/useStableActions"; type CounterActions = { increment: () => number; diff --git a/lib/src/__tests__/renderPerformance.test.tsx b/lib/tests/renderPerformance.test.tsx similarity index 98% rename from lib/src/__tests__/renderPerformance.test.tsx rename to lib/tests/renderPerformance.test.tsx index e54dc87..2a44605 100644 --- a/lib/src/__tests__/renderPerformance.test.tsx +++ b/lib/tests/renderPerformance.test.tsx @@ -1,8 +1,8 @@ import React from "react"; import { describe, test, expect } from "vitest"; import { act, render, screen } from "@test-utils"; -import { createProvider } from "../createProvider"; -import { shallow } from "../shallow"; +import { createProvider } from "../src/createProvider"; +import { shallow } from "../src/shallow"; describe("render performance", () => { test("no unnecessary re-renders on unrelated state change", () => { diff --git a/lib/src/__tests__/shallow.test.ts b/lib/tests/shallow.test.ts similarity index 97% rename from lib/src/__tests__/shallow.test.ts rename to lib/tests/shallow.test.ts index 31da594..5b990b0 100644 --- a/lib/src/__tests__/shallow.test.ts +++ b/lib/tests/shallow.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { shallow } from "../shallow"; +import { shallow } from "../src/shallow"; describe("shallow", () => { test("returns true for the same reference", () => { diff --git a/lib/src/__tests__/test-utils-react16.ts b/lib/tests/test-utils-react16.ts similarity index 100% rename from lib/src/__tests__/test-utils-react16.ts rename to lib/tests/test-utils-react16.ts diff --git a/lib/src/__tests__/test-utils-react17.ts b/lib/tests/test-utils-react17.ts similarity index 100% rename from lib/src/__tests__/test-utils-react17.ts rename to lib/tests/test-utils-react17.ts diff --git a/lib/src/__tests__/test-utils.ts b/lib/tests/test-utils.ts similarity index 100% rename from lib/src/__tests__/test-utils.ts rename to lib/tests/test-utils.ts diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 8dc180c..073fbd2 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -8,8 +8,8 @@ "noEmit": false, "jsx": "react-jsx", "paths": { - "@test-utils": ["./src/__tests__/test-utils.ts"] + "@test-utils": ["./tests/test-utils.ts"] } }, - "include": ["src/**/*"] + "include": ["src/**/*", "tests/createProvider.test.tsx", "tests/createStableActions.test.ts", "tests/renderPerformance.test.tsx", "tests/shallow.test.ts", "tests/test-utils-react16.ts", "tests/test-utils-react17.ts", "tests/test-utils.ts"] } diff --git a/lib/vitest.config.ts b/lib/vitest.config.ts index 1b968b7..9a719aa 100644 --- a/lib/vitest.config.ts +++ b/lib/vitest.config.ts @@ -1,19 +1,21 @@ -import { defineConfig } from 'vitest/config'; -import react from '@vitejs/plugin-react'; -import { fileURLToPath } from 'url'; +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import { fileURLToPath } from "url"; export default defineConfig({ plugins: [react()], test: { - environment: 'jsdom', + environment: "jsdom", globals: true, }, resolve: { alias: [ { - find: '@test-utils', - replacement: fileURLToPath(new URL('./src/__tests__/test-utils.ts', import.meta.url)), + find: "@test-utils", + replacement: fileURLToPath( + new URL("./tests/test-utils.ts", import.meta.url), + ), }, ], }, -}); \ No newline at end of file +}); diff --git a/lib/vitest.workspace.ts b/lib/vitest.workspace.ts index 27cc684..7725ee0 100644 --- a/lib/vitest.workspace.ts +++ b/lib/vitest.workspace.ts @@ -18,7 +18,7 @@ export default defineWorkspace([ alias: [ { find: "@test-utils", - replacement: resolve(__dir, "src/__tests__/test-utils-react17.ts"), + replacement: resolve(__dir, "tests/test-utils-react17.ts"), }, { find: "@testing-library/react", @@ -46,7 +46,7 @@ export default defineWorkspace([ alias: [ { find: "@test-utils", - replacement: resolve(__dir, "src/__tests__/test-utils-react16.ts"), + replacement: resolve(__dir, "tests/test-utils-react16.ts"), }, { find: "@testing-library/react", From 2471e905291216ffc1c1373aeebc6e7ebf87d4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 4 Jun 2026 19:05:26 +0200 Subject: [PATCH 8/8] chore: remove sourcemaps --- lib/package.json | 3 +-- rollup.config.js | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/package.json b/lib/package.json index 9643bf5..0982abc 100644 --- a/lib/package.json +++ b/lib/package.json @@ -22,8 +22,7 @@ } }, "files": [ - "dist", - "src" + "dist" ], "scripts": { "build": "rollup -c ../rollup.config.js", diff --git a/rollup.config.js b/rollup.config.js index 4fd042d..967817b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -20,7 +20,7 @@ export default { declaration: true, declarationDir: path.resolve(__dirname, "lib/dist"), rootDir: path.resolve(__dirname, "lib/src"), - sourceMap: true, + sourceMap: false, jsx: "react-jsx", }), filesize({ @@ -31,24 +31,20 @@ export default { { file: path.resolve(__dirname, "lib/dist/index.esm.js"), format: "esm", - sourcemap: true, }, { file: path.resolve(__dirname, "lib/dist/index.esm.min.js"), format: "esm", - sourcemap: true, plugins: [terser()], }, { file: path.resolve(__dirname, "lib/dist/index.cjs.js"), format: "cjs", - sourcemap: true, exports: "named", }, { file: path.resolve(__dirname, "lib/dist/index.cjs.min.js"), format: "cjs", - sourcemap: true, exports: "named", plugins: [terser()], },