Skip to content

Commit fba5321

Browse files
christopherthielenmergify[bot]
authored andcommitted
fix(useSref): Call go() with the actual user provided state string
Pass fully qualified state name to parent UISrefActive
1 parent 4c6b0f0 commit fba5321

File tree

2 files changed

+72
-68
lines changed

2 files changed

+72
-68
lines changed

src/hooks/__tests__/useSref.test.tsx

Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,37 @@
11
import * as React from 'react';
22
import { act } from 'react-dom/test-utils';
33
import { makeTestRouter, muteConsoleErrors } from '../../__tests__/util';
4+
import { UIView } from '../../components';
5+
import { ReactStateDeclaration } from '../../interface';
46
import { useSref } from '../useSref';
57
import { UISrefActive, UISrefActiveContext } from '../../components/UISrefActive';
68

79
const state = { name: 'state', url: '/state' };
810
const state2 = { name: 'state2', url: '/state2' };
911
const state3 = { name: 'state3', url: '/state3/:param' };
1012

13+
const Link = ({ to, params = undefined, children = undefined }) => {
14+
const sref = useSref(to, params);
15+
return <a {...sref}>{children}</a>;
16+
};
17+
1118
describe('useUiSref', () => {
1219
let { router, routerGo, mountInRouter } = makeTestRouter([]);
1320
beforeEach(() => ({ router, routerGo, mountInRouter } = makeTestRouter([state, state2, state3])));
1421

1522
it('throws if to is not a string', () => {
16-
const Component = () => {
17-
const sref = useSref(5 as any, {});
18-
return <a {...sref} />;
19-
};
20-
2123
muteConsoleErrors();
22-
expect(() => mountInRouter(<Component />)).toThrow(/must be a string/);
24+
expect(() => mountInRouter(<Link to={5} />)).toThrow(/must be a string/);
2325
});
2426

2527
it('returns an href for the target state', () => {
26-
const Component = () => {
27-
const uiSref = useSref('state2', {});
28-
return <a {...uiSref}>state2</a>;
29-
};
30-
const wrapper = mountInRouter(<Component />);
28+
const wrapper = mountInRouter(<Link to="state2">state2</Link>);
3129
expect(wrapper.html()).toBe('<a href="/state2">state2</a>');
3230
});
3331

3432
it('returns an onClick function which activates the target state', () => {
3533
const spy = jest.spyOn(router.stateService, 'go');
36-
const Component = () => {
37-
const uiSref = useSref('state');
38-
return <a {...uiSref}>state</a>;
39-
};
40-
41-
const wrapper = mountInRouter(<Component />);
34+
const wrapper = mountInRouter(<Link to="state" />);
4235
wrapper.simulate('click');
4336

4437
expect(spy).toBeCalledTimes(1);
@@ -47,50 +40,30 @@ describe('useUiSref', () => {
4740

4841
it('returns an onClick function which respects defaultPrevented', () => {
4942
const spy = jest.spyOn(router.stateService, 'go');
50-
const Component = () => {
51-
const uiSref = useSref('state');
52-
return <a {...uiSref}>state</a>;
53-
};
54-
55-
const wrapper = mountInRouter(<Component />);
43+
const wrapper = mountInRouter(<Link to="state" />);
5644
wrapper.simulate('click', { defaultPrevented: true });
5745

5846
expect(spy).not.toHaveBeenCalled();
5947
});
6048

6149
it('updates the href when the stateName changes', () => {
62-
const Component = props => {
63-
const uiSref = useSref(props.state);
64-
return <a {...uiSref} />;
65-
};
66-
67-
const wrapper = mountInRouter(<Component state="state" />);
50+
const wrapper = mountInRouter(<Link to="state" />);
6851
expect(wrapper.html()).toBe('<a href="/state"></a>');
6952

70-
wrapper.setProps({ state: 'state2' });
53+
wrapper.setProps({ to: 'state2' });
7154
expect(wrapper.html()).toBe('<a href="/state2"></a>');
7255
});
7356

7457
it('updates the href when the params changes', () => {
75-
const State3Link = props => {
76-
const sref = useSref('state3', { param: props.param });
77-
return <a {...sref} />;
78-
};
79-
80-
const wrapper = mountInRouter(<State3Link param="123" />);
58+
const wrapper = mountInRouter(<Link to="state3" params={{ param: '123' }} />);
8159
expect(wrapper.html()).toBe('<a href="/state3/123"></a>');
8260

83-
wrapper.setProps({ param: '456' });
61+
wrapper.setProps({ params: { param: '456' } });
8462
expect(wrapper.html()).toBe('<a href="/state3/456"></a>');
8563
});
8664

8765
it('updates href when the registered state is swapped out', async () => {
88-
const State2Link = () => {
89-
const sref = useSref('state2');
90-
return <a {...sref} />;
91-
};
92-
93-
const wrapper = mountInRouter(<State2Link />);
66+
const wrapper = mountInRouter(<Link to="state2" />);
9467
expect(wrapper.html()).toBe('<a href="/state2"></a>');
9568
act(() => {
9669
router.stateRegistry.deregister('state2');
@@ -113,16 +86,27 @@ describe('useUiSref', () => {
11386
});
11487
router.stateRegistry.register({ name: 'future.**', url: '/future', lazyLoad: lazyLoadFutureStates });
11588

89+
const wrapper = mountInRouter(<Link to="future.child" />);
90+
expect(wrapper.html()).toBe('<a href="/future"></a>');
91+
92+
await routerGo('future.child');
93+
expect(wrapper.update().html()).toBe('<a href="/future/child"></a>');
94+
});
95+
96+
it('calls go() with the actual string provided (not with the name of the matched future state)', async () => {
97+
router.stateRegistry.register({ name: 'future.**', url: '/future' });
98+
11699
const Link = props => {
117100
const sref = useSref('future.child', { param: props.param });
118101
return <a {...sref} />;
119102
};
120103

104+
spyOn(router.stateService, 'go');
121105
const wrapper = mountInRouter(<Link />);
122-
expect(wrapper.html()).toBe('<a href="/future"></a>');
123106

124-
await routerGo('future.child');
125-
expect(wrapper.update().html()).toBe('<a href="/future/child"></a>');
107+
wrapper.find('a').simulate('click');
108+
expect(router.stateService.go).toHaveBeenCalledTimes(1);
109+
expect(router.stateService.go).toHaveBeenCalledWith('future.child', expect.anything(), expect.anything());
126110
});
127111

128112
it('participates in parent UISrefActive component active state', async () => {
@@ -131,7 +115,7 @@ describe('useUiSref', () => {
131115
const State2Link = props => {
132116
const sref = useSref('state2');
133117
return (
134-
<a {...sref} className={props.className}>
118+
<a {...sref} {...props}>
135119
state2
136120
</a>
137121
);
@@ -145,6 +129,24 @@ describe('useUiSref', () => {
145129
expect(wrapper.html()).toBe('<a href="/state2" class="active">state2</a>');
146130
});
147131

132+
it('provides a fully qualified state name to the parent UISrefActive', async () => {
133+
const spy = jest.fn();
134+
const State2Link = () => {
135+
return (
136+
<UISrefActiveContext.Provider value={spy}>
137+
<Link to={'.child'} />
138+
</UISrefActiveContext.Provider>
139+
);
140+
};
141+
142+
router.stateRegistry.register({ name: 'parent', component: State2Link } as ReactStateDeclaration);
143+
router.stateRegistry.register({ name: 'parent.child', component: UIView } as ReactStateDeclaration);
144+
145+
await routerGo('parent');
146+
mountInRouter(<UIView />);
147+
expect(spy).toHaveBeenCalledWith('parent.child', expect.anything());
148+
});
149+
148150
it('participates in grandparent UISrefActive component active state', async () => {
149151
await routerGo('state2');
150152

src/hooks/useSref.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { isString, StateDeclaration, TransitionOptions } from '@uirouter/core';
2+
import { isString, RawParams, StateDeclaration, TransitionOptions } from '@uirouter/core';
33
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
44
import { StateRegistry, UIRouterReact, UIViewAddress } from '../index';
55
import { UISrefActiveContext, UIViewContext } from '../components';
@@ -14,26 +14,29 @@ export interface LinkProps {
1414
/** @hidden */
1515
export const IncorrectStateNameTypeError = `The state name passed to useSref must be a string.`;
1616

17+
/** Gets all StateDeclarations that are registered in the StateRegistry. */
1718
function useListOfAllStates(router: UIRouterReact) {
1819
const initial = useMemo(() => router.stateRegistry.get(), []);
1920
const [states, setStates] = useState(initial);
2021
useEffect(() => router.stateRegistry.onStatesChanged(() => setStates(router.stateRegistry.get())), []);
2122
return states;
2223
}
2324

24-
// The state the sref was defined in. Used to resolve relative srefs.
25+
/** Gets the StateDeclaration that this sref was defined in. Used to resolve relative refs. */
2526
function useSrefContextState(router: UIRouterReact): StateDeclaration {
2627
const parentUIViewAddress = useContext(UIViewContext);
2728
return useMemo(() => {
2829
return parentUIViewAddress ? parentUIViewAddress.context : router.stateRegistry.root();
2930
}, [parentUIViewAddress, router]);
3031
}
3132

32-
// returns the StateDeclaration that this sref targets, or undefined
33+
/** Gets the StateDeclaration that this sref targets */
3334
function useTargetState(router: UIRouterReact, stateName: string, context: StateDeclaration): StateDeclaration {
34-
// Whenever allStates changes, get the target state again
35+
// Whenever any states are added/removed from the registry, get the target state again
3536
const allStates = useListOfAllStates(router);
36-
return useMemo(() => router.stateRegistry.get(stateName, context), [stateName, context, allStates]);
37+
return useMemo(() => {
38+
return router.stateRegistry.get(stateName, context);
39+
}, [router, stateName, context, allStates]);
3740
}
3841

3942
/**
@@ -45,28 +48,23 @@ function useTargetState(router: UIRouterReact, stateName: string, context: State
4548
* @param params Any parameter values
4649
* @param options Transition options
4750
*/
48-
export function useSref(relativeStateName: string, params: object = {}, options: TransitionOptions = {}): LinkProps {
49-
if (!isString(relativeStateName)) {
51+
export function useSref(stateName: string, params: object = {}, options: TransitionOptions = {}): LinkProps {
52+
if (!isString(stateName)) {
5053
throw new Error(IncorrectStateNameTypeError);
5154
}
5255

5356
const router = useRouter();
54-
const parentUISrefActiveAddStateInfo = useContext(UISrefActiveContext);
55-
const { stateService } = router;
56-
57-
const contextState: StateDeclaration = useSrefContextState(router);
58-
const targetState: StateDeclaration = useTargetState(router, relativeStateName, contextState);
59-
const stateName = targetState && targetState.name;
60-
// Keep a memoized reference to the initial params object until the nested values actually change
57+
// memoize the params object until the nested values actually change so they can be used as deps
6158
const paramsMemo = useMemo(() => params, [useDeepObjectDiff(params)]);
6259

63-
const optionsMemo = useMemo(() => {
64-
return { relative: contextState, inherit: true, ...options };
65-
}, [contextState, options]);
66-
60+
const contextState: StateDeclaration = useSrefContextState(router);
61+
const optionsMemo = useMemo(() => ({ relative: contextState, inherit: true, ...options }), [contextState, options]);
62+
const targetState = useTargetState(router, stateName, contextState);
63+
// Update href when the target StateDeclaration changes (in case the the state definition itself changes)
64+
// This is necessary to handle things like future states
6765
const href = useMemo(() => {
68-
return router.stateService.href(stateName, paramsMemo, optionsMemo);
69-
}, [router, stateName, paramsMemo, optionsMemo]);
66+
return router.stateService.href(stateName, params, options);
67+
}, [router, stateName, params, options, targetState]);
7068

7169
const onClick = useCallback(
7270
(e: React.MouseEvent) => {
@@ -78,7 +76,11 @@ export function useSref(relativeStateName: string, params: object = {}, options:
7876
[router, stateName, paramsMemo, optionsMemo]
7977
);
8078

81-
useEffect(() => parentUISrefActiveAddStateInfo(stateName, paramsMemo), [stateName, paramsMemo]);
79+
// Participate in any parent UISrefActive
80+
const parentUISrefActiveAddStateInfo = useContext(UISrefActiveContext);
81+
useEffect(() => {
82+
return parentUISrefActiveAddStateInfo(targetState && targetState.name, paramsMemo);
83+
}, [targetState, paramsMemo]);
8284

8385
return { onClick, href };
8486
}

0 commit comments

Comments
 (0)