@tramvai/state 1.84.2 → 1.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,7 @@
1
- export declare const Provider: ({ context, children }: any) => JSX.Element;
1
+ import type { ReactElement } from 'react';
2
+ import type { ConsumerContext, ServerState } from './types';
3
+ export declare const Provider: ({ context, children, serverState, }: {
4
+ context: ConsumerContext;
5
+ children: ReactElement;
6
+ serverState?: ServerState;
7
+ }) => JSX.Element;
@@ -1,2 +1,3 @@
1
1
  import type { ConsumerContext } from './types';
2
2
  export declare const ConnectContext: import("react").Context<ConsumerContext>;
3
+ export declare const ServerStateContext: import("react").Context<any>;
@@ -1 +1,2 @@
1
1
  export { Action, ConsumerContext } from '@tramvai/types-actions-state-context';
2
+ export declare type ServerState = any;
package/lib/index.es.js CHANGED
@@ -4,15 +4,18 @@ import identity from '@tinkoff/utils/function/identity';
4
4
  import pick from '@tinkoff/utils/object/pick';
5
5
  import shallowEqual from '@tinkoff/utils/is/shallowEqual';
6
6
  import strictEqual from '@tinkoff/utils/is/strictEqual';
7
+ import { jsx } from 'react/jsx-runtime';
8
+ import noop from '@tinkoff/utils/function/noop';
7
9
  import hoistStatics from 'hoist-non-react-statics';
8
10
  import invariant from 'invariant';
9
- import React, { createContext, useContext, useMemo, useRef, useReducer, useCallback } from 'react';
11
+ import React, { createContext, useContext, useMemo, useRef, useCallback } from 'react';
10
12
  import { isValidElementType } from 'react-is';
13
+ import { useSyncExternalStore } from 'use-sync-external-store/shim';
11
14
  import isArray from '@tinkoff/utils/is/array';
12
15
  import { useShallowEqual, useIsomorphicLayoutEffect } from '@tinkoff/react-hooks';
13
16
  export { useIsomorphicLayoutEffect } from '@tinkoff/react-hooks';
14
- import noop from '@tinkoff/utils/function/noop';
15
17
  import toArray from '@tinkoff/utils/array/toArray';
18
+ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
16
19
  import always from '@tinkoff/utils/function/always';
17
20
  import mapObject from '@tinkoff/utils/object/map';
18
21
  import isPlainObject from '@tinkoff/utils/is/plainObject';
@@ -652,6 +655,7 @@ class Subscription {
652
655
  }
653
656
 
654
657
  const ConnectContext = createContext(null);
658
+ const ServerStateContext = createContext(null);
655
659
 
656
660
  const useConsumerContext = () => {
657
661
  const context = useContext(ConnectContext);
@@ -674,10 +678,9 @@ function useActions(actions) {
674
678
 
675
679
  function useStore(reducer) {
676
680
  const context = useConsumerContext();
681
+ const serverState = useContext(ServerStateContext);
677
682
  const reducerRef = useRef(reducer);
678
683
  const addedReducerRef = useRef(null);
679
- const unsubscribeRef = useRef(noop);
680
- const [, forceRender] = useReducer((s) => s + 1, 0);
681
684
  // если текущий редьюсер не зарегистрирован в диспетчере,
682
685
  // регистрируем его вручную, что бы гарантировать работоспособность `context.getState(reducer)`,
683
686
  // и сохраняем в `addedReducerRef`, что бы удалить при unmount
@@ -685,26 +688,14 @@ function useStore(reducer) {
685
688
  context.registerStore(reducer);
686
689
  addedReducerRef.current = reducer.storeName;
687
690
  }
688
- const stateRef = useRef(context.getState(reducer));
689
- useIsomorphicLayoutEffect(() => {
690
- const subscribe = (updatedState) => {
691
- // если состояние текущего редьюсера изменилось,
692
- // обновляем локальное состояние и ререндерим компонент
693
- if (stateRef.current !== updatedState) {
694
- stateRef.current = updatedState;
695
- forceRender();
696
- }
697
- };
698
- // сразу обновляем состояние
699
- subscribe(context.getState(reducer));
700
- // и подписываемся на обновления редьюсера
701
- unsubscribeRef.current = context.subscribe(reducer, subscribe);
691
+ const subscribe = useCallback((reactUpdate) => {
692
+ const unsubscribe = context.subscribe(reducer, reactUpdate);
702
693
  // заменяем текущий редьюсер
703
694
  reducerRef.current = reducer;
704
695
  return () => {
705
696
  // гарантируем отписку от обновлений текущего редьюсера,
706
697
  // при анмаунте компонента
707
- unsubscribeRef.current();
698
+ unsubscribe();
708
699
  // если текущий редьюсер был зарегистрирован в диспетчере в этом хуке,
709
700
  // удаляем его из диспетчера
710
701
  if (addedReducerRef.current) {
@@ -713,7 +704,7 @@ function useStore(reducer) {
713
704
  }
714
705
  };
715
706
  }, [reducer, context]);
716
- return stateRef.current;
707
+ return useSyncExternalStore(subscribe, () => context.getState(reducer), serverState ? () => serverState[reducer.storeName] : () => context.getState(reducer));
717
708
  }
718
709
 
719
710
  const contextExecution = typeof window !== 'undefined' ? window : global;
@@ -731,23 +722,29 @@ const schedule = scheduling();
731
722
  function useSelector(storesOrStore, selector, equalityFn = shallowEqual) {
732
723
  invariant(selector, `You must pass a selector to useSelectors`);
733
724
  const context = useConsumerContext();
734
- const [, forceRender] = useReducer((s) => s + 1, 0);
725
+ const serverState = useContext(ServerStateContext);
735
726
  const renderIsScheduled = useRef(false);
736
727
  const storesRef = useShallowEqual(storesOrStore);
737
728
  const subscription = useMemo(() => new Subscription(toArray(storesRef).map(context.getStore)), [storesRef, context]);
738
729
  const latestSubscriptionCallbackError = useRef();
739
- const latestSelector = useRef(selector);
740
- const latestSelectedState = useRef();
730
+ const subscribe = useCallback((reactUpdate) => {
731
+ subscription.setOnStateChange(() => {
732
+ if (!renderIsScheduled.current) {
733
+ renderIsScheduled.current = true;
734
+ schedule(() => {
735
+ reactUpdate();
736
+ renderIsScheduled.current = false;
737
+ });
738
+ }
739
+ });
740
+ subscription.trySubscribe();
741
+ return () => {
742
+ return subscription.tryUnsubscribe();
743
+ };
744
+ }, [subscription]);
741
745
  let selectedState;
742
746
  try {
743
- if (!latestSelectedState.current ||
744
- selector !== latestSelector.current ||
745
- latestSubscriptionCallbackError.current) {
746
- selectedState = selector(context.getState());
747
- }
748
- else {
749
- selectedState = latestSelectedState.current;
750
- }
747
+ selectedState = useSyncExternalStoreWithSelector(subscribe, context.getState, serverState ? () => serverState : context.getState, selector, equalityFn);
751
748
  }
752
749
  catch (err) {
753
750
  let errorMessage = `An error occured while selecting the store state: ${err.message}.`;
@@ -757,46 +754,8 @@ function useSelector(storesOrStore, selector, equalityFn = shallowEqual) {
757
754
  throw new Error(errorMessage);
758
755
  }
759
756
  useIsomorphicLayoutEffect(() => {
760
- latestSelector.current = selector;
761
- latestSelectedState.current = selectedState;
762
757
  latestSubscriptionCallbackError.current = undefined;
763
758
  });
764
- useIsomorphicLayoutEffect(() => {
765
- let didUnsubscribe = false;
766
- function checkForUpdates() {
767
- renderIsScheduled.current = false;
768
- if (didUnsubscribe) {
769
- return;
770
- }
771
- try {
772
- const newSelectedState = latestSelector.current(context.getState());
773
- if (equalityFn(newSelectedState, latestSelectedState.current)) {
774
- return;
775
- }
776
- latestSelectedState.current = newSelectedState;
777
- }
778
- catch (err) {
779
- // we ignore all errors here, since when the component
780
- // is re-rendered, the selectors are called again, and
781
- // will throw again, if neither props nor store state
782
- // changed
783
- latestSubscriptionCallbackError.current = err;
784
- }
785
- forceRender();
786
- }
787
- subscription.setOnStateChange(() => {
788
- if (!renderIsScheduled.current) {
789
- renderIsScheduled.current = true;
790
- schedule(checkForUpdates);
791
- }
792
- });
793
- subscription.trySubscribe();
794
- checkForUpdates();
795
- return () => {
796
- didUnsubscribe = true;
797
- return subscription.tryUnsubscribe();
798
- };
799
- }, [subscription]);
800
759
  return selectedState;
801
760
  }
802
761
 
@@ -807,8 +766,6 @@ const useStoreSelector = (store, selector) => {
807
766
  return useSelector(store, memoizedSelector);
808
767
  };
809
768
 
810
- // Define some constant arrays just to avoid re-creating these
811
- const EMPTY_ARRAY = [];
812
769
  const stringifyComponent = (Comp) => {
813
770
  try {
814
771
  return JSON.stringify(Comp);
@@ -817,11 +774,6 @@ const stringifyComponent = (Comp) => {
817
774
  return String(Comp);
818
775
  }
819
776
  };
820
- function storeStateUpdatesReducer(state, action) {
821
- const [, updateCount] = state;
822
- return [action.payload, updateCount + 1];
823
- }
824
- const initStateUpdates = () => [null, 0];
825
777
  function connectAdvanced(
826
778
  /*
827
779
  selectorFactory is a func that is responsible for returning the selector function used to
@@ -875,10 +827,6 @@ pure = true,
875
827
  wrappedComponentName,
876
828
  WrappedComponent,
877
829
  };
878
- // If we aren't running in "pure" mode, we don't want to memoize values.
879
- // To avoid conditionally calling hooks, we fall back to a tiny wrapper
880
- // that just executes the given callback immediately.
881
- const usePureOnlyMemo = pure ? useMemo : (callback) => callback();
882
830
  const ConnectFunction = (props) => {
883
831
  const [forwardedRef, wrapperProps] = useMemo(() => {
884
832
  // Distinguish between actual "data" props that were passed to the wrapper component,
@@ -890,6 +838,7 @@ pure = true,
890
838
  }, [props]);
891
839
  // Retrieve the store and ancestor subscription via context, if available
892
840
  const contextValue = useConsumerContext();
841
+ const serverState = useContext(ServerStateContext);
893
842
  invariant(Boolean(contextValue), `Could not find context in ` +
894
843
  `"${displayName}". Either wrap the root component in a <Provider>, ` +
895
844
  `or pass a custom React context provider to <Provider> and the corresponding ` +
@@ -909,124 +858,34 @@ pure = true,
909
858
  subscription.trySubscribe();
910
859
  }
911
860
  }, [subscription]);
912
- // We need to force this wrapper component to re-render whenever a Redux store update
913
- // causes a change to the calculated child component props (or we caught an error in mapState)
914
- const [[previousStateUpdateResult], forceComponentUpdateDispatch] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates);
915
- // Propagate any mapState/mapDispatch errors upwards
916
- if (previousStateUpdateResult && previousStateUpdateResult.error) {
917
- throw previousStateUpdateResult.error;
918
- }
919
- // Set up refs to coordinate values between the subscription effect and the render logic
920
- const lastChildProps = useRef();
921
- const lastWrapperProps = useRef(wrapperProps);
922
- const childPropsFromStoreUpdate = useRef();
923
861
  const renderIsScheduled = useRef(false);
924
- const actualChildProps = usePureOnlyMemo(() => {
925
- // Tricky logic here:
926
- // - This render may have been triggered by a Redux store update that produced new child props
927
- // - However, we may have gotten new wrapper props after that
928
- // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
929
- // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
930
- // So, we'll use the child props from store update only if the wrapper props are the same as last time.
931
- if (childPropsFromStoreUpdate.current && wrapperProps === lastWrapperProps.current) {
932
- return childPropsFromStoreUpdate.current;
933
- }
934
- // TODO We're reading the store directly in render() here. Bad idea?
935
- // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
936
- // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
937
- // to determine what the child props should be.
862
+ const actualChildPropsSelector = useCallback(() => {
938
863
  return childPropsSelector(contextValue.getState(), wrapperProps);
939
- }, [contextValue, previousStateUpdateResult, wrapperProps]);
940
- // We need this to execute synchronously every time we re-render. However, React warns
941
- // about useLayoutEffect in SSR, so we try to detect environment and fall back to
942
- // just useEffect instead to avoid the warning, since neither will run anyway.
943
- useIsomorphicLayoutEffect(() => {
944
- // We want to capture the wrapper props and child props we used for later comparisons
945
- lastWrapperProps.current = wrapperProps;
946
- lastChildProps.current = actualChildProps;
947
- // If the render was from a store update, clear out that reference and cascade the subscriber update
948
- if (childPropsFromStoreUpdate.current) {
949
- childPropsFromStoreUpdate.current = undefined;
950
- }
951
- });
864
+ }, [contextValue, wrapperProps, childPropsSelector]);
952
865
  // Our re-subscribe logic only runs when the store/subscription setup changes
953
- useIsomorphicLayoutEffect(() => {
954
- // If we're not subscribed to the store, nothing to do here
955
- if (!subscription)
956
- return;
957
- // Capture values for checking if and when this component unmounts
958
- let didUnsubscribe = false;
959
- let lastThrownError = null;
960
- // We'll run this callback every time a store subscription update propagates to this component
961
- const checkForUpdates = () => {
962
- renderIsScheduled.current = false;
963
- if (didUnsubscribe) {
964
- // Don't run stale listeners.
965
- // Redux doesn't guarantee unsubscriptions happen until next dispatch.
966
- return;
967
- }
968
- const latestStoreState = contextValue.getState();
969
- let newChildProps;
970
- let error;
971
- try {
972
- // Actually run the selector with the most recent store state and wrapper props
973
- // to determine what the child props should be
974
- newChildProps = childPropsSelector(latestStoreState, lastWrapperProps.current);
975
- }
976
- catch (e) {
977
- error = e;
978
- lastThrownError = e;
979
- }
980
- if (!error) {
981
- lastThrownError = null;
982
- }
983
- // If the child props haven't changed, nothing to do here - cascade the subscription update
984
- if (newChildProps !== lastChildProps.current) {
985
- // Save references to the new child props. Note that we track the "child props from store update"
986
- // as a ref instead of a useState/useReducer because we need a way to determine if that value has
987
- // been processed. If this went into useState/useReducer, we couldn't clear out the value without
988
- // forcing another re-render, which we don't want.
989
- lastChildProps.current = newChildProps;
990
- childPropsFromStoreUpdate.current = newChildProps;
991
- // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
992
- forceComponentUpdateDispatch({
993
- type: 'STORE_UPDATED',
994
- payload: {
995
- latestStoreState,
996
- error,
997
- },
998
- });
999
- }
1000
- };
1001
- // Actually subscribe to the nearest connected ancestor (or store)
866
+ const subscribe = useCallback((reactUpdate) => {
867
+ if (!subscription) {
868
+ return noop;
869
+ }
1002
870
  subscription.setOnStateChange(() => {
1003
871
  if (!renderIsScheduled.current) {
1004
872
  renderIsScheduled.current = true;
1005
- schedule(checkForUpdates);
873
+ schedule(() => {
874
+ reactUpdate();
875
+ renderIsScheduled.current = false;
876
+ });
1006
877
  }
1007
878
  });
1008
- // Pull data from the store after first render in case the store has
1009
- // changed since we began.
1010
- checkForUpdates();
1011
- const unsubscribeWrapper = () => {
1012
- didUnsubscribe = true;
1013
- subscription.tryUnsubscribe();
1014
- if (lastThrownError) {
1015
- // It's possible that we caught an error due to a bad mapState function, but the
1016
- // parent re-rendered without this component and we're about to unmount.
1017
- // This shouldn't happen as long as we do top-down subscriptions correctly, but
1018
- // if we ever do those wrong, this throw will surface the error in our tests.
1019
- // In that case, throw the error from here so it doesn't get lost.
1020
- throw lastThrownError;
1021
- }
879
+ return () => {
880
+ return subscription.tryUnsubscribe();
1022
881
  };
1023
- return unsubscribeWrapper;
1024
- }, [contextValue, subscription, childPropsSelector]);
882
+ }, [subscription]);
883
+ const actualChildProps = useSyncExternalStore(subscribe, actualChildPropsSelector, serverState ? childPropsSelector(serverState, wrapperProps) : actualChildPropsSelector);
1025
884
  // Now that all that's done, we can finally try to actually render the child component.
1026
885
  // We memoize the elements for the rendered child component as an optimization.
1027
886
  const renderedWrappedComponent = useMemo(
1028
887
  // eslint-disable-next-line react/jsx-props-no-spreading
1029
- () => React.createElement(WrappedComponent, Object.assign({}, actualChildProps, { ref: forwardedRef })), [forwardedRef, actualChildProps]);
888
+ () => jsx(WrappedComponent, Object.assign({}, actualChildProps, { ref: forwardedRef }), void 0), [forwardedRef, actualChildProps]);
1030
889
  return renderedWrappedComponent;
1031
890
  };
1032
891
  // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
@@ -1036,7 +895,7 @@ pure = true,
1036
895
  if (forwardRef) {
1037
896
  const forwarded = React.forwardRef(function forwardConnectRef(props, ref) {
1038
897
  // eslint-disable-next-line react/jsx-props-no-spreading
1039
- return React.createElement(Connect, Object.assign({}, props, { forwardedRef: ref }));
898
+ return jsx(Connect, Object.assign({}, props, { forwardedRef: ref }), void 0);
1040
899
  });
1041
900
  forwarded.displayName = displayName;
1042
901
  forwarded.WrappedComponent = WrappedComponent;
@@ -1300,8 +1159,8 @@ function finalPropsSelectorFactory(context, { initMapStateToProps, initMapContex
1300
1159
  return selectorFactory(mapStateToProps, mapContextToProps, mergeProps, context, options);
1301
1160
  }
1302
1161
 
1303
- const Provider = ({ context, children }) => {
1304
- return React.createElement(ConnectContext.Provider, { value: context }, children);
1162
+ const Provider = ({ context, children, serverState, }) => {
1163
+ return (jsx(ConnectContext.Provider, Object.assign({ value: context }, { children: jsx(ServerStateContext.Provider, Object.assign({ value: serverState }, { children: children }), void 0) }), void 0));
1305
1164
  };
1306
1165
 
1307
1166
  /*
package/lib/index.js CHANGED
@@ -8,14 +8,17 @@ var identity = require('@tinkoff/utils/function/identity');
8
8
  var pick = require('@tinkoff/utils/object/pick');
9
9
  var shallowEqual = require('@tinkoff/utils/is/shallowEqual');
10
10
  var strictEqual = require('@tinkoff/utils/is/strictEqual');
11
+ var jsxRuntime = require('react/jsx-runtime');
12
+ var noop = require('@tinkoff/utils/function/noop');
11
13
  var hoistStatics = require('hoist-non-react-statics');
12
14
  var invariant = require('invariant');
13
15
  var React = require('react');
14
16
  var reactIs = require('react-is');
17
+ var shim = require('use-sync-external-store/shim');
15
18
  var isArray = require('@tinkoff/utils/is/array');
16
19
  var reactHooks = require('@tinkoff/react-hooks');
17
- var noop = require('@tinkoff/utils/function/noop');
18
20
  var toArray = require('@tinkoff/utils/array/toArray');
21
+ var withSelector = require('use-sync-external-store/shim/with-selector');
19
22
  var always = require('@tinkoff/utils/function/always');
20
23
  var mapObject = require('@tinkoff/utils/object/map');
21
24
  var isPlainObject = require('@tinkoff/utils/is/plainObject');
@@ -29,11 +32,11 @@ var identity__default = /*#__PURE__*/_interopDefaultLegacy(identity);
29
32
  var pick__default = /*#__PURE__*/_interopDefaultLegacy(pick);
30
33
  var shallowEqual__default = /*#__PURE__*/_interopDefaultLegacy(shallowEqual);
31
34
  var strictEqual__default = /*#__PURE__*/_interopDefaultLegacy(strictEqual);
35
+ var noop__default = /*#__PURE__*/_interopDefaultLegacy(noop);
32
36
  var hoistStatics__default = /*#__PURE__*/_interopDefaultLegacy(hoistStatics);
33
37
  var invariant__default = /*#__PURE__*/_interopDefaultLegacy(invariant);
34
38
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
35
39
  var isArray__default = /*#__PURE__*/_interopDefaultLegacy(isArray);
36
- var noop__default = /*#__PURE__*/_interopDefaultLegacy(noop);
37
40
  var toArray__default = /*#__PURE__*/_interopDefaultLegacy(toArray);
38
41
  var always__default = /*#__PURE__*/_interopDefaultLegacy(always);
39
42
  var mapObject__default = /*#__PURE__*/_interopDefaultLegacy(mapObject);
@@ -674,6 +677,7 @@ class Subscription {
674
677
  }
675
678
 
676
679
  const ConnectContext = React.createContext(null);
680
+ const ServerStateContext = React.createContext(null);
677
681
 
678
682
  const useConsumerContext = () => {
679
683
  const context = React.useContext(ConnectContext);
@@ -696,10 +700,9 @@ function useActions(actions) {
696
700
 
697
701
  function useStore(reducer) {
698
702
  const context = useConsumerContext();
703
+ const serverState = React.useContext(ServerStateContext);
699
704
  const reducerRef = React.useRef(reducer);
700
705
  const addedReducerRef = React.useRef(null);
701
- const unsubscribeRef = React.useRef(noop__default["default"]);
702
- const [, forceRender] = React.useReducer((s) => s + 1, 0);
703
706
  // если текущий редьюсер не зарегистрирован в диспетчере,
704
707
  // регистрируем его вручную, что бы гарантировать работоспособность `context.getState(reducer)`,
705
708
  // и сохраняем в `addedReducerRef`, что бы удалить при unmount
@@ -707,26 +710,14 @@ function useStore(reducer) {
707
710
  context.registerStore(reducer);
708
711
  addedReducerRef.current = reducer.storeName;
709
712
  }
710
- const stateRef = React.useRef(context.getState(reducer));
711
- reactHooks.useIsomorphicLayoutEffect(() => {
712
- const subscribe = (updatedState) => {
713
- // если состояние текущего редьюсера изменилось,
714
- // обновляем локальное состояние и ререндерим компонент
715
- if (stateRef.current !== updatedState) {
716
- stateRef.current = updatedState;
717
- forceRender();
718
- }
719
- };
720
- // сразу обновляем состояние
721
- subscribe(context.getState(reducer));
722
- // и подписываемся на обновления редьюсера
723
- unsubscribeRef.current = context.subscribe(reducer, subscribe);
713
+ const subscribe = React.useCallback((reactUpdate) => {
714
+ const unsubscribe = context.subscribe(reducer, reactUpdate);
724
715
  // заменяем текущий редьюсер
725
716
  reducerRef.current = reducer;
726
717
  return () => {
727
718
  // гарантируем отписку от обновлений текущего редьюсера,
728
719
  // при анмаунте компонента
729
- unsubscribeRef.current();
720
+ unsubscribe();
730
721
  // если текущий редьюсер был зарегистрирован в диспетчере в этом хуке,
731
722
  // удаляем его из диспетчера
732
723
  if (addedReducerRef.current) {
@@ -735,7 +726,7 @@ function useStore(reducer) {
735
726
  }
736
727
  };
737
728
  }, [reducer, context]);
738
- return stateRef.current;
729
+ return shim.useSyncExternalStore(subscribe, () => context.getState(reducer), serverState ? () => serverState[reducer.storeName] : () => context.getState(reducer));
739
730
  }
740
731
 
741
732
  const contextExecution = typeof window !== 'undefined' ? window : global;
@@ -753,23 +744,29 @@ const schedule = scheduling();
753
744
  function useSelector(storesOrStore, selector, equalityFn = shallowEqual__default["default"]) {
754
745
  invariant__default["default"](selector, `You must pass a selector to useSelectors`);
755
746
  const context = useConsumerContext();
756
- const [, forceRender] = React.useReducer((s) => s + 1, 0);
747
+ const serverState = React.useContext(ServerStateContext);
757
748
  const renderIsScheduled = React.useRef(false);
758
749
  const storesRef = reactHooks.useShallowEqual(storesOrStore);
759
750
  const subscription = React.useMemo(() => new Subscription(toArray__default["default"](storesRef).map(context.getStore)), [storesRef, context]);
760
751
  const latestSubscriptionCallbackError = React.useRef();
761
- const latestSelector = React.useRef(selector);
762
- const latestSelectedState = React.useRef();
752
+ const subscribe = React.useCallback((reactUpdate) => {
753
+ subscription.setOnStateChange(() => {
754
+ if (!renderIsScheduled.current) {
755
+ renderIsScheduled.current = true;
756
+ schedule(() => {
757
+ reactUpdate();
758
+ renderIsScheduled.current = false;
759
+ });
760
+ }
761
+ });
762
+ subscription.trySubscribe();
763
+ return () => {
764
+ return subscription.tryUnsubscribe();
765
+ };
766
+ }, [subscription]);
763
767
  let selectedState;
764
768
  try {
765
- if (!latestSelectedState.current ||
766
- selector !== latestSelector.current ||
767
- latestSubscriptionCallbackError.current) {
768
- selectedState = selector(context.getState());
769
- }
770
- else {
771
- selectedState = latestSelectedState.current;
772
- }
769
+ selectedState = withSelector.useSyncExternalStoreWithSelector(subscribe, context.getState, serverState ? () => serverState : context.getState, selector, equalityFn);
773
770
  }
774
771
  catch (err) {
775
772
  let errorMessage = `An error occured while selecting the store state: ${err.message}.`;
@@ -779,46 +776,8 @@ function useSelector(storesOrStore, selector, equalityFn = shallowEqual__default
779
776
  throw new Error(errorMessage);
780
777
  }
781
778
  reactHooks.useIsomorphicLayoutEffect(() => {
782
- latestSelector.current = selector;
783
- latestSelectedState.current = selectedState;
784
779
  latestSubscriptionCallbackError.current = undefined;
785
780
  });
786
- reactHooks.useIsomorphicLayoutEffect(() => {
787
- let didUnsubscribe = false;
788
- function checkForUpdates() {
789
- renderIsScheduled.current = false;
790
- if (didUnsubscribe) {
791
- return;
792
- }
793
- try {
794
- const newSelectedState = latestSelector.current(context.getState());
795
- if (equalityFn(newSelectedState, latestSelectedState.current)) {
796
- return;
797
- }
798
- latestSelectedState.current = newSelectedState;
799
- }
800
- catch (err) {
801
- // we ignore all errors here, since when the component
802
- // is re-rendered, the selectors are called again, and
803
- // will throw again, if neither props nor store state
804
- // changed
805
- latestSubscriptionCallbackError.current = err;
806
- }
807
- forceRender();
808
- }
809
- subscription.setOnStateChange(() => {
810
- if (!renderIsScheduled.current) {
811
- renderIsScheduled.current = true;
812
- schedule(checkForUpdates);
813
- }
814
- });
815
- subscription.trySubscribe();
816
- checkForUpdates();
817
- return () => {
818
- didUnsubscribe = true;
819
- return subscription.tryUnsubscribe();
820
- };
821
- }, [subscription]);
822
781
  return selectedState;
823
782
  }
824
783
 
@@ -829,8 +788,6 @@ const useStoreSelector = (store, selector) => {
829
788
  return useSelector(store, memoizedSelector);
830
789
  };
831
790
 
832
- // Define some constant arrays just to avoid re-creating these
833
- const EMPTY_ARRAY = [];
834
791
  const stringifyComponent = (Comp) => {
835
792
  try {
836
793
  return JSON.stringify(Comp);
@@ -839,11 +796,6 @@ const stringifyComponent = (Comp) => {
839
796
  return String(Comp);
840
797
  }
841
798
  };
842
- function storeStateUpdatesReducer(state, action) {
843
- const [, updateCount] = state;
844
- return [action.payload, updateCount + 1];
845
- }
846
- const initStateUpdates = () => [null, 0];
847
799
  function connectAdvanced(
848
800
  /*
849
801
  selectorFactory is a func that is responsible for returning the selector function used to
@@ -897,10 +849,6 @@ pure = true,
897
849
  wrappedComponentName,
898
850
  WrappedComponent,
899
851
  };
900
- // If we aren't running in "pure" mode, we don't want to memoize values.
901
- // To avoid conditionally calling hooks, we fall back to a tiny wrapper
902
- // that just executes the given callback immediately.
903
- const usePureOnlyMemo = pure ? React.useMemo : (callback) => callback();
904
852
  const ConnectFunction = (props) => {
905
853
  const [forwardedRef, wrapperProps] = React.useMemo(() => {
906
854
  // Distinguish between actual "data" props that were passed to the wrapper component,
@@ -912,6 +860,7 @@ pure = true,
912
860
  }, [props]);
913
861
  // Retrieve the store and ancestor subscription via context, if available
914
862
  const contextValue = useConsumerContext();
863
+ const serverState = React.useContext(ServerStateContext);
915
864
  invariant__default["default"](Boolean(contextValue), `Could not find context in ` +
916
865
  `"${displayName}". Either wrap the root component in a <Provider>, ` +
917
866
  `or pass a custom React context provider to <Provider> and the corresponding ` +
@@ -931,124 +880,34 @@ pure = true,
931
880
  subscription.trySubscribe();
932
881
  }
933
882
  }, [subscription]);
934
- // We need to force this wrapper component to re-render whenever a Redux store update
935
- // causes a change to the calculated child component props (or we caught an error in mapState)
936
- const [[previousStateUpdateResult], forceComponentUpdateDispatch] = React.useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates);
937
- // Propagate any mapState/mapDispatch errors upwards
938
- if (previousStateUpdateResult && previousStateUpdateResult.error) {
939
- throw previousStateUpdateResult.error;
940
- }
941
- // Set up refs to coordinate values between the subscription effect and the render logic
942
- const lastChildProps = React.useRef();
943
- const lastWrapperProps = React.useRef(wrapperProps);
944
- const childPropsFromStoreUpdate = React.useRef();
945
883
  const renderIsScheduled = React.useRef(false);
946
- const actualChildProps = usePureOnlyMemo(() => {
947
- // Tricky logic here:
948
- // - This render may have been triggered by a Redux store update that produced new child props
949
- // - However, we may have gotten new wrapper props after that
950
- // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
951
- // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
952
- // So, we'll use the child props from store update only if the wrapper props are the same as last time.
953
- if (childPropsFromStoreUpdate.current && wrapperProps === lastWrapperProps.current) {
954
- return childPropsFromStoreUpdate.current;
955
- }
956
- // TODO We're reading the store directly in render() here. Bad idea?
957
- // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
958
- // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
959
- // to determine what the child props should be.
884
+ const actualChildPropsSelector = React.useCallback(() => {
960
885
  return childPropsSelector(contextValue.getState(), wrapperProps);
961
- }, [contextValue, previousStateUpdateResult, wrapperProps]);
962
- // We need this to execute synchronously every time we re-render. However, React warns
963
- // about useLayoutEffect in SSR, so we try to detect environment and fall back to
964
- // just useEffect instead to avoid the warning, since neither will run anyway.
965
- reactHooks.useIsomorphicLayoutEffect(() => {
966
- // We want to capture the wrapper props and child props we used for later comparisons
967
- lastWrapperProps.current = wrapperProps;
968
- lastChildProps.current = actualChildProps;
969
- // If the render was from a store update, clear out that reference and cascade the subscriber update
970
- if (childPropsFromStoreUpdate.current) {
971
- childPropsFromStoreUpdate.current = undefined;
972
- }
973
- });
886
+ }, [contextValue, wrapperProps, childPropsSelector]);
974
887
  // Our re-subscribe logic only runs when the store/subscription setup changes
975
- reactHooks.useIsomorphicLayoutEffect(() => {
976
- // If we're not subscribed to the store, nothing to do here
977
- if (!subscription)
978
- return;
979
- // Capture values for checking if and when this component unmounts
980
- let didUnsubscribe = false;
981
- let lastThrownError = null;
982
- // We'll run this callback every time a store subscription update propagates to this component
983
- const checkForUpdates = () => {
984
- renderIsScheduled.current = false;
985
- if (didUnsubscribe) {
986
- // Don't run stale listeners.
987
- // Redux doesn't guarantee unsubscriptions happen until next dispatch.
988
- return;
989
- }
990
- const latestStoreState = contextValue.getState();
991
- let newChildProps;
992
- let error;
993
- try {
994
- // Actually run the selector with the most recent store state and wrapper props
995
- // to determine what the child props should be
996
- newChildProps = childPropsSelector(latestStoreState, lastWrapperProps.current);
997
- }
998
- catch (e) {
999
- error = e;
1000
- lastThrownError = e;
1001
- }
1002
- if (!error) {
1003
- lastThrownError = null;
1004
- }
1005
- // If the child props haven't changed, nothing to do here - cascade the subscription update
1006
- if (newChildProps !== lastChildProps.current) {
1007
- // Save references to the new child props. Note that we track the "child props from store update"
1008
- // as a ref instead of a useState/useReducer because we need a way to determine if that value has
1009
- // been processed. If this went into useState/useReducer, we couldn't clear out the value without
1010
- // forcing another re-render, which we don't want.
1011
- lastChildProps.current = newChildProps;
1012
- childPropsFromStoreUpdate.current = newChildProps;
1013
- // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
1014
- forceComponentUpdateDispatch({
1015
- type: 'STORE_UPDATED',
1016
- payload: {
1017
- latestStoreState,
1018
- error,
1019
- },
1020
- });
1021
- }
1022
- };
1023
- // Actually subscribe to the nearest connected ancestor (or store)
888
+ const subscribe = React.useCallback((reactUpdate) => {
889
+ if (!subscription) {
890
+ return noop__default["default"];
891
+ }
1024
892
  subscription.setOnStateChange(() => {
1025
893
  if (!renderIsScheduled.current) {
1026
894
  renderIsScheduled.current = true;
1027
- schedule(checkForUpdates);
895
+ schedule(() => {
896
+ reactUpdate();
897
+ renderIsScheduled.current = false;
898
+ });
1028
899
  }
1029
900
  });
1030
- // Pull data from the store after first render in case the store has
1031
- // changed since we began.
1032
- checkForUpdates();
1033
- const unsubscribeWrapper = () => {
1034
- didUnsubscribe = true;
1035
- subscription.tryUnsubscribe();
1036
- if (lastThrownError) {
1037
- // It's possible that we caught an error due to a bad mapState function, but the
1038
- // parent re-rendered without this component and we're about to unmount.
1039
- // This shouldn't happen as long as we do top-down subscriptions correctly, but
1040
- // if we ever do those wrong, this throw will surface the error in our tests.
1041
- // In that case, throw the error from here so it doesn't get lost.
1042
- throw lastThrownError;
1043
- }
901
+ return () => {
902
+ return subscription.tryUnsubscribe();
1044
903
  };
1045
- return unsubscribeWrapper;
1046
- }, [contextValue, subscription, childPropsSelector]);
904
+ }, [subscription]);
905
+ const actualChildProps = shim.useSyncExternalStore(subscribe, actualChildPropsSelector, serverState ? childPropsSelector(serverState, wrapperProps) : actualChildPropsSelector);
1047
906
  // Now that all that's done, we can finally try to actually render the child component.
1048
907
  // We memoize the elements for the rendered child component as an optimization.
1049
908
  const renderedWrappedComponent = React.useMemo(
1050
909
  // eslint-disable-next-line react/jsx-props-no-spreading
1051
- () => React__default["default"].createElement(WrappedComponent, Object.assign({}, actualChildProps, { ref: forwardedRef })), [forwardedRef, actualChildProps]);
910
+ () => jsxRuntime.jsx(WrappedComponent, Object.assign({}, actualChildProps, { ref: forwardedRef }), void 0), [forwardedRef, actualChildProps]);
1052
911
  return renderedWrappedComponent;
1053
912
  };
1054
913
  // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
@@ -1058,7 +917,7 @@ pure = true,
1058
917
  if (forwardRef) {
1059
918
  const forwarded = React__default["default"].forwardRef(function forwardConnectRef(props, ref) {
1060
919
  // eslint-disable-next-line react/jsx-props-no-spreading
1061
- return React__default["default"].createElement(Connect, Object.assign({}, props, { forwardedRef: ref }));
920
+ return jsxRuntime.jsx(Connect, Object.assign({}, props, { forwardedRef: ref }), void 0);
1062
921
  });
1063
922
  forwarded.displayName = displayName;
1064
923
  forwarded.WrappedComponent = WrappedComponent;
@@ -1322,8 +1181,8 @@ function finalPropsSelectorFactory(context, { initMapStateToProps, initMapContex
1322
1181
  return selectorFactory(mapStateToProps, mapContextToProps, mergeProps, context, options);
1323
1182
  }
1324
1183
 
1325
- const Provider = ({ context, children }) => {
1326
- return React__default["default"].createElement(ConnectContext.Provider, { value: context }, children);
1184
+ const Provider = ({ context, children, serverState, }) => {
1185
+ return (jsxRuntime.jsx(ConnectContext.Provider, Object.assign({ value: context }, { children: jsxRuntime.jsx(ServerStateContext.Provider, Object.assign({ value: serverState }, { children: children }), void 0) }), void 0));
1327
1186
  };
1328
1187
 
1329
1188
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/state",
3
- "version": "1.84.2",
3
+ "version": "1.90.1",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -20,11 +20,12 @@
20
20
  "dependencies": {
21
21
  "@tinkoff/react-hooks": "0.0.24",
22
22
  "@tinkoff/utils": "^2.1.2",
23
- "@tramvai/types-actions-state-context": "1.84.2",
23
+ "@tramvai/types-actions-state-context": "1.90.1",
24
24
  "@types/hoist-non-react-statics": "^3.3.1",
25
25
  "invariant": "^2.2.4",
26
26
  "react-is": ">=17",
27
- "tslib": "^2.0.3"
27
+ "tslib": "^2.0.3",
28
+ "use-sync-external-store": "^1.0.0"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "hoist-non-react-statics": "^3.3.1",
@@ -36,6 +37,7 @@
36
37
  "@reatom/core": "^1.1.5",
37
38
  "@types/invariant": "^2.2.31",
38
39
  "@types/react-is": "^17.0.0",
40
+ "@types/use-sync-external-store": "^0.0.3",
39
41
  "redux": "^4.0.5"
40
42
  },
41
43
  "gitHead": "8e826a214c87b188fc4d254cdd8f2a2b2c55f3a8",