atoms-state v0.8.1
git clone https://git.pyrossh.dev/atoms-state
Simple State management for react
index.js
| a43e611 | 1 | import { useState, useEffect, useRef, useCallback } from 'react'; |
| a43e611 | 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; |
| a43e611 | 3 | import dot from 'dot-object'; |
| 551beab | 4 | |
| 551beab | 5 | export const atom = (initial) => { |
| a43e611 | 6 | let value; |
| a43e611 | 7 | const isDerived = typeof initial === 'function'; |
| a43e611 | 8 | const subs = new Set(); |
| a43e611 | 9 | const get = (a) => { |
| a43e611 | 10 | a.subscribe(compute); |
| a43e611 | 11 | return a.getValue(); |
| a43e611 | 12 | }; |
| a43e611 | 13 | const compute = () => { |
| a43e611 | 14 | value = initial(get); |
| a43e611 | 15 | subs.forEach((sub) => { |
| a43e611 | 16 | sub(value); |
| a43e611 | 17 | }); |
| a43e611 | 18 | }; |
| a43e611 | 19 | if (isDerived) { |
| a43e611 | 20 | compute(); |
| a43e611 | 21 | } else { |
| a43e611 | 22 | value = initial; |
| a43e611 | 23 | } |
| a43e611 | 24 | return { |
| a43e611 | 25 | getValue() { |
| a43e611 | 26 | return value; |
| a43e611 | 27 | }, |
| a43e611 | 28 | subscribe(fn) { |
| a43e611 | 29 | subs.add(fn); |
| a43e611 | 30 | return () => subs.delete(fn); |
| a43e611 | 31 | }, |
| a43e611 | 32 | update(fn) { |
| a43e611 | 33 | value = fn(value); |
| a43e611 | 34 | subs.forEach((sub) => { |
| a43e611 | 35 | sub(value); |
| a43e611 | 36 | }); |
| a43e611 | 37 | }, |
| a43e611 | 38 | }; |
| 551beab | 39 | }; |
| 551beab | 40 | |
| 551beab | 41 | export const useAtom = (atom) => { |
| a43e611 | 42 | const [data, setData] = useState(atom.getValue()); |
| a43e611 | 43 | useEffect(() => { |
| a43e611 | 44 | return atom.subscribe((value) => { |
| a43e611 | 45 | setData(value); |
| a43e611 | 46 | }); |
| a43e611 | 47 | }, []); |
| a43e611 | 48 | return data; |
| 551beab | 49 | }; |
| 551beab | 50 | |
| 551beab | 51 | export const asyncAtom = (fn) => { |
| a43e611 | 52 | let value; |
| a43e611 | 53 | let params; |
| a43e611 | 54 | const subs = new Set(); |
| a43e611 | 55 | return { |
| a43e611 | 56 | compute(p) { |
| a43e611 | 57 | if (JSON.stringify(p) !== JSON.stringify(params)) { |
| a43e611 | 58 | params = p; |
| a43e611 | 59 | value = fn(p); |
| a43e611 | 60 | value |
| a43e611 | 61 | .then((res) => { |
| a43e611 | 62 | value = res; |
| a43e611 | 63 | }) |
| a43e611 | 64 | .catch((err) => { |
| a43e611 | 65 | value = err; |
| a43e611 | 66 | }); |
| a43e611 | 67 | } |
| a43e611 | 68 | return value; |
| a43e611 | 69 | }, |
| a43e611 | 70 | fetch(p) { |
| a43e611 | 71 | return fn(p); |
| a43e611 | 72 | }, |
| a43e611 | 73 | subscribe(fn) { |
| a43e611 | 74 | subs.add(fn); |
| a43e611 | 75 | return () => subs.delete(fn); |
| a43e611 | 76 | }, |
| a43e611 | 77 | update(updater) { |
| a43e611 | 78 | value = updater(value); |
| a43e611 | 79 | subs.forEach((sub) => { |
| a43e611 | 80 | sub(value); |
| a43e611 | 81 | }); |
| a43e611 | 82 | }, |
| a43e611 | 83 | }; |
| 551beab | 84 | }; |
| 551beab | 85 | |
| 551beab | 86 | export const useAsyncAtom = (a, params) => { |
| a43e611 | 87 | const [, toggle] = useState(false); |
| a43e611 | 88 | useEffect(() => { |
| a43e611 | 89 | return a.subscribe(() => toggle((v) => !v)); |
| a43e611 | 90 | }, []); |
| a43e611 | 91 | const v = a.compute(params); |
| a43e611 | 92 | if (v instanceof Error) { |
| a43e611 | 93 | throw v; |
| a43e611 | 94 | } else if (v instanceof Promise) { |
| a43e611 | 95 | throw v; |
| a43e611 | 96 | } else { |
| a43e611 | 97 | return v; |
| a43e611 | 98 | } |
| 551beab | 99 | }; |
| 551beab | 100 | |
| 551beab | 101 | const usePromiseCache = new Map(); |
| 551beab | 102 | export const usePromise = (fn, params) => { |
| a43e611 | 103 | const [data, setData] = useState(false); |
| a43e611 | 104 | const fnCache = usePromiseCache.get(fn) || new Map(); |
| a43e611 | 105 | if (!usePromiseCache.get(fn)) { |
| a43e611 | 106 | usePromiseCache.set(fn, fnCache); |
| a43e611 | 107 | } |
| a43e611 | 108 | const key = typeof params === 'string' ? params : JSON.stringify(params); |
| a43e611 | 109 | const value = fnCache.get(key); |
| a43e611 | 110 | if (value) { |
| a43e611 | 111 | if (value instanceof Promise) { |
| a43e611 | 112 | throw value; |
| a43e611 | 113 | } else if (value instanceof Error || (value === null || value === void 0 ? void 0 : value.errors)) { |
| a43e611 | 114 | throw value; |
| 551beab | 115 | } |
| a43e611 | 116 | return [ |
| a43e611 | 117 | value, |
| a43e611 | 118 | (v) => { |
| a43e611 | 119 | fnCache.set(key, v); |
| a43e611 | 120 | setData(!data); |
| a43e611 | 121 | }, |
| a43e611 | 122 | ]; |
| a43e611 | 123 | } |
| a43e611 | 124 | fnCache.set( |
| a43e611 | 125 | key, |
| a43e611 | 126 | fn(params) |
| a43e611 | 127 | .then((res) => { |
| a43e611 | 128 | fnCache.set(key, res); |
| a43e611 | 129 | }) |
| a43e611 | 130 | .catch((err) => fnCache.set(key, err)), |
| a43e611 | 131 | ); |
| a43e611 | 132 | throw fnCache.get(key); |
| a43e611 | 133 | }; |
| a43e611 | 134 | |
| a43e611 | 135 | export const Storage = { |
| a43e611 | 136 | listeners: {}, |
| a43e611 | 137 | async getItem(key) { |
| a43e611 | 138 | const item = await AsyncStorage.getItem(key); |
| a43e611 | 139 | return item ? JSON.parse(item) : null; |
| a43e611 | 140 | }, |
| a43e611 | 141 | async setItem(key, value) { |
| 95fe2b3 | 142 | await AsyncStorage.setItem(key, JSON.stringify(value)); |
| a43e611 | 143 | if (this.listeners[key]) { |
| a43e611 | 144 | for (const cb of this.listeners[key]) { |
| a43e611 | 145 | cb(value); |
| a43e611 | 146 | } |
| a43e611 | 147 | } |
| a43e611 | 148 | }, |
| a43e611 | 149 | async removeItem(key) { |
| a43e611 | 150 | await AsyncStorage.removeItem(key); |
| 95fe2b3 | 151 | if (this.listeners[key]) { |
| 95fe2b3 | 152 | for (const cb of this.listeners[key]) { |
| 95fe2b3 | 153 | cb(null); |
| 95fe2b3 | 154 | } |
| 95fe2b3 | 155 | } |
| a43e611 | 156 | }, |
| a43e611 | 157 | async clear() { |
| a43e611 | 158 | await AsyncStorage.clear(); |
| a43e611 | 159 | }, |
| a43e611 | 160 | subscribe(key, cb) { |
| a43e611 | 161 | if (!this.listeners[key]) { |
| a43e611 | 162 | this.listeners[key] = new Set(); |
| a43e611 | 163 | } |
| a43e611 | 164 | this.listeners[key].add(cb); |
| a43e611 | 165 | return () => { |
| a43e611 | 166 | this.listeners[key].delete(cb); |
| a43e611 | 167 | }; |
| a43e611 | 168 | }, |
| a43e611 | 169 | unsubscribe(key, cb) { |
| a43e611 | 170 | this.listeners[key].delete(cb); |
| a43e611 | 171 | }, |
| a43e611 | 172 | }; |
| a43e611 | 173 | |
| a43e611 | 174 | export const useAsyncStorage = (key, initialValue) => { |
| c8aa1af | 175 | const [storedValue, setStoredValue] = useState({ |
| c8aa1af | 176 | ...initialValue, |
| c8aa1af | 177 | loading: true, |
| c8aa1af | 178 | }); |
| a43e611 | 179 | const setData = async (v) => { |
| a43e611 | 180 | await Storage.setItem(key, v); |
| a43e611 | 181 | }; |
| a43e611 | 182 | useEffect(() => { |
| a43e611 | 183 | Storage.getItem(key) |
| a43e611 | 184 | .then((v) => { |
| a43e611 | 185 | if (v) { |
| 316fb6e | 186 | setData({ ...v, loading: false }); |
| 6cdc58c | 187 | } else { |
| 316fb6e | 188 | setData({ ...initialValue, loading: false }); |
| 551beab | 189 | } |
| a43e611 | 190 | }) |
| 316fb6e | 191 | .catch((err) => setStoredValue({ ...initialValue, loading: false, err })); |
| 316fb6e | 192 | |
| a43e611 | 193 | return Storage.subscribe(key, (v) => { |
| a43e611 | 194 | setStoredValue(v); |
| a43e611 | 195 | }); |
| a43e611 | 196 | }, [key, JSON.stringify(initialValue)]); |
| c8aa1af | 197 | return [storedValue, setData]; |
| a43e611 | 198 | }; |
| a43e611 | 199 | |
| 316fb6e | 200 | export const Fields = { |
| 316fb6e | 201 | data: {}, |
| 316fb6e | 202 | keys() { |
| 316fb6e | 203 | return Object.keys(this.data); |
| 316fb6e | 204 | }, |
| 316fb6e | 205 | get(k) { |
| 316fb6e | 206 | return this.data[k]; |
| 316fb6e | 207 | }, |
| 316fb6e | 208 | set(k, v) { |
| 316fb6e | 209 | this.data[k] = v; |
| 316fb6e | 210 | }, |
| 316fb6e | 211 | remove(k) { |
| 316fb6e | 212 | delete this.data[k]; |
| 316fb6e | 213 | }, |
| 316fb6e | 214 | }; |
| a43e611 | 215 | |
| a43e611 | 216 | export const useField = (name) => { |
| a43e611 | 217 | const [error, setError] = useState(''); |
| a43e611 | 218 | const errorRef = useRef(''); |
| a43e611 | 219 | if (!name) { |
| a43e611 | 220 | throw new Error('You need to provide the "name" prop.'); |
| a43e611 | 221 | } |
| a43e611 | 222 | const setErrorMessage = (message) => { |
| a43e611 | 223 | errorRef.current = message; |
| a43e611 | 224 | setError(message); |
| a43e611 | 225 | }; |
| a43e611 | 226 | return { |
| a43e611 | 227 | name, |
| a43e611 | 228 | error, |
| a43e611 | 229 | setError: setErrorMessage, |
| 316fb6e | 230 | getField: () => Fields.get(name), |
| a43e611 | 231 | registerField: (f) => { |
| 316fb6e | 232 | const oldField = Fields.get(name) || {}; |
| 316fb6e | 233 | Fields.set(name, { |
| 316fb6e | 234 | ...oldField, |
| a43e611 | 235 | ...f, |
| a43e611 | 236 | getError: () => errorRef.current, |
| a43e611 | 237 | setError: setErrorMessage, |
| 316fb6e | 238 | }); |
| a43e611 | 239 | return () => { |
| 316fb6e | 240 | // TODO: this remove happens after refresh so that state persists for 1 render |
| 316fb6e | 241 | // could cause wrong calcuations later on |
| 316fb6e | 242 | Fields.remove(name); |
| a43e611 | 243 | }; |
| a43e611 | 244 | }, |
| a43e611 | 245 | }; |
| a43e611 | 246 | }; |
| a43e611 | 247 | |
| 316fb6e | 248 | export const useForm = (initial, submit, watchOn = []) => { |
| a43e611 | 249 | const [error, setError] = useState(''); |
| 316fb6e | 250 | const [, rerender] = useState(false); |
| 316fb6e | 251 | const refresh = () => rerender((v) => !v); |
| a43e611 | 252 | const getData = () => { |
| a43e611 | 253 | const obj = {}; |
| 316fb6e | 254 | for (const name of Fields.keys()) { |
| 316fb6e | 255 | obj[name] = Fields.get(name).get(); |
| 551beab | 256 | } |
| a43e611 | 257 | return dot.object(obj); |
| a43e611 | 258 | }; |
| a43e611 | 259 | const setData = useCallback((data) => { |
| a43e611 | 260 | const dotData = dot.dot(data); |
| 316fb6e | 261 | for (const name of Fields.keys()) { |
| 316fb6e | 262 | Fields.get(name).set(dotData[name]); |
| a43e611 | 263 | } |
| a43e611 | 264 | }, []); |
| a43e611 | 265 | useEffect(() => { |
| a43e611 | 266 | if (initial) { |
| a43e611 | 267 | setData(initial); |
| a43e611 | 268 | } |
| 316fb6e | 269 | }, [JSON.stringify(initial), setData]); |
| 316fb6e | 270 | useEffect(() => { |
| 316fb6e | 271 | for (const name of watchOn) { |
| 316fb6e | 272 | const field = Fields.get(name); |
| 316fb6e | 273 | if (field) { |
| 316fb6e | 274 | Fields.set(name, { |
| 316fb6e | 275 | ...field, |
| 316fb6e | 276 | watch: refresh, |
| 316fb6e | 277 | }); |
| 316fb6e | 278 | } |
| 316fb6e | 279 | } |
| 316fb6e | 280 | }, [Fields.keys().length]); |
| a43e611 | 281 | return { |
| a43e611 | 282 | error, |
| a43e611 | 283 | setError, |
| a43e611 | 284 | getData, |
| a43e611 | 285 | setData, |
| a43e611 | 286 | clear: () => { |
| 316fb6e | 287 | for (const name of Fields.keys()) { |
| 316fb6e | 288 | Fields.get(name).clear(); |
| a43e611 | 289 | } |
| a43e611 | 290 | }, |
| a43e611 | 291 | reset: () => { |
| a43e611 | 292 | setData(initial); |
| a43e611 | 293 | }, |
| a43e611 | 294 | getFieldValue: (name) => { |
| 316fb6e | 295 | const field = Fields.get(name); |
| a43e611 | 296 | if (field) { |
| a43e611 | 297 | return field.get(); |
| a43e611 | 298 | } |
| a43e611 | 299 | }, |
| a43e611 | 300 | setFieldValue: (name, value) => { |
| 316fb6e | 301 | const field = Fields.get(name); |
| a43e611 | 302 | if (field) { |
| a43e611 | 303 | field.set(value); |
| a43e611 | 304 | } |
| a43e611 | 305 | }, |
| a43e611 | 306 | clearFieldValue: (name) => { |
| 316fb6e | 307 | const field = Fields.get(name); |
| a43e611 | 308 | if (field) { |
| a43e611 | 309 | field.clear(); |
| a43e611 | 310 | } |
| a43e611 | 311 | }, |
| a43e611 | 312 | getFieldError: (name) => { |
| 316fb6e | 313 | const field = Fields.get(name); |
| a43e611 | 314 | if (field) { |
| a43e611 | 315 | return field.getError(); |
| a43e611 | 316 | } |
| a43e611 | 317 | return ''; |
| a43e611 | 318 | }, |
| a43e611 | 319 | setFieldError: (name, value) => { |
| 316fb6e | 320 | const field = Fields.get(name); |
| a43e611 | 321 | if (field) { |
| a43e611 | 322 | field.setError(value); |
| a43e611 | 323 | } |
| a43e611 | 324 | }, |
| a43e611 | 325 | onSubmit: async () => { |
| a43e611 | 326 | try { |
| a43e611 | 327 | const data = getData(); |
| a43e611 | 328 | setError(''); |
| 316fb6e | 329 | for (const name of Fields.keys()) { |
| 316fb6e | 330 | Fields.get(name).setError(''); |
| a43e611 | 331 | } |
| 316fb6e | 332 | const allValid = Fields.keys() |
| 316fb6e | 333 | .map((key) => Fields.get(key)) |
| a43e611 | 334 | .reduce((acc, field) => acc && field.getError() === '', true); |
| a43e611 | 335 | if (allValid) { |
| a43e611 | 336 | await submit(data); |
| a43e611 | 337 | } |
| a43e611 | 338 | } catch (err) { |
| a43e611 | 339 | // this is for validation errors |
| a43e611 | 340 | if (err.error && typeof err.error !== 'string') { |
| a43e611 | 341 | Object.keys(err.error).forEach((k) => { |
| 316fb6e | 342 | const field = Fields.get(k); |
| a43e611 | 343 | if (field) { |
| a43e611 | 344 | field.setError(err.error[k]); |
| a43e611 | 345 | } |
| a43e611 | 346 | }); |
| a43e611 | 347 | setError('Validation failed. Please correct the necessary fields.'); |
| a43e611 | 348 | } else { |
| a43e611 | 349 | setError(err.message || err.error); |
| a43e611 | 350 | } |
| a43e611 | 351 | } |
| a43e611 | 352 | }, |
| a43e611 | 353 | }; |
| 551beab | 354 | }; |