Skip to content

Commit 55d4c98

Browse files
christopherthielenmergify[bot]
authored andcommitted
fix: Refactor UIView for compatibility with @uirouter/react-hybrid.
BREAKING CHANGE: UIRouterConsumer now is of type `import { UIRouter } from '@uirouter/core'` instead of `import { UIRouterReact } from '@uirouter/react'`
1 parent 3ed8ce1 commit 55d4c98

File tree

5 files changed

+110
-59
lines changed

5 files changed

+110
-59
lines changed

src/components/UIRouter.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as React from 'react';
33
import { useRef } from 'react';
44

5-
import { UIRouterPlugin, servicesPlugin, PluginFactory } from '@uirouter/core';
5+
import { UIRouter as _UIRouter, UIRouterPlugin, servicesPlugin, PluginFactory } from '@uirouter/core';
66

77
import { UIRouterReact } from '../core';
88
import { ReactStateDeclaration } from '../interface';
@@ -19,8 +19,8 @@ import { ReactStateDeclaration } from '../interface';
1919
* </UIRouterContext.Consumer>
2020
* ```
2121
*/
22-
export const UIRouterContext = React.createContext<UIRouterReact>(undefined);
23-
/** @deprecated use [[useRouter]] or React.useContext(UIRouterContext) */
22+
export const UIRouterContext = React.createContext<_UIRouter>(undefined);
23+
/** @deprecated use [[useRouter]] or React.useContext(UIRouterContext) */
2424
export const UIRouterConsumer = UIRouterContext.Consumer;
2525

2626
export interface UIRouterProps {

src/components/UIView.tsx

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
useEffect,
1313
useMemo,
1414
useState,
15+
Component,
16+
forwardRef,
17+
useImperativeHandle,
18+
useRef,
1519
} from 'react';
1620
import {
1721
ActiveUIView,
@@ -23,16 +27,14 @@ import {
2327
ViewConfig,
2428
ViewContext,
2529
applyPairs,
30+
UIRouter,
2631
} from '@uirouter/core';
2732
import { useParentView } from '../hooks/useParentView';
2833
import { useRouter } from '../hooks/useRouter';
29-
import { useTransitionHook } from '../hooks/useTransitionHook';
3034
import { ReactViewConfig } from '../reactViews';
3135

3236
/** @internalapi */
33-
let id = 0;
34-
/** @hidden */
35-
let keyCounter = 0;
37+
let viewIdCounter = 0;
3638

3739
/** @internalapi */
3840
export interface UIViewAddress {
@@ -114,17 +116,22 @@ function useResolvesWithStringTokens(resolveContext: ResolveContext, injector: U
114116
}
115117

116118
/* @hidden These are the props are passed to the routed component. */
117-
function useChildProps(
119+
function useRoutedComponentProps(
120+
router: UIRouter,
121+
stateName: string,
122+
viewConfig: ViewConfig,
118123
component: React.FunctionComponent<any> | React.ComponentClass<any> | React.ClassicComponentClass<any>,
119124
resolves: TypedMap<any> | {},
120125
className: string,
121126
style: Object,
122-
transition: any,
123-
key: string,
124-
setComponentInstance: (instance: any) => void
125-
): UIViewInjectedProps {
126-
return useMemo(() => {
127-
const componentProps: UIViewInjectedProps & { key: string } = {
127+
transition: any
128+
): UIViewInjectedProps & { key: string } {
129+
const keyCounterRef = useRef(0);
130+
// Always re-mount if the viewConfig changes
131+
const key = useMemo(() => (++keyCounterRef.current).toString(), [viewConfig]);
132+
133+
const baseChildProps = useMemo(
134+
() => ({
128135
// spread each string resolve as a separate prop
129136
...resolves,
130137
// if a className prop was passed to the UIView, forward it
@@ -135,63 +142,99 @@ function useChildProps(
135142
transition,
136143
// this key updates whenever the state is reloaded, causing the component to remount
137144
key,
138-
};
145+
}),
146+
[component, resolves, className, style, transition, key]
147+
);
148+
149+
const maybeRefProp = useUiCanExitClassComponentHook(router, stateName, component);
150+
151+
return useMemo(() => ({ ...baseChildProps, ...maybeRefProp }), [baseChildProps, maybeRefProp]);
152+
}
139153

140-
const maybeComponent = component as any;
141-
if (maybeComponent?.prototype?.render || !!maybeComponent?.render) {
142-
// for class components, add a ref to grab the component instance
143-
return { ...componentProps, ref: setComponentInstance };
154+
function useViewConfig() {
155+
const [viewConfig, setViewConfig] = useState<ReactViewConfig>();
156+
const viewConfigRef = useRef(viewConfig);
157+
viewConfigRef.current = viewConfig;
158+
const configUpdated = (newConfig: ViewConfig) => {
159+
if (newConfig !== viewConfigRef.current) {
160+
setViewConfig(newConfig as ReactViewConfig);
161+
}
162+
};
163+
return { viewConfig, configUpdated };
164+
}
165+
166+
function useReactHybridApi(ref: React.Ref<unknown>, uiViewData: ActiveUIView, uiViewAddress: UIViewAddress) {
167+
const reactHybridApi = useRef({ uiViewData, uiViewAddress });
168+
reactHybridApi.current.uiViewData = uiViewData;
169+
reactHybridApi.current.uiViewAddress = uiViewAddress;
170+
useImperativeHandle(ref, () => reactHybridApi.current);
171+
}
172+
173+
// If a class component is being rendered, wire up its uiCanExit method
174+
// Return a { ref: Ref<ClassComponentInstance> } if passed a component class
175+
// Return an empty object {} if passed anything else
176+
// The returned object should be spread as props onto the child component
177+
function useUiCanExitClassComponentHook(router: UIRouter, stateName: string, maybeComponentClass: any) {
178+
const ref = useRef<any>();
179+
const isComponentClass = maybeComponentClass?.prototype?.render || maybeComponentClass?.render;
180+
const componentInstance = isComponentClass && ref.current;
181+
const uiCanExit = componentInstance?.uiCanExit;
182+
183+
useEffect(() => {
184+
if (uiCanExit) {
185+
const deregister = router.transitionService.onBefore({ exiting: stateName }, uiCanExit.bind(ref.current));
186+
return () => deregister();
144187
} else {
145-
setComponentInstance(undefined);
146-
return componentProps;
188+
return () => undefined;
147189
}
148-
}, [component, resolves, className, style, transition, key]);
190+
}, [uiCanExit]);
191+
192+
return useMemo(() => (isComponentClass ? { ref } : undefined), [isComponentClass, ref]);
149193
}
150194

151-
export function UIView(props: UIViewProps) {
195+
const View = forwardRef(function View(props: UIViewProps, forwardedRef) {
152196
const { children, render, className, style } = props;
153197

154198
const router = useRouter();
155199
const parent = useParentView();
200+
const creationContext = parent.context;
156201

157-
// If a class component is being rendered, this is the component instance
158-
const [componentInstance, setComponentInstance] = useState();
159-
const [viewConfig, setViewConfig] = useState<ReactViewConfig>();
202+
const { viewConfig, configUpdated } = useViewConfig();
160203
const component = useMemo(() => viewConfig?.viewDecl?.component, [viewConfig]);
161204

162205
const name = props.name || '$default';
206+
const fqn = parent.fqn ? parent.fqn + '.' + name : name;
207+
const id = useMemo(() => ++viewIdCounter, []);
163208

164209
// This object contains all the metadata for this UIView
165210
const uiViewData: ActiveUIView = useMemo(() => {
166-
return {
167-
$type: 'react',
168-
id: ++id,
169-
name,
170-
fqn: parent.fqn ? parent.fqn + '.' + name : name,
171-
creationContext: parent.context,
172-
configUpdated: config => setViewConfig(config as ReactViewConfig),
173-
config: viewConfig as ViewConfig,
174-
};
175-
}, [name, parent, viewConfig]);
176-
177-
const viewContext: ViewContext = uiViewData?.config?.viewDecl?.$context;
178-
const uiViewAddress: UIViewAddress = { fqn: uiViewData.fqn, context: viewContext };
179-
const stateName: string = uiViewAddress?.context?.name;
211+
return { $type: 'react', id, name, fqn, creationContext, configUpdated, config: viewConfig as ViewConfig };
212+
}, [id, name, fqn, parent, creationContext, viewConfig]);
213+
const viewContext: ViewContext = viewConfig?.viewDecl?.$context;
214+
const stateName: string = viewContext?.name;
215+
const uiViewAddress: UIViewAddress = { fqn, context: viewContext };
180216
const resolveContext = useMemo(() => (viewConfig ? new ResolveContext(viewConfig.path) : undefined), [viewConfig]);
181217
const injector = useMemo(() => resolveContext?.injector(), [resolveContext]);
182218
const transition = useMemo(() => injector?.get(Transition), [injector]);
183219
const resolves = useResolvesWithStringTokens(resolveContext, injector);
184-
const key = useMemo(() => (++keyCounter).toString(), [viewConfig]);
185-
const childProps = useChildProps(component, resolves, className, style, transition, key, setComponentInstance);
220+
221+
const childProps = useRoutedComponentProps(
222+
router,
223+
stateName,
224+
viewConfig,
225+
component,
226+
resolves,
227+
className,
228+
style,
229+
transition
230+
);
231+
232+
// temporarily expose a ref with an API on it for @uirouter/react-hybrid to use
233+
useReactHybridApi(forwardedRef, uiViewData, uiViewAddress);
186234

187235
// Register/deregister any time the uiViewData changes
188236
useEffect(() => router.viewService.registerUIView(uiViewData), [uiViewData]);
189237

190-
// Handle component class with a 'uiCanExit()' method
191-
const canExitCallback = componentInstance?.uiCanExit;
192-
const hookMatchCriteria = canExitCallback ? { exiting: stateName } : undefined;
193-
useTransitionHook('onBefore', hookMatchCriteria, canExitCallback);
194-
195238
const childElement =
196239
!component && isValidElement(children)
197240
? cloneElement(children, childProps)
@@ -201,13 +244,22 @@ export function UIView(props: UIViewProps) {
201244
const ChildOrRenderFunction =
202245
typeof render !== 'undefined' && component ? render(component, childProps) : childElement;
203246
return <UIViewContext.Provider value={uiViewAddress}>{ChildOrRenderFunction}</UIViewContext.Provider>;
204-
}
247+
});
205248

206-
UIView.displayName = 'UIView';
207-
UIView.__internalViewComponent = UIView;
208-
UIView.propTypes = {
249+
View.displayName = 'UIView';
250+
View.propTypes = {
209251
name: PropTypes.string,
210252
className: PropTypes.string,
211253
style: PropTypes.object,
212254
render: PropTypes.func,
213255
} as ValidationMap<UIViewProps>;
256+
257+
// A wrapper class for react-hybrid to monkey patch
258+
export class UIView extends Component<UIViewProps> {
259+
static displayName = 'UIView';
260+
static propTypes = View.propTypes;
261+
static __internalViewComponent: ComponentType<UIViewProps> = View;
262+
render() {
263+
return <View {...this.props} />;
264+
}
265+
}

src/hooks/useIsActive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/** @packageDocumentation @reactapi @module react_hooks */
22

33
import { useEffect, useMemo, useState } from 'react';
4-
import { UIRouterReact } from '../core';
4+
import { UIRouter } from '@uirouter/core';
55
import { useDeepObjectDiff } from './useDeepObjectDiff';
66
import { useOnStateChanged } from './useOnStateChanged';
77
import { useParentView } from './useParentView';
88
import { useRouter } from './useRouter';
99

1010
/** @hidden */
11-
function checkIfActive(router: UIRouterReact, stateName: string, params: object, relative: string, exact: boolean) {
11+
function checkIfActive(router: UIRouter, stateName: string, params: object, relative: string, exact: boolean) {
1212
return exact
1313
? router.stateService.is(stateName, params, { relative })
1414
: router.stateService.includes(stateName, params, { relative });

src/hooks/useRouter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @packageDocumentation @reactapi @module react_hooks */
22

33
import { useContext } from 'react';
4-
import { UIRouterReact } from '../core';
4+
import { UIRouter } from '@uirouter/core';
55
import { UIRouterContext } from '../components/UIRouter';
66

77
/** @hidden */
@@ -25,7 +25,7 @@ export const UIRouterInstanceUndefinedError = `UIRouter instance is undefined. D
2525
* }
2626
* ```
2727
*/
28-
export function useRouter(): UIRouterReact {
28+
export function useRouter(): UIRouter {
2929
const router = useContext(UIRouterContext);
3030
if (!router) {
3131
throw new Error(UIRouterInstanceUndefinedError);

src/hooks/useSref.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
import * as React from 'react';
44
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
5-
import { isString, StateDeclaration, TransitionOptions } from '@uirouter/core';
5+
import { isString, StateDeclaration, TransitionOptions, UIRouter } from '@uirouter/core';
66
import { UISrefActiveContext } from '../components';
7-
import { UIRouterReact } from '../core';
87
import { useDeepObjectDiff } from './useDeepObjectDiff';
98
import { useParentView } from './useParentView';
109
import { useRouter } from './useRouter';
@@ -18,15 +17,15 @@ export interface LinkProps {
1817
export const IncorrectStateNameTypeError = `The state name passed to useSref must be a string.`;
1918

2019
/** @hidden Gets all StateDeclarations that are registered in the StateRegistry. */
21-
function useListOfAllStates(router: UIRouterReact) {
20+
function useListOfAllStates(router: UIRouter) {
2221
const initial = useMemo(() => router.stateRegistry.get(), []);
2322
const [states, setStates] = useState(initial);
2423
useEffect(() => router.stateRegistry.onStatesChanged(() => setStates(router.stateRegistry.get())), []);
2524
return states;
2625
}
2726

2827
/** @hidden Gets the StateDeclaration that this sref targets */
29-
function useTargetState(router: UIRouterReact, stateName: string, relative: string): StateDeclaration {
28+
function useTargetState(router: UIRouter, stateName: string, relative: string): StateDeclaration {
3029
// Whenever any states are added/removed from the registry, get the target state again
3130
const allStates = useListOfAllStates(router);
3231
return useMemo(() => {

0 commit comments

Comments
 (0)