atoms-state v0.8.1

#js#react#flux

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
};