|
| 1 | +# react-rv |
| 2 | + |
| 3 | +react-rv is a lightweight and efficient state management library for React that allows you to create reactive variables and subscribe to them with minimal overhead. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Tiny**: No dependencies, minimal API surface. |
| 8 | +- **Easier than React Context**: More efficient updates without unnecessary re-renders. |
| 9 | +- **Efficient**: Only re-renders components that are subscribed to reactive variables. |
| 10 | +- **Flexible**: Works inside and outside React components. |
| 11 | +- **Custom equality checks**: Define your own comparison logic to avoid redundant updates. |
| 12 | +- **TypeScript Support**: Fully typed for better developer experience. |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +### npm |
| 17 | + |
| 18 | +```sh |
| 19 | +npm install react-rv |
| 20 | +``` |
| 21 | + |
| 22 | +### pnpm |
| 23 | + |
| 24 | +```sh |
| 25 | +pnpm add react-rv |
| 26 | +``` |
| 27 | + |
| 28 | +### yarn |
| 29 | + |
| 30 | +```sh |
| 31 | +yarn add react-rv |
| 32 | +``` |
| 33 | + |
| 34 | +## API and Usage |
| 35 | + |
| 36 | +### `rv<T>(initialValue: T, options?: RvOptions<T>): Rv<T>` |
| 37 | + |
| 38 | +Creates a new reactive variable. |
| 39 | + |
| 40 | +#### Usage: |
| 41 | +```ts |
| 42 | +const state = rv(0) |
| 43 | +// calling variable with no arguments will return its current value |
| 44 | +console.log(state()) // 0 |
| 45 | +// calling variable with an argument will set its value to this argument |
| 46 | +state(10) |
| 47 | +console.log(state()) // 10 |
| 48 | +``` |
| 49 | + |
| 50 | +#### Options: |
| 51 | + |
| 52 | +```ts |
| 53 | +const state = rv(0, { eq: (oldValue, newValue) => Math.abs(oldValue - newValue) < 0.01 }) |
| 54 | +state(0.005) // Won't trigger an update because the values are "equal" under this custom rule. |
| 55 | +state(0.005, { |
| 56 | + // you can disable initial `eq` function by pasing false here |
| 57 | + // it will use a default `eq` function which is just a strict check: `===` |
| 58 | + // in this case, update WILL happen |
| 59 | + eq: false |
| 60 | +}) |
| 61 | +state(0.005, { |
| 62 | + // you can override initial `eq` function by passing another one just for this update call |
| 63 | + eq: (oldValue, newValue): boolean => false |
| 64 | +}) |
| 65 | +``` |
| 66 | + |
| 67 | +### `rv.on(listener: (value: T) => void): CleanupFn` |
| 68 | + |
| 69 | +Subscribes to changes of the reactive variable. |
| 70 | + |
| 71 | +#### Usage: |
| 72 | + |
| 73 | +```ts |
| 74 | +const state = rv(0) |
| 75 | +const unsubscribe = state.on(value => console.log('New value:', value)) |
| 76 | +state(1) // Logs: New value: 1 |
| 77 | +unsubscribe() |
| 78 | +state(2) // No logs |
| 79 | +``` |
| 80 | + |
| 81 | +### `useRv<T>(rv: Rv<T>): T` |
| 82 | + |
| 83 | +Subscribes to changes of the reactive variable inside a react component and updates it when the state is updated. |
| 84 | + |
| 85 | +```tsx |
| 86 | +import React from 'react' |
| 87 | +import { rv, useRv } from 'react-rv' |
| 88 | + |
| 89 | +const counter = rv(0) |
| 90 | + |
| 91 | +const Counter = () => { |
| 92 | + const value = useRv(counter) |
| 93 | + return ( |
| 94 | + <div> |
| 95 | + <p>Count: {value}</p> |
| 96 | + <button onClick={() => counter(value + 1)}>Increment</button> |
| 97 | + </div> |
| 98 | + ) |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +## Why `react-rv` Over React Context? |
| 103 | + |
| 104 | +- **More granual state splitting**: React Context would require to create a separate context for every piece of state you'd use, which would also force you to wrap the app with providers for each react context. |
| 105 | +If you don't do that and decide to create one big state, it would involve unnecessary re-renders in components that only need one field of the state. |
| 106 | +- **Simpler API**: No need for providers, reducers, or complex state logic. |
| 107 | +- **No need for state setters**: The variable that's returned by `rv()` function is already a getter/setter itself. |
| 108 | +- **Usage outside React component tree**: Since the variable itself is a setter as well, you can use it outside react component tree. For example in your util functions that's defined outside react components. |
| 109 | + |
| 110 | +#### React Context example |
| 111 | + |
| 112 | +```tsx |
| 113 | +// you need to provide non-sensical default setters in order to please TypeScript |
| 114 | +// or manually cast the value into the correct type |
| 115 | +const CountContext = React.createContext({ count: 0, setCount: () => {} }) |
| 116 | +const NameContext = React.createContext({ name: "John", setName: () => {} }) |
| 117 | + |
| 118 | +const App = () => { |
| 119 | + // you need to use `useState` hook to manage getting/setting your state |
| 120 | + const [count, setCount] = useState(0) |
| 121 | + const [name, setName] = useState("John") |
| 122 | + |
| 123 | + // With react context, you need to wrap the app into providers. |
| 124 | + // Additionally, you need to provide "setters" for every state in case |
| 125 | + // this state can be updated. |
| 126 | + return ( |
| 127 | + <CountContext.Provider value={{ count, setCount }}> |
| 128 | + <NameContext.Provider value={{ name, setName }}> |
| 129 | + {/* uses CountContext */} |
| 130 | + <Counter /> |
| 131 | + {/* uses NameContext */} |
| 132 | + <NameDisplay /> |
| 133 | + </NameContext.Provider> |
| 134 | + </CountContext.Provider> |
| 135 | + ) |
| 136 | +} |
| 137 | + |
| 138 | +const Counter = () => { |
| 139 | + const { count, setCount } = useContext(CountContext) |
| 140 | + |
| 141 | + // you can only define your own custom setters inside react component tree |
| 142 | + // in order to get access to current state |
| 143 | + const increment = () => setCount(count + 1) |
| 144 | + |
| 145 | + return <button onClick={increment}>Count: {count}</button>; |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +#### react-rv example |
| 150 | + |
| 151 | +```tsx |
| 152 | +// after you provide these default values, you don't need to set them again in any `useState` hook |
| 153 | +const countVar = rv(0) |
| 154 | +const nameVar = rv("John") |
| 155 | + |
| 156 | +// there's no need to use provider components |
| 157 | +const App = () => ( |
| 158 | + <> |
| 159 | + {/* uses countVar */} |
| 160 | + <Counter /> |
| 161 | + {/* uses nameVar */} |
| 162 | + <NameDisplay /> |
| 163 | + </> |
| 164 | +) |
| 165 | + |
| 166 | +// you can define your own setters anywhere and you can still read the current state. |
| 167 | +// calling a reactive variable with no arguments will return its current value, no matter where you are. |
| 168 | +const increment = () => countVar(countVar() + 1) |
| 169 | + |
| 170 | +const Counter = () => { |
| 171 | + // whenever `countVar` value is updated, this hook will re-render this component |
| 172 | + const count = useRv(countVar) |
| 173 | + |
| 174 | + return <button onClick={increment}>Count: {count}</button>; |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +## License |
| 179 | + |
| 180 | +MIT |
| 181 | + |
0 commit comments