Skip to content

Commit

Permalink
fix(router): fix router state computed issue
Browse files Browse the repository at this point in the history
  • Loading branch information
unadlib committed Dec 24, 2024
1 parent 20a6c25 commit 258fd8d
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/reactant-module/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ export * from './watch';
export * from './load';
export * from './applyMiddleware';
export * from './getRef';
export { untracked } from './signal';
export { untracked, Signal, signal } from './signal';
46 changes: 45 additions & 1 deletion packages/reactant-router/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
/* eslint-disable consistent-return */
import React, { PropsWithChildren, FunctionComponent } from 'react';
import { PluginModule, injectable, inject, storeKey } from 'reactant-module';
import {
PluginModule,
injectable,
inject,
storeKey,
signalMapKey,
type Service,
signal,
enableAutoComputedKey,
type Signal,
} from 'reactant-module';
import type { ReducersMapObject } from 'redux';
import {
connectRouter,
Expand Down Expand Up @@ -91,6 +101,34 @@ class ReactantRouter extends PluginModule {
`The identifier '${this.stateKey}' has a duplicate name, please reset the option 'stateKey' of 'ReactantRouter' module.`
);
}
if ((this as Service)[enableAutoComputedKey]) {
const signalMap: Record<string, Signal> =
(this as Service)[signalMapKey] ?? {};
signalMap[this.stateKey] = signal({});
const current = signalMap[this.stateKey];
if (!(this as Service)[signalMapKey]) {
Object.defineProperties(this, {
[signalMapKey]: {
enumerable: false,
configurable: false,
writable: false,
value: signalMap,
},
});
}
const reducer = connectRouter(this.history ?? this.defaultHistory);
return Object.assign(reducers, {
// TODO: fix type
[this.stateKey]: (state: any, action: any) => {
const result = reducer(state, action);
if (result !== state) {
// update signal value for auto-computed
current.value = result;
}
return result;
},
});
}
return Object.assign(reducers, {
[this.stateKey]: connectRouter(this.history ?? this.defaultHistory),
});
Expand All @@ -106,6 +144,12 @@ class ReactantRouter extends PluginModule {
);

get router(): RouterState | undefined {
if ((this as Service)[enableAutoComputedKey]) {
const currentRouter = (this as Service)[signalMapKey]?.[this.stateKey];
if (currentRouter) {
return currentRouter!.value as RouterState;
}
}
return this.store?.getState()[this.stateKey];
}

Expand Down
129 changes: 129 additions & 0 deletions packages/reactant-router/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
useConnector,
action,
state,
computed,
watch,
} from 'reactant';
import type { IRouterOptions } from '..';
import {
Expand Down Expand Up @@ -497,3 +499,130 @@ test('`router` module with auto provider with createBrowserHistory', async () =>
);
expect(container.textContent).toBe('HomeDashboard0');
});

test('`router` module with auto computed', () => {
const fn = jest.fn();
const computedFn = jest.fn();

@injectable()
class Count {
@state
num = 0;

@action
increase() {
this.num += 1;
}
}

@injectable()
class DashboardView extends ViewModule {
constructor(public count: Count) {
super();
}

component() {
const num = useConnector(() => this.count.num);
return (
<div onClick={() => this.count.increase()} id="increase">
{num}
</div>
);
}
}
@injectable()
class HomeView extends ViewModule {
text = 'app';

getProps(version: string) {
return {
version: `${this.text} v${version}`,
};
}

component({ version = '0.0.1' }) {
const data = useConnector(() => this.getProps(version));
return <span id="version">{data.version}</span>;
}
}

@injectable()
class AppView extends ViewModule {
constructor(
public homeView: HomeView,
public dashboardView: DashboardView,
public router: Router
) {
super();
watch(this, () => this.currentPath, fn);
}

@computed
get currentPath() {
computedFn(this.router?.currentPath);
return this.router?.currentPath;
}

component() {
const { ConnectedRouter } = this.router;
return (
<ConnectedRouter>
<Switch>
<Route exact path="/">
<this.homeView.component version="0.1.0" />
</Route>
<Route path="/a">
<this.dashboardView.component />
</Route>
<Route path="/b">
<this.dashboardView.component />
</Route>
</Switch>
</ConnectedRouter>
);
}
}

const app = createApp({
modules: [
{
provide: RouterOptions,
useValue: {
autoProvide: false,
createHistory: () => createHashHistory(),
} as IRouterOptions,
},
],
main: AppView,
render,
devOptions: {
reduxDevTools: true,
autoComputed: true,
},
});
act(() => {
app.bootstrap(container);
});
expect(fn.mock.calls[0][0]).toBe('/');
expect(computedFn.mock.calls[0][0]).toBe(undefined);
app.instance.router.push('/a');
expect(fn.mock.calls.slice(-1)[0][0]).toBe('/a');
expect(computedFn.mock.calls.slice(-1)[0][0]).toBe('/a');
app.instance.router.replace('/b');
expect(fn.mock.calls.slice(-1)[0][0]).toBe('/b');
expect(computedFn.mock.calls.slice(-1)[0][0]).toBe('/b');
expect(app.instance.dashboardView.count.num).toBe(0);
expect(computedFn.mock.calls.length).toBe(4);
expect(app.instance.currentPath).toBe('/b');
app.instance.dashboardView.count.increase();
expect(app.instance.dashboardView.count.num).toBe(1);
expect(computedFn.mock.calls.length).toBe(4);
app.instance.router.replace('/');
expect(fn.mock.calls.slice(-1)[0][0]).toBe('/');
expect(computedFn.mock.calls.slice(-1)[0][0]).toBe('/');

expect(computedFn.mock.calls.length).toBe(5);
expect(app.instance.currentPath).toBe('/');
expect(app.instance.currentPath).toBe('/');
expect(computedFn.mock.calls.length).toBe(5);
});

0 comments on commit 258fd8d

Please sign in to comment.