@aha-app/react-easy-state
Version:
React state management with a minimal API. Made with ES6 Proxies.
721 lines (579 loc) • 22.1 kB
JavaScript
import { useState, memo, useMemo, useEffect, Component } from 'react';
import { unstable_batchedUpdates } from './react-platform';
import { observe, unobserve, isObservable, raw, observable } from '@nx-js/observer-util';
export { unobserve as clearEffect } from '@nx-js/observer-util';
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
enumerableOnly && (symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
})), keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = null != arguments[i] ? arguments[i] : {};
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
_defineProperty(target, key, source[key]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
return target;
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
Object.defineProperty(subClass, "prototype", {
writable: false
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
} else if (call !== void 0) {
throw new TypeError("Derived constructors may only return object or undefined");
}
return _assertThisInitialized(self);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _superPropBase(object, property) {
while (!Object.prototype.hasOwnProperty.call(object, property)) {
object = _getPrototypeOf(object);
if (object === null) break;
}
return object;
}
function _get() {
if (typeof Reflect !== "undefined" && Reflect.get) {
_get = Reflect.get;
} else {
_get = function _get(target, property, receiver) {
var base = _superPropBase(target, property);
if (!base) return;
var desc = Object.getOwnPropertyDescriptor(base, property);
if (desc.get) {
return desc.get.call(arguments.length < 3 ? target : receiver);
}
return desc.value;
};
}
return _get.apply(this, arguments);
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _iterableToArrayLimit(arr, i) {
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
if (_i == null) return;
var _arr = [];
var _n = true;
var _d = false;
var _s, _e;
try {
for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
// it is window in the DOM and global in NodeJS and React Native
var isDOM = typeof window !== 'undefined';
var isNative = typeof global !== 'undefined';
var globalObj = isDOM ? window : isNative ? global : undefined;
var hasHooks = typeof useState === 'function';
var isInsideFunctionComponent = false;
var isInsideClassComponentRender = false;
var isInsideFunctionComponentWithoutHooks = false;
var COMPONENT = Symbol('owner component');
function mapStateToStores(state) {
// find store properties and map them to their none observable raw value
// to do not trigger none static this.setState calls
// from the static getDerivedStateFromProps lifecycle method
var component = state[COMPONENT];
return Object.keys(component).map(function (key) {
return component[key];
}).filter(isObservable).map(raw);
} // We batch all updates to the view until the end of the current task. This
// is to prevent excessive rendering in situations where updates can occur
// outside of React's built-in batching. e.g. after resolving a promise,
// in a setTimeout callback, in an event handler.
//
// NOTE: This should be revisited after React improves batching for
// Suspense / etc.
var batchesPending = {};
var taskPending = false;
var viewIndexCounter = 0;
var inEventLoop = false;
function runBatch() {
var batchesToRun = batchesPending;
taskPending = false;
batchesPending = {};
unstable_batchedUpdates(function () {
return Object.values(batchesToRun).forEach(function (setStateFn) {
return setStateFn();
});
});
}
function batchSetState(viewIndex, fn) {
batchesPending[viewIndex] = fn;
if (!taskPending) {
taskPending = true; // If we're in an event handler, we'll run the batch at the end of it.
if (inEventLoop) return;
queueMicrotask(function () {
runBatch();
});
}
} // No need to trigger an update for this view since it has been removed.
function clearBatch(viewIndex) {
delete batchesPending[viewIndex];
} // this creates and returns a wrapped version of the passed function
// the cache is necessary to always map the same thing to the same function
// which makes sure that addEventListener/removeEventListener pairs don't break
var cache = new WeakMap();
function wrapFn(fn, wrapper) {
if (typeof fn !== 'function') {
return fn;
}
var wrapped = cache.get(fn);
if (!wrapped) {
wrapped = function wrapped() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return wrapper(fn, this, args);
};
cache.set(fn, wrapped);
}
return wrapped;
}
function wrapMethodCallbacks(obj, method, wrapper) {
var descriptor = Object.getOwnPropertyDescriptor(obj, method);
if (descriptor && descriptor.writable && typeof descriptor.value === 'function') {
obj[method] = new Proxy(descriptor.value, {
apply: function apply(target, ctx, args) {
return Reflect.apply(target, ctx, args.map(function (f) {
return wrapFn(f, wrapper);
}));
}
});
}
} // wrapped obj.addEventListener(cb) like callbacks
function wrapMethodsCallbacks(obj, methods, wrapper) {
methods.forEach(function (method) {
return wrapMethodCallbacks(obj, method, wrapper);
});
} // batch addEventListener calls
if (globalObj.EventTarget) {
wrapMethodsCallbacks(EventTarget.prototype, ['addEventListener', 'removeEventListener'], function (fn, ctx, args) {
inEventLoop = true;
try {
fn.apply(ctx, args);
if (taskPending) {
runBatch();
}
} finally {
inEventLoop = false;
}
});
}
function view(Comp) {
var isStatelessComp = !(Comp.prototype && Comp.prototype.isReactComponent);
var ReactiveComp;
if (isStatelessComp && hasHooks) {
// use a hook based reactive wrapper when we can
ReactiveComp = function ReactiveComp(props) {
// Unique ID for each view instance.
viewIndexCounter += 1;
var viewIndex = viewIndexCounter; // use a dummy setState to update the component
var _useState = useState(),
_useState2 = _slicedToArray(_useState, 2),
setState = _useState2[1]; // create a memoized reactive wrapper of the original component (render)
// at the very first run of the component function
var render = useMemo(function () {
return observe(Comp, {
scheduler: function scheduler() {
return batchSetState(viewIndex, function () {
setState({});
});
},
lazy: true
});
}, // Adding the original Comp here is necessary to make React Hot Reload work
// it does not affect behavior otherwise
[Comp]); // cleanup the reactive connections after the very last render of the component
useEffect(function () {
return function () {
// We don't need to trigger a render after the component is removed.
clearBatch(viewIndex);
unobserve(render);
};
}, []); // the isInsideFunctionComponent flag is used to toggle `store` behavior
// based on where it was called from
isInsideFunctionComponent = true;
try {
// run the reactive render instead of the original one
return render(props);
} finally {
isInsideFunctionComponent = false;
}
};
} else {
var BaseComp = isStatelessComp ? Component : Comp; // a HOC which overwrites render, shouldComponentUpdate and componentWillUnmount
// it decides when to run the new reactive methods and when to proxy to the original methods
var ReactiveClassComp = /*#__PURE__*/function (_BaseComp) {
_inherits(ReactiveClassComp, _BaseComp);
var _super = _createSuper(ReactiveClassComp);
function ReactiveClassComp(props, context) {
var _this;
_classCallCheck(this, ReactiveClassComp);
_this = _super.call(this, props, context); // Unique ID for each class insance.
viewIndexCounter += 1;
_this.viewIndex = viewIndexCounter;
_this.state = _this.state || {};
_this.state[COMPONENT] = _assertThisInitialized(_this); // create a reactive render for the component
_this.render = observe(_this.render, {
scheduler: function scheduler() {
return batchSetState(_this.viewIndex, function () {
return _this.setState({});
});
},
lazy: true
});
return _this;
}
_createClass(ReactiveClassComp, [{
key: "render",
value: function render() {
isInsideClassComponentRender = !isStatelessComp;
isInsideFunctionComponentWithoutHooks = isStatelessComp;
try {
return isStatelessComp ? Comp(this.props, this.context) : _get(_getPrototypeOf(ReactiveClassComp.prototype), "render", this).call(this);
} finally {
isInsideClassComponentRender = false;
isInsideFunctionComponentWithoutHooks = false;
}
} // react should trigger updates on prop changes, while easyState handles store changes
}, {
key: "shouldComponentUpdate",
value: function shouldComponentUpdate(nextProps, nextState) {
var props = this.props,
state = this.state; // respect the case when the user defines a shouldComponentUpdate
if (_get(_getPrototypeOf(ReactiveClassComp.prototype), "shouldComponentUpdate", this)) {
return _get(_getPrototypeOf(ReactiveClassComp.prototype), "shouldComponentUpdate", this).call(this, nextProps, nextState);
} // return true if it is a reactive render or state changes
if (state !== nextState) {
return true;
} // the component should update if any of its props shallowly changed value
var keys = Object.keys(props);
var nextKeys = Object.keys(nextProps);
return nextKeys.length !== keys.length || nextKeys.some(function (key) {
return props[key] !== nextProps[key];
});
} // add a custom deriveStoresFromProps lifecyle method
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
// call user defined componentWillUnmount
if (_get(_getPrototypeOf(ReactiveClassComp.prototype), "componentWillUnmount", this)) {
_get(_getPrototypeOf(ReactiveClassComp.prototype), "componentWillUnmount", this).call(this);
} // We don't need to trigger a render.
clearBatch(this.viewIndex); // clean up memory used by Easy State
unobserve(this.render);
}
}], [{
key: "getDerivedStateFromProps",
value: function getDerivedStateFromProps(props, state) {
if (_get(_getPrototypeOf(ReactiveClassComp), "deriveStoresFromProps", this)) {
var _get2;
// inject all local stores and let the user mutate them directly
var stores = mapStateToStores(state);
(_get2 = _get(_getPrototypeOf(ReactiveClassComp), "deriveStoresFromProps", this)).call.apply(_get2, [this, props].concat(_toConsumableArray(stores)));
} // respect user defined getDerivedStateFromProps
if (_get(_getPrototypeOf(ReactiveClassComp), "getDerivedStateFromProps", this)) {
return _get(_getPrototypeOf(ReactiveClassComp), "getDerivedStateFromProps", this).call(this, props, state);
}
return null;
}
}]);
return ReactiveClassComp;
}(BaseComp);
ReactiveComp = ReactiveClassComp;
}
ReactiveComp.displayName = Comp.displayName || Comp.name; // static props are inherited by class components,
// but have to be copied for function components
if (isStatelessComp) {
Object.keys(Comp).forEach(function (key) {
ReactiveComp[key] = Comp[key];
});
}
return isStatelessComp && hasHooks ? memo(ReactiveComp) : ReactiveComp;
}
var taskQueue = new Set();
var scheduler = {
isOn: false,
add: function add(task) {
if (scheduler.isOn) {
taskQueue.add(task);
} else {
task();
}
},
flush: function flush() {
taskQueue.forEach(function (task) {
return task();
});
taskQueue.clear();
},
on: function on() {
scheduler.isOn = true;
},
off: function off() {
scheduler.isOn = false;
}
};
// until the function is finished running
// react renders are batched by unstable_batchedUpdates
// autoEffects and other custom reactions are batched by our scheduler
function batch(fn, ctx, args) {
// do not apply scheduler logic if it is already applied from a parent function
// it would flush in the middle of the parent's batch
if (scheduler.isOn) {
return unstable_batchedUpdates(function () {
return fn.apply(ctx, args);
});
}
try {
scheduler.on();
return unstable_batchedUpdates(function () {
return fn.apply(ctx, args);
});
} finally {
scheduler.flush();
scheduler.off();
}
} // this creates and returns a batched version of the passed function
// the cache is necessary to always map the same thing to the same function
// which makes sure that addEventListener/removeEventListener pairs don't break
var cache$1 = new WeakMap();
function batchFn(fn) {
if (typeof fn !== 'function') {
return fn;
}
var batched = cache$1.get(fn);
if (!batched) {
batched = new Proxy(fn, {
apply: function apply(target, thisArg, args) {
return batch(target, thisArg, args);
}
});
cache$1.set(fn, batched);
}
return batched;
}
function batchMethod(obj, method) {
var descriptor = Object.getOwnPropertyDescriptor(obj, method);
if (!descriptor) {
return;
}
var value = descriptor.value,
writable = descriptor.writable,
set = descriptor.set,
configurable = descriptor.configurable;
if (configurable && typeof set === 'function') {
Object.defineProperty(obj, method, _objectSpread2(_objectSpread2({}, descriptor), {}, {
set: batchFn(set)
}));
} else if (writable && typeof value === 'function') {
obj[method] = batchFn(value);
}
} // batches obj.onevent = fn like calls and store methods
function batchMethods(obj, methods) {
methods = methods || Object.getOwnPropertyNames(obj);
methods.forEach(function (method) {
return batchMethod(obj, method);
});
return obj;
}
function createStore(obj) {
return batchMethods(observable(typeof obj === 'function' ? obj() : obj));
}
function store(obj) {
// do not create new versions of the store on every render
// if it is a local store in a function component
// create a memoized store at the first call instead
if (isInsideFunctionComponent) {
// useMemo is not a semantic guarantee
// In the future, React may choose to “forget” some previously memoized values and recalculate them on next render
// see this docs for more explanation: https://reactjs.org/docs/hooks-reference.html#usememo
return useMemo(function () {
return createStore(obj);
}, []);
}
if (isInsideFunctionComponentWithoutHooks) {
throw new Error('You cannot use state inside a function component with a pre-hooks version of React. Please update your React version to at least v16.8.0 to use this feature.');
}
if (isInsideClassComponentRender) {
throw new Error('You cannot use state inside a render of a class component. Please create your store outside of the render function.');
}
return createStore(obj);
}
function autoEffect(fn) {
var deps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
if (isInsideFunctionComponent) {
return useEffect(function () {
var observer = observe(fn, {
scheduler: function scheduler$1() {
return scheduler.add(observer);
}
});
return function () {
return unobserve(observer);
};
}, deps);
}
if (isInsideFunctionComponentWithoutHooks) {
throw new Error('You cannot use autoEffect inside a function component with a pre-hooks version of React. Please update your React version to at least v16.8.0 to use this feature.');
}
if (isInsideClassComponentRender) {
throw new Error('You cannot use autoEffect inside a render of a class component. Please use it in the constructor or lifecycle methods instead.');
}
var observer = observe(fn, {
scheduler: function scheduler$1() {
return scheduler.add(observer);
}
});
return observer;
}
export { autoEffect, batch, store, view };
//# sourceMappingURL=es.es5.js.map