UNPKG

miniplex-react

Version:

React glue for Miniplex.

279 lines (257 loc) 9.52 kB
import { Bucket } from 'miniplex'; import React, { useLayoutEffect, useEffect, useMemo, useContext, memo, forwardRef, useRef, createContext, useState, useImperativeHandle } from 'react'; import { useRerender } from '@hmans/use-rerender'; function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } 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 _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 _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."); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } /* https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a */ var useIsomorphicLayoutEffect$1 = typeof window !== "undefined" ? useLayoutEffect : useEffect; var useIsomorphicLayoutEffect$2 = useIsomorphicLayoutEffect$1; /** * Subscribes to changes in the specified bucket, and re-renders the component * whenever entities are added to or removed from it. * * @param bucket The bucket to watch for changes * @returns The bucket passed in, for convenience */ function useEntities(bucket) { var rerender = useRerender(); /* Re-render any time the bucket changes */ useOnEntityAdded(bucket, rerender); useOnEntityRemoved(bucket, rerender); return bucket; } function useOnEntityAdded(bucket, callback) { useOnceIfBucketVersionChanged(bucket, callback); useIsomorphicLayoutEffect$2(function () { return bucket.onEntityAdded.subscribe(callback); }, [bucket, callback]); } function useOnEntityRemoved(bucket, callback) { useOnceIfBucketVersionChanged(bucket, callback); useIsomorphicLayoutEffect$2(function () { return bucket.onEntityRemoved.subscribe(callback); }, [bucket, callback]); } /** * A utility function that will invoke the specified callback in a layout effect * if the version of the specified bucket has changed since the component was * initially rendered. * * This solves the problem of useEntities and similar callbacks registering their * bucket change callbacks in a layout effect, which can sometimes cause them to * miss entities being created or destroyed within the same render cycle (since * this will often also happen in layout effects.) * * @param bucket The bucket to watch for changes * @param callback The callback to invoke if the bucket version has changed */ function useOnceIfBucketVersionChanged(bucket, callback) { var originalVersion = useMemo(function () { return bucket.version; }, [bucket]); useIsomorphicLayoutEffect$2(function () { if (bucket.version !== originalVersion) callback(); }, [bucket]); } var mergeRefs = function mergeRefs(refs) { return function (v) { refs.forEach(function (ref) { if (typeof ref === "function") ref(v);else if (!!ref) ref.current = v; }); }; }; var _excluded = ["entities"], _excluded2 = ["bucket"], _excluded3 = ["in"]; var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; var createReactAPI = function createReactAPI(world) { var EntityContext = /*#__PURE__*/createContext(null); var useCurrentEntity = function useCurrentEntity() { var entity = useContext(EntityContext); if (!entity) { throw new Error("useCurrentEntity must be called from a child of <Entity>."); } return entity; }; var RawEntity = function RawEntity(_ref, ref) { var givenChildren = _ref.children, givenEntity = _ref.entity; var _useState = useState(function () { return {}; }), _useState2 = _slicedToArray(_useState, 1), defaultEntity = _useState2[0]; var entity = givenEntity || defaultEntity; /* Add the entity to the bucket represented by this component if it isn't already part of it. */ useIsomorphicLayoutEffect(function () { if (world.has(entity)) return; world.add(entity); return function () { world.remove(entity); }; }, [world, entity]); var children = typeof givenChildren === "function" ? givenChildren(entity) : givenChildren; useImperativeHandle(ref, function () { return entity; }); return /*#__PURE__*/React.createElement(EntityContext.Provider, { value: entity }, children); }; /* We need to typecast here because forwardRef doesn't support generics. */ var Entity = /*#__PURE__*/memo( /*#__PURE__*/forwardRef(RawEntity)); var EntitiesInList = function EntitiesInList(_ref2) { var entities = _ref2.entities, props = _objectWithoutProperties(_ref2, _excluded); return /*#__PURE__*/React.createElement(React.Fragment, null, entities.map(function (entity) { return /*#__PURE__*/React.createElement(Entity, _extends({ key: world.id(entity), entity: entity }, props)); })); }; var RawEntitiesInBucket = function RawEntitiesInBucket(_ref3) { var bucket = _ref3.bucket, props = _objectWithoutProperties(_ref3, _excluded2); return /*#__PURE__*/React.createElement(EntitiesInList, _extends({ entities: useEntities(bucket).entities }, props)); }; var EntitiesInBucket = /*#__PURE__*/memo(RawEntitiesInBucket); function Entities(_ref4) { var source = _ref4["in"], props = _objectWithoutProperties(_ref4, _excluded3); if (source instanceof Bucket) { return /*#__PURE__*/React.createElement(EntitiesInBucket, _extends({ bucket: source }, props)); } else { return /*#__PURE__*/React.createElement(EntitiesInList, _extends({ entities: source }, props)); } } var Component = function Component(props) { var entity = useContext(EntityContext); var ref = useRef(null); if (!entity) { throw new Error("<Component> must be a child of <Entity>"); } /* Handle creation and removal of component with a value prop */ useIsomorphicLayoutEffect(function () { world.addComponent(entity, props.name, props.data || ref.current); return function () { return world.removeComponent(entity, props.name); }; }, [entity, props.name]); /* Handle updates to existing component */ useIsomorphicLayoutEffect(function () { if (props.data === undefined) return; entity[props.name] = props.data || ref.current; }, [entity, props.name, props.data, ref.current]); /* Handle setting of child value */ if (props.children) { var child = React.Children.only(props.children); return /*#__PURE__*/React.cloneElement(child, { ref: mergeRefs([child.ref, ref]) }); } return null; }; return { world: world, Component: Component, Entity: Entity, Entities: Entities, useCurrentEntity: useCurrentEntity }; }; export { createReactAPI, createReactAPI as default, useEntities, useOnEntityAdded, useOnEntityRemoved };