Skip to content

Commit 1dcdd16

Browse files
authored
Merge pull request #74 from webKrafters/refactor
ext. resetState default Behavior and refactor
2 parents 949248b + 3abb9af commit 1dcdd16

4 files changed

Lines changed: 180 additions & 163 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
"test:watch": "eslint --fix && jest --updateSnapshot --watchAll"
110110
},
111111
"types": "dist/index.d.ts",
112-
"version": "6.0.0-rc.4.1",
112+
"version": "6.0.0-rc.5",
113113
"dependencies": {
114114
"@webkrafters/auto-immutable": "^2.0.0-rc.10"
115115
}

src/main/index.test.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,7 @@ describe( 'ReactObservableContext', () => {
10901090
expect( ( renderCount.current.TallyDisplay as RenderCountField ).value ).toBe( 1 );
10911091
const currentState = storeRef.current!.getState();
10921092
storeRef.current!.setState({ price: 45 });
1093-
const newState = { ...state, price: 45 };
1093+
let newState = { ...state, price: 45 };
10941094
await wait(() => {});
10951095
await new Promise( resolve => setTimeout( resolve, 50 ) );
10961096
expect( ( renderCount.current.TallyDisplay as RenderCountField ).value ).toBe( 2 );
@@ -1100,9 +1100,27 @@ describe( 'ReactObservableContext', () => {
11001100
await wait(() => {});
11011101
await new Promise( resolve => setTimeout( resolve, 50 ) );
11021102
expect( ( renderCount.current.TallyDisplay as RenderCountField ).value ).toBe( 3 );
1103-
const currentState2 = storeRef.current!.getState();
1103+
let currentState2 = storeRef.current!.getState();
11041104
expect( currentState2 ).toStrictEqual( state );
11051105
expect( currentState2 ).toStrictEqual( currentState );
1106+
// alter internal state to ready for default reset feature
1107+
storeRef.current!.setState({ price: 300 });
1108+
currentState2 = storeRef.current!.getState();
1109+
await wait(() => {});
1110+
await new Promise( resolve => setTimeout( resolve, 50 ) );
1111+
newState = { ...state, price: 300 };
1112+
expect( currentState2 ).toEqual( newState );
1113+
expect( currentState2 ).not.toEqual( state );
1114+
expect( ( renderCount.current.TallyDisplay as RenderCountField ).value ).toBe( 4 );
1115+
// default reset results in no-operation
1116+
storeRef.current!.resetState();
1117+
const currentState3 = storeRef.current!.getState();
1118+
await wait(() => {});
1119+
await new Promise( resolve => setTimeout( resolve, 50 ) );
1120+
expect( ( renderCount.current.TallyDisplay as RenderCountField ).value ).toBe( 4 );
1121+
expect( newState ).toEqual( currentState3 );
1122+
expect( state ).not.toEqual( currentState3 );
1123+
expect( currentState2 ).toBe( currentState3 );
11061124
cleanupPerfTest();
11071125
}, 3e4 );
11081126
test( 'subscribes to state changes', async () => {

src/main/index.tsx

Lines changed: 158 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,31 @@ import useStore from './hooks/use-store';
6262

6363
const __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

6991
export 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+
86161
export function createContext<T extends State = State>() {
87162
return new ObservableContext<T>();
88163
};
89164

90-
const connRegister : Record<string, Connection<State>> = {};
91-
92165
function 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

Comments
 (0)