Skip to content

Commit be542c7

Browse files
committed
example: redux-dynamic-modules
1 parent 01b9a53 commit be542c7

File tree

16 files changed

+5720
-0
lines changed

16 files changed

+5720
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
extends: [
3+
"plugin:@typescript-eslint/recommended",
4+
"plugin:react/recommended",
5+
"plugin:prettier/recommended",
6+
"prettier/@typescript-eslint"
7+
],
8+
plugins: ["react-hooks"],
9+
parser: "@typescript-eslint/parser",
10+
parserOptions: {
11+
ecmaFeatures: {
12+
jsx: true
13+
},
14+
project: "./tsconfig.json"
15+
},
16+
rules: {
17+
"react-hooks/rules-of-hooks": "error",
18+
"react-hooks/exhaustive-deps": "warn",
19+
"@typescript-eslint/explicit-function-return-type": [
20+
"warn",
21+
{ allowExpressions: true, allowTypedFunctionExpressions: true }
22+
],
23+
"react/prop-types": "disabled"
24+
}
25+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "redux-dynamic-modules",
3+
"version": "0.1.0",
4+
"description": "Example for redux-dynamic-modules",
5+
"author": "MasuqaT <occar@hotmail.co.jp>",
6+
"license": "MIT",
7+
"scripts": {
8+
"start": "webpack-dev-server --mode=development",
9+
"build": "webpack --mode=production",
10+
"style": "eslint --ext .ts,.tsx,.js,.jsx src/"
11+
},
12+
"private": true,
13+
"dependencies": {
14+
"react": "^16.8.6",
15+
"react-dom": "^16.8.6",
16+
"react-redux": "^6.0.1",
17+
"react-router": "^5.0.0",
18+
"react-router-dom": "^5.0.0",
19+
"redux": "^4.0.1",
20+
"redux-dynamic-modules": "^3.5.0",
21+
"redux-dynamic-modules-saga": "^3.5.0",
22+
"redux-saga": "^1.0.2",
23+
"typescript-fsa": "^3.0.0-beta-2"
24+
},
25+
"devDependencies": {
26+
"@types/react": "^16.8.12",
27+
"@types/react-dom": "^16.8.3",
28+
"@types/react-redux": "^7.0.6",
29+
"@types/react-router": "^4.4.5",
30+
"@types/react-router-dom": "^4.3.1",
31+
"@typescript-eslint/eslint-plugin": "^1.6.0",
32+
"eslint": "^5.16.0",
33+
"eslint-config-prettier": "^4.1.0",
34+
"eslint-plugin-prettier": "^3.0.1",
35+
"eslint-plugin-react": "^7.12.4",
36+
"eslint-plugin-react-hooks": "^1.6.0",
37+
"html-webpack-plugin": "^3.2.0",
38+
"prettier": "^1.16.4",
39+
"ts-loader": "^5.3.3",
40+
"typescript": "^3.4.2",
41+
"webpack": "^4.29.6",
42+
"webpack-cli": "^3.3.0",
43+
"webpack-dev-server": "^3.2.1"
44+
}
45+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from "react";
2+
import {
3+
BrowserRouter as Router,
4+
Link,
5+
Redirect,
6+
Route,
7+
Switch
8+
} from "react-router-dom";
9+
import IndexPage from "./pages/IndexPage";
10+
import { DynamicModuleLoader } from "redux-dynamic-modules";
11+
import {
12+
AuthModuleOwnState,
13+
getAuthModule,
14+
loginOperation
15+
} from "./modules/auth";
16+
import { connect } from "react-redux";
17+
import { bindActionCreators } from "redux";
18+
19+
const CustomersPage = React.lazy(() =>
20+
import(/* webpackChunkName: "customers-page" */ "./pages/CustomersPage")
21+
);
22+
23+
const EmployeesPage = React.lazy(() =>
24+
import(/* webpackChunkName: "employees-page" */ "./pages/EmployeesPage")
25+
);
26+
27+
const Auth = connect((state: AuthModuleOwnState) => ({
28+
isAuthenticated: !!state.authInfo
29+
}))(({ isAuthenticated, children }) =>
30+
isAuthenticated ? (
31+
children
32+
) : (
33+
<Redirect to={`/login/?r=${encodeURIComponent(location.pathname)}`} />
34+
)
35+
);
36+
37+
const Login = connect(
38+
(state: AuthModuleOwnState) => ({
39+
isAuthenticated: !!state.authInfo
40+
}),
41+
dispatch => bindActionCreators({ fakeLogin: loginOperation }, dispatch)
42+
)(({ isAuthenticated, fakeLogin }) =>
43+
isAuthenticated ? (
44+
<Redirect to={new URLSearchParams(location.search).get("r") || "/"} />
45+
) : (
46+
<p>
47+
Login Page
48+
<button onClick={fakeLogin}>Fake Login</button>
49+
</p>
50+
)
51+
);
52+
53+
const App: React.FunctionComponent = () => (
54+
<Router>
55+
<DynamicModuleLoader modules={[getAuthModule()]}>
56+
<Switch>
57+
<Route path="/login/" component={Login} />
58+
<Auth>
59+
<div>
60+
<nav>
61+
<Link to="/">Dashboard</Link>
62+
<Link to="/customers/">Customers</Link>
63+
<Link to="/employees/">Employees</Link>
64+
</nav>
65+
<main>
66+
<React.Suspense fallback={<div>Loading...</div>}>
67+
<Switch>
68+
<Route path="/" exact={true} component={IndexPage} />
69+
<Route path="/customers/" component={CustomersPage} />
70+
<Route path="/employees/" component={EmployeesPage} />
71+
<Route component={() => <Redirect to="/" />} />
72+
</Switch>
73+
</React.Suspense>
74+
</main>
75+
</div>
76+
</Auth>
77+
</Switch>
78+
</DynamicModuleLoader>
79+
</Router>
80+
);
81+
82+
export default App;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>redux-dynamic-modules Example</title>
6+
</head>
7+
<body>
8+
<div id="root"></div>
9+
</body>
10+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from "react";
2+
import { Provider } from "react-redux";
3+
import { render } from "react-dom";
4+
import App from "./App";
5+
import store from "./modules";
6+
7+
const root = document.getElementById("root");
8+
9+
render(
10+
<Provider store={store}>
11+
<App />
12+
</Provider>,
13+
root
14+
);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Reducer } from "redux";
2+
import actionCreatorFactory, { isType } from "typescript-fsa";
3+
import { ISagaModule } from "redux-dynamic-modules-saga";
4+
import { SagaIterator } from "redux-saga";
5+
import { takeLatest, put, delay } from "redux-saga/effects";
6+
7+
interface AuthInfo {
8+
id: string;
9+
name: string;
10+
title: string;
11+
token: string;
12+
}
13+
14+
const actionCreator = actionCreatorFactory("AUTH");
15+
16+
const login = actionCreator.async<{}, AuthInfo>("LOGIN");
17+
18+
export const loginOperation = actionCreator("LOGIN_OPERATION");
19+
20+
type AuthInfoState = AuthInfo | null;
21+
22+
const authInfoReducer: Reducer<AuthInfoState> = (
23+
state = null /* FIXME: get from local storage */,
24+
action
25+
) => {
26+
if (isType(action, login.done)) {
27+
return action.payload.result;
28+
}
29+
return state;
30+
};
31+
32+
export interface AuthModuleOwnState {
33+
authInfo: AuthInfoState;
34+
}
35+
36+
function* fetchAuthInfoSaga(): SagaIterator {
37+
yield put(login.started({}));
38+
39+
yield delay(500);
40+
41+
// FIXME: save to local storage
42+
const result = {
43+
id: "charlie@example.com",
44+
name: "Charlie",
45+
title: "Engineering Manager",
46+
token: "secret_token"
47+
}; // mock
48+
yield put(login.done({ params: {}, result }));
49+
}
50+
51+
function* rootSaga(): SagaIterator {
52+
yield takeLatest(loginOperation, fetchAuthInfoSaga);
53+
}
54+
55+
export function getAuthModule(): ISagaModule<AuthModuleOwnState> {
56+
return {
57+
id: "auth",
58+
reducerMap: {
59+
authInfo: authInfoReducer
60+
},
61+
sagas: [rootSaga]
62+
};
63+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Reducer } from "redux";
2+
import actionCreatorFactory, { isType } from "typescript-fsa";
3+
import { ISagaModule } from "redux-dynamic-modules-saga";
4+
import { SagaIterator } from "redux-saga";
5+
import { takeLatest, put, delay, select } from "redux-saga/effects";
6+
import { AuthModuleOwnState, getAuthModule } from "../auth";
7+
import { IModule } from "redux-dynamic-modules";
8+
9+
interface BossInfo {
10+
id: string;
11+
name: string;
12+
title: string;
13+
}
14+
15+
const actionCreator = actionCreatorFactory("BOSS");
16+
17+
const fetchBossInfo = actionCreator.async<{ token: string | null }, BossInfo>(
18+
"FETCH_BOSS_INFO"
19+
);
20+
21+
export const fetchBossInfoOperation = actionCreator(
22+
"FETCH_BOSS_INFO_OPERATION"
23+
);
24+
25+
type BossInfoState = BossInfo | null;
26+
27+
const bossInfoReducer: Reducer<BossInfoState> = (state = null, action) => {
28+
if (isType(action, fetchBossInfo.done)) {
29+
return action.payload.result;
30+
}
31+
return state;
32+
};
33+
34+
export interface BossModuleOwnState {
35+
bossInfo: BossInfoState;
36+
}
37+
38+
type GlobalState = BossModuleOwnState & AuthModuleOwnState;
39+
40+
function* fetchBossInfoSaga(): SagaIterator {
41+
const token: string | null = yield select(
42+
(state: GlobalState) => state.authInfo && state.authInfo.token
43+
);
44+
45+
const params = { token };
46+
yield put(fetchBossInfo.started(params));
47+
48+
yield delay(500);
49+
50+
if (!token) {
51+
yield put(fetchBossInfo.failed({ params, error: "Invalid Token" }));
52+
return;
53+
}
54+
55+
const result = {
56+
id: "bob@example.com",
57+
name: "Bob",
58+
title: "VPoE"
59+
}; // mock
60+
yield put(fetchBossInfo.done({ params, result }));
61+
}
62+
63+
function* rootSaga(): SagaIterator {
64+
yield takeLatest(fetchBossInfoOperation, fetchBossInfoSaga);
65+
}
66+
67+
function getBossModuleInternal(): ISagaModule<BossModuleOwnState> {
68+
return {
69+
id: "boss",
70+
reducerMap: {
71+
bossInfo: bossInfoReducer
72+
},
73+
initialActions: [fetchBossInfoOperation()],
74+
sagas: [rootSaga]
75+
};
76+
}
77+
78+
export function getBossModule(): IModule<unknown>[] {
79+
// "boss" module depends on "auth" module
80+
return [getAuthModule(), getBossModuleInternal()];
81+
}

0 commit comments

Comments
 (0)