@@ -62,9 +62,31 @@ import useStore from './hooks/use-store';
6262
6363const __CTX_SYM__ = Symbol ( 'Context Symbol' ) ;
6464
65- const reportNonReactUsage : NonReactUsageReport = ( ) => {
66- throw new UsageError ( 'Detected usage outside of this context\'s Provider component tree. Please apply the exported Provider component' ) ;
67- } ;
65+ const connRegister : Record < string , Connection < State > > = { } ;
66+
67+ const ChildMemo : FC < { child : ReactNode } > = ( ( ) => {
68+
69+ const useNodeMemo = ( node : ReactNode ) : ReactNode => {
70+ const nodeRef = useRef ( node ) ;
71+ if ( ! isEqual (
72+ omit ( nodeRef . current , '_owner' ) ,
73+ omit ( node , '_owner' )
74+ ) ) { nodeRef . current = node }
75+ return nodeRef . current ;
76+ } ;
77+
78+ const ChildMemo = memo < { child : ReactNode } > ( ( { child } ) => ( < > { child } </ > ) ) ;
79+ ChildMemo . displayName = 'ObservableContext.Provider.Internal.Guardian.ChildMemo' ;
80+
81+ const Guardian : FC < { child : ReactNode } > = ( { child } ) => (
82+ < ChildMemo child = { useNodeMemo ( child ) } />
83+ ) ;
84+ Guardian . displayName = 'ObservableContext.Provider.Internal.Guardian' ;
85+
86+ return Guardian ;
87+ } ) ( ) ;
88+
89+ const defaultPrehooks : Readonly < Prehooks < State > > = Object . freeze ( { } ) ;
6890
6991export class ObservableContext < T extends State > {
7092 private cxt : React . Context < IStoreInternal > ;
@@ -83,12 +105,63 @@ export class ObservableContext<T extends State> {
83105 get Provider ( ) { return this . provider }
84106}
85107
108+ const reportNonReactUsage : NonReactUsageReport = ( ) => {
109+ throw new UsageError ( 'Detected usage outside of this context\'s Provider component tree. Please apply the exported Provider component' ) ;
110+ } ;
111+
112+ export class UsageError extends Error { } ;
113+
114+ /**
115+ * Provides an HOC function for connecting its WrappedComponent argument to the context store.
116+ *
117+ * The HOC function automatically memoizes any un-memoized WrappedComponent argument.
118+ *
119+ * @param context - Refers to the PublicObservableContext<T> type of the ObservableContext<T>
120+ * @param [selectorMap] - Key:value pairs where `key` => arbitrary key given to a Store.data property holding a state slice and `value` => property path to a state slice used by this component: see examples below. May add a mapping for a certain arbitrary key='state' and value='@@STATE' to indicate a desire to obtain the entire state object and assign to a `state` property of Store.data. A change in any of the referenced properties results in this component render. When using '@@STATE', note that any change within the state object will result in this component render.
121+ * @see {useContext} for selectorMap sample
122+ */
123+ export function connect <
124+ STATE extends State = State ,
125+ SELECTOR_MAP extends SelectorMap = SelectorMap
126+ > (
127+ context : ObservableContext < STATE > ,
128+ selectorMap ? : SELECTOR_MAP
129+ ) {
130+ function connector <
131+ P extends ExtractInjectedProps < STATE , SELECTOR_MAP >
132+ > (
133+ WrappedComponent : ElementType < ConnectProps < P , STATE , SELECTOR_MAP > >
134+ ) : ConnectedComponent < P > ;
135+ function connector <
136+ P extends ExtractInjectedProps < STATE , SELECTOR_MAP >
137+ > (
138+ WrappedComponent : NamedExoticComponent < ConnectProps < P , STATE , SELECTOR_MAP > >
139+ ) : ConnectedComponent < P > ;
140+ function connector <
141+ P extends ExtractInjectedProps < STATE , SELECTOR_MAP >
142+ > ( WrappedComponent ) : ConnectedComponent < P > {
143+ const Wrapped = (
144+ ! ( isPlainObject ( WrappedComponent ) && 'compare' in WrappedComponent as { } )
145+ ? memo ( WrappedComponent )
146+ : WrappedComponent
147+ ) ;
148+ const ConnectedComponent = memo ( forwardRef <
149+ P extends IProps ? P [ "ref" ] : never ,
150+ Omit < P , "ref" >
151+ > ( ( ownProps , ref ) => {
152+ const store = useContext ( context , selectorMap ) ;
153+ return ( < Wrapped { ...store } { ...ownProps } ref = { ref } /> ) ;
154+ } ) ) ;
155+ ConnectedComponent . displayName = 'ObservableContext.Connected' ;
156+ return ConnectedComponent as ConnectedComponent < P > ;
157+ }
158+ return connector ;
159+ }
160+
86161export function createContext < T extends State = State > ( ) {
87162 return new ObservableContext < T > ( ) ;
88163} ;
89164
90- const connRegister : Record < string , Connection < State > > = { } ;
91-
92165function getConnectionFrom < T extends State > (
93166 connKey : MutableRefObject < string > ,
94167 cache : Immutable < Partial < T > >
@@ -105,6 +178,86 @@ function getConnectionFrom<T extends State>(
105178 return connRegister [ connKey . current ] as Connection < T > ;
106179}
107180
181+ function getStoreRef < T extends State > (
182+ store : StoreInternal < T > ,
183+ connection : Connection < T >
184+ ) : StoreRef < T > {
185+ return {
186+ getState : ( propertyPaths = [ ] ) => {
187+ if ( ! propertyPaths . length || propertyPaths . indexOf ( constants . FULL_STATE_SELECTOR ) !== - 1 ) {
188+ return connection . get ( constants . GLOBAL_SELECTOR ) [ constants . GLOBAL_SELECTOR ] ;
189+ }
190+ const data = connection . get ( ...propertyPaths ) ;
191+ const state = { } as T ;
192+ for ( const d in data ) { set ( state , d , data [ d ] ) }
193+ return mkReadonly ( state ) ;
194+ } ,
195+ resetState : ( propertyPaths = [ ] ) => store . resetState ( connection , propertyPaths ) ,
196+ setState : changes => store . setState ( connection , changes ) ,
197+ subscribe : store . subscribe
198+ } ;
199+ }
200+
201+ function makeObservable < T extends State = State > ( Provider : Provider < IStore > ) {
202+ const Observable : ObservableProvider < T > = forwardRef <
203+ StoreRef < T > ,
204+ ProviderProps < T >
205+ > ( ( {
206+ children = null ,
207+ prehooks = defaultPrehooks as Readonly < Prehooks < T > > ,
208+ storage = null ,
209+ value
210+ } , storeRef ) => {
211+ const connKey = useRef < string > ( ) ;
212+ const store = useStore ( prehooks , value , storage ) ;
213+ const [ connection ] = useState ( ( ) => getConnectionFrom ( connKey , store . cache ) ) ;
214+ useImperativeHandle ( storeRef , ( ) => ( {
215+ ...( storeRef as MutableRefObject < StoreRef < T > > ) ?. current ?? { } ,
216+ ...getStoreRef ( store , connection )
217+ } ) , [ ( storeRef as MutableRefObject < StoreRef < T > > ) ?. current ] ) ;
218+ useEffect ( ( ) => ( ) => {
219+ connection . disconnect ( ) ;
220+ delete connRegister [ connKey . current ] ;
221+ connKey . current = undefined ;
222+ } , [ ] ) ;
223+ return (
224+ < Provider value = { store } >
225+ { memoizeImmediateChildTree ( children ) }
226+ </ Provider >
227+ ) ;
228+ } ) ;
229+ Observable . displayName = 'ObservableContext.Provider' ;
230+ return Observable ;
231+ }
232+
233+ export function mkReadonly ( v : any ) {
234+ if ( Object . isFrozen ( v ) ) { return v }
235+ if ( isPlainObject ( v ) || Array . isArray ( v ) ) {
236+ for ( const k in v ) { v [ k ] = mkReadonly ( v [ k ] ) }
237+ }
238+ return Object . freeze ( v ) ;
239+ }
240+
241+ function memoizeImmediateChildTree ( children : ReactNode ) : ReactNode {
242+ return Children . map ( children , _child => {
243+ let child = _child as JSX . Element ;
244+ if ( ! ( child ?. type ) || ( // skip memoized or non element(s)
245+ typeof child . type === 'object' &&
246+ child . type . $$typeof ?. toString ( ) === 'Symbol(react.memo)'
247+ ) ) {
248+ return child ;
249+ }
250+ if ( child . props ?. children ) {
251+ child = cloneElement (
252+ child ,
253+ omit ( child . props , 'children' ) ,
254+ memoizeImmediateChildTree ( child . props . children )
255+ ) ;
256+ }
257+ return ( < ChildMemo child = { child } /> ) ;
258+ } ) ;
259+ }
260+
108261/**
109262 * Actively monitors the store and triggers component re-render if any of the watched keys in the state objects changes
110263 *
@@ -260,158 +413,4 @@ export function useContext<
260413 ( ) => ( { data, resetState, setState } ) ,
261414 [ data ]
262415 ) ;
263- } ;
264-
265- /**
266- * Provides an HOC function for connecting its WrappedComponent argument to the context store.
267- *
268- * The HOC function automatically memoizes any un-memoized WrappedComponent argument.
269- *
270- * @param context - Refers to the PublicObservableContext<T> type of the ObservableContext<T>
271- * @param [selectorMap] - Key:value pairs where `key` => arbitrary key given to a Store.data property holding a state slice and `value` => property path to a state slice used by this component: see examples below. May add a mapping for a certain arbitrary key='state' and value='@@STATE' to indicate a desire to obtain the entire state object and assign to a `state` property of Store.data. A change in any of the referenced properties results in this component render. When using '@@STATE', note that any change within the state object will result in this component render.
272- * @see {useContext} for selectorMap sample
273- */
274- export function connect <
275- STATE extends State = State ,
276- SELECTOR_MAP extends SelectorMap = SelectorMap
277- > (
278- context : ObservableContext < STATE > ,
279- selectorMap ? : SELECTOR_MAP
280- ) {
281- function connector <
282- P extends ExtractInjectedProps < STATE , SELECTOR_MAP >
283- > (
284- WrappedComponent : ElementType < ConnectProps < P , STATE , SELECTOR_MAP > >
285- ) : ConnectedComponent < P > ;
286- function connector <
287- P extends ExtractInjectedProps < STATE , SELECTOR_MAP >
288- > (
289- WrappedComponent : NamedExoticComponent < ConnectProps < P , STATE , SELECTOR_MAP > >
290- ) : ConnectedComponent < P > ;
291- function connector <
292- P extends ExtractInjectedProps < STATE , SELECTOR_MAP >
293- > ( WrappedComponent ) : ConnectedComponent < P > {
294-
295- const Wrapped = (
296- ! ( isPlainObject ( WrappedComponent ) && 'compare' in WrappedComponent as { } )
297- ? memo ( WrappedComponent )
298- : WrappedComponent
299- ) ;
300-
301- const ConnectedComponent = memo ( forwardRef <
302- P extends IProps ? P [ "ref" ] : never ,
303- Omit < P , "ref" >
304- > ( ( ownProps , ref ) => {
305- const store = useContext ( context , selectorMap ) ;
306- return ( < Wrapped { ...store } { ...ownProps } ref = { ref } /> ) ;
307- } ) ) ;
308- ConnectedComponent . displayName = 'ObservableContext.Connected' ;
309-
310- return ConnectedComponent as ConnectedComponent < P > ;
311-
312- }
313-
314- return connector ;
315-
316- }
317-
318- export class UsageError extends Error { } ;
319-
320- const ChildMemo : FC < { child : ReactNode } > = ( ( ) => {
321-
322- const useNodeMemo = ( node : ReactNode ) : ReactNode => {
323- const nodeRef = useRef ( node ) ;
324- if ( ! isEqual (
325- omit ( nodeRef . current , '_owner' ) ,
326- omit ( node , '_owner' )
327- ) ) { nodeRef . current = node }
328- return nodeRef . current ;
329- } ;
330-
331- const ChildMemo = memo < { child : ReactNode } > ( ( { child } ) => ( < > { child } </ > ) ) ;
332- ChildMemo . displayName = 'ObservableContext.Provider.Internal.Guardian.ChildMemo' ;
333-
334- const Guardian : FC < { child : ReactNode } > = ( { child } ) => (
335- < ChildMemo child = { useNodeMemo ( child ) } />
336- ) ;
337- Guardian . displayName = 'ObservableContext.Provider.Internal.Guardian' ;
338-
339- return Guardian ;
340- } ) ( ) ;
341-
342- const defaultPrehooks : Readonly < Prehooks < State > > = Object . freeze ( { } ) ;
343-
344- function makeObservable < T extends State = State > ( Provider : Provider < IStore > ) {
345- const Observable : ObservableProvider < T > = forwardRef <
346- StoreRef < T > ,
347- ProviderProps < T >
348- > ( ( {
349- children = null ,
350- prehooks = defaultPrehooks ,
351- storage = null ,
352- value
353- } , storeRef ) => {
354- const connKey = useRef < string > ( ) ;
355- const store = useStore ( prehooks , value , storage ) ;
356- const [ connection ] = useState ( ( ) => getConnectionFrom ( connKey , store . cache ) ) ;
357- const getState = useCallback < StoreRef < T > [ "getState" ] > (
358- ( propertyPaths = [ ] ) => {
359- if ( ! propertyPaths . length || propertyPaths . indexOf ( constants . FULL_STATE_SELECTOR ) !== - 1 ) {
360- return connection . get ( constants . GLOBAL_SELECTOR ) [ constants . GLOBAL_SELECTOR ] as T ;
361- }
362- const data = connection . get ( ...propertyPaths ) ;
363- const state = { } as T ;
364- for ( const d in data ) { set ( state , d , data [ d ] ) }
365- return mkReadonly ( state ) ;
366- } ,
367- [ ]
368- ) ;
369- useImperativeHandle ( storeRef , ( ) => ( {
370- ...( storeRef as MutableRefObject < StoreRef < T > > ) ?. current ?? { } ,
371- getState,
372- resetState : propertyPaths => store . resetState ( connection , propertyPaths ) ,
373- setState : changes => store . setState ( connection , changes ) ,
374- subscribe : store . subscribe
375- } ) , [ ( storeRef as MutableRefObject < StoreRef < T > > ) ?. current ] ) ;
376- useEffect ( ( ) => ( ) => {
377- connection . disconnect ( ) ;
378- delete connRegister [ connKey . current ] ;
379- connKey . current = undefined ;
380- } , [ ] ) ;
381- return (
382- < Provider value = { store } >
383- { memoizeImmediateChildTree ( children ) }
384- </ Provider >
385- ) ;
386- } ) ;
387- Observable . displayName = 'ObservableContext.Provider' ;
388- return Observable ;
389- }
390-
391- export function mkReadonly ( v : any ) {
392- if ( Object . isFrozen ( v ) ) { return v }
393- if ( isPlainObject ( v ) || Array . isArray ( v ) ) {
394- for ( const k in v ) { v [ k ] = mkReadonly ( v [ k ] ) }
395- }
396- return Object . freeze ( v ) ;
397- }
398-
399- function memoizeImmediateChildTree ( children : ReactNode ) : ReactNode {
400- return Children . map ( children , _child => {
401- let child = _child as JSX . Element ;
402- if ( ! ( child ?. type ) || ( // skip memoized or non element(s)
403- typeof child . type === 'object' &&
404- child . type . $$typeof ?. toString ( ) === 'Symbol(react.memo)'
405- ) ) {
406- return child ;
407- }
408- if ( child . props ?. children ) {
409- child = cloneElement (
410- child ,
411- omit ( child . props , 'children' ) ,
412- memoizeImmediateChildTree ( child . props . children )
413- ) ;
414- }
415- return ( < ChildMemo child = { child } /> ) ;
416- } ) ;
417416}
0 commit comments