diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4ebe7cf..338488d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,7 +8,9 @@
},
"files.insertFinalNewline": true,
"javascript.format.enable": false,
- "prettier.tslintIntegration": true,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.tslint": true
+ },
"[javascript]": {
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
diff --git a/README.md b/README.md
index f1771e1..c280771 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,8 @@ VSCode と prettier と TSLint によって、リアルタイムに整形と構
### For desktop
-
+
+
### For mobile
@@ -26,6 +27,7 @@ VSCode と prettier と TSLint によって、リアルタイムに整形と構
- [Next.js v9](https://nextjs.org/)
- [MATERIAL-UI v4](https://material-ui.com/)
- [Redux](https://redux.js.org/)
+- [redux-saga](https://redux-saga.js.org/)
- [TSLint](https://palantir.github.io/tslint/)
## Requirement
diff --git a/components/molecules/ReduxSagaResponse.tsx b/components/molecules/ReduxSagaResponse.tsx
new file mode 100644
index 0000000..f0da9f8
--- /dev/null
+++ b/components/molecules/ReduxSagaResponse.tsx
@@ -0,0 +1,30 @@
+import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
+import React from "react"
+
+const useStyles = makeStyles((_: Theme) =>
+ createStyles({
+ root: {},
+ })
+)
+
+type Props = {
+ responses: string[]
+}
+
+/**
+ * redux-saga response component
+ * @param props Props
+ */
+export const ReduxSagaResponse = function(props: Props) {
+ const classes = useStyles(props)
+ const { responses } = props
+ return (
+
+ {responses.map((value: string, index: number) => (
+ -
+ [{String(index + 1).padStart(2, "0")}] {value}
+
+ ))}
+
+ )
+}
diff --git a/components/molecules/index.ts b/components/molecules/index.ts
index bc9f7eb..a88a476 100644
--- a/components/molecules/index.ts
+++ b/components/molecules/index.ts
@@ -1,2 +1,3 @@
export * from "./NextListItem"
export * from "./PageHeader"
+export * from "./ReduxSagaResponse"
diff --git a/components/organisms/ReduxSagaSample.tsx b/components/organisms/ReduxSagaSample.tsx
new file mode 100644
index 0000000..33c57b6
--- /dev/null
+++ b/components/organisms/ReduxSagaSample.tsx
@@ -0,0 +1,124 @@
+import { Box, InputAdornment, TextField, Typography } from "@material-ui/core"
+import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
+import { Create, Timer } from "@material-ui/icons"
+import React, { useEffect, useState } from "react"
+import { IReduxSagaState } from "../../store/redux-saga"
+import { ReduxSagaResponse } from "../molecules"
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ root: {},
+ title: {
+ fontSize: "50px",
+ marginBottom: theme.spacing(2),
+ },
+ subTitle: {
+ fontSize: "35px",
+ },
+ section: {
+ marginBottom: theme.spacing(4),
+ },
+ })
+)
+
+type Props = {
+ title: string
+ description?: React.ReactNode
+ onChange: (inputValue: string) => void
+ storeState?: IReduxSagaState
+ responseResultMax: number
+ interval: number
+}
+
+/**
+ * redux-saga sample component
+ * @param props {Props} props
+ */
+export const ReduxSagaSample = function(props: Props) {
+ const {
+ title,
+ description,
+ onChange,
+ storeState,
+ responseResultMax,
+ interval,
+ } = props
+ const classes = useStyles(props)
+ const [requestValue, setRequestValue] = useState("")
+ const [responseValues, setResponseValues] = useState([])
+ const [previousResponseValue, setPreviousResponseValue] = useState("")
+
+ useEffect(() => {
+ if (!storeState.timestamp) {
+ return
+ }
+ const fetchResult = `${storeState.timestamp} - ${storeState.input}`
+ if (fetchResult === previousResponseValue) {
+ return
+ }
+ responseValues.unshift(fetchResult)
+ if (responseValues && responseResultMax < responseValues.length) {
+ responseValues.pop()
+ }
+ setResponseValues(responseValues)
+ setPreviousResponseValue(fetchResult)
+ }, [storeState.timestamp])
+
+ const handleChangeInput = (e: React.ChangeEvent) => {
+ const value = e.target.value || ""
+ setRequestValue(value)
+ onChange(value)
+ }
+
+ return (
+ <>
+
+ {title} with redux-saga
+
+
+ {description && {description}}
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+ {title} response
+
+
+
+ {responseValues && }
+
+ >
+ )
+}
diff --git a/components/organisms/index.ts b/components/organisms/index.ts
index 83f2123..5b11762 100644
--- a/components/organisms/index.ts
+++ b/components/organisms/index.ts
@@ -1,3 +1,4 @@
export * from "./HeaderArticleContainer"
+export * from "./ReduxSagaSample"
export * from "./ResponsiveDrawer"
export * from "./Sidenavi"
diff --git a/constants/Page.ts b/constants/Page.ts
index 6e3c13a..4ce0dbf 100644
--- a/constants/Page.ts
+++ b/constants/Page.ts
@@ -1,9 +1,7 @@
import { Color } from "@material-ui/core"
-import { blue, orange, pink, red } from "@material-ui/core/colors"
+import { blue, orange, pink, red, teal } from "@material-ui/core/colors"
import { SvgIconProps } from "@material-ui/core/SvgIcon"
-import HomeIcon from "@material-ui/icons/Home"
-import InfoIcon from "@material-ui/icons/Info"
-import SaveIcon from "@material-ui/icons/Save"
+import { Home, Info, Save, Whatshot } from "@material-ui/icons"
import { IEnum } from "."
/**
@@ -23,7 +21,7 @@ export class Page implements IEnum {
"Top page | sample",
"Feat typescript and next.js and redux and material-ui !!",
"/",
- HomeIcon,
+ Home,
pink
)
public static readonly REDUX = new Page(
@@ -33,17 +31,27 @@ export class Page implements IEnum {
"Redux sample | sample",
"Basic redux examples with typescript-fsa and immer.",
"/redux",
- SaveIcon,
+ Save,
blue
)
- public static readonly ABOUT = new Page(
+ public static readonly REDUX_SAGAA = new Page(
3,
+ "Redux Saga",
+ "Redux Saga sample",
+ "Redux Saga sample | sample",
+ "Basic redux-saga examples with typescript-fsa and immer.",
+ "/redux-saga",
+ Whatshot,
+ teal
+ )
+ public static readonly ABOUT = new Page(
+ 10,
"About",
"About this site",
"About | sample",
"Site about page.",
"/about",
- InfoIcon,
+ Info,
orange
)
public static readonly ERROR = new Page(
@@ -53,7 +61,7 @@ export class Page implements IEnum {
"Error | sample",
"Error.",
"/error",
- InfoIcon,
+ Info,
red
)
diff --git a/constants/SagaSetting.ts b/constants/SagaSetting.ts
new file mode 100644
index 0000000..936db50
--- /dev/null
+++ b/constants/SagaSetting.ts
@@ -0,0 +1,4 @@
+export const SagaSetting = {
+ DEBOUNCE_INTERVAL: 2000,
+ THROTTLE_INTERVAL: 1000,
+}
diff --git a/constants/index.ts b/constants/index.ts
index a2ba69f..0c3bb0a 100644
--- a/constants/index.ts
+++ b/constants/index.ts
@@ -1,3 +1,4 @@
export * from "./IEnum"
export * from "./Page"
+export * from "./SagaSetting"
export * from "./SiteInfo"
diff --git a/model/InputResponseModel.ts b/model/InputResponseModel.ts
new file mode 100644
index 0000000..437511c
--- /dev/null
+++ b/model/InputResponseModel.ts
@@ -0,0 +1,5 @@
+export interface InputResponseModel {
+ input: string
+ timestamp: string
+ error?: Error
+}
diff --git a/model/index.ts b/model/index.ts
new file mode 100644
index 0000000..8055988
--- /dev/null
+++ b/model/index.ts
@@ -0,0 +1 @@
+export * from "./InputResponseModel"
diff --git a/package-lock.json b/package-lock.json
index 9db4282..705d3a8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1226,6 +1226,53 @@
"react-is": "^16.8.0"
}
},
+ "@redux-saga/core": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz",
+ "integrity": "sha512-8tInBftak8TPzE6X13ABmEtRJGjtK17w7VUs7qV17S8hCO5S3+aUTWZ/DBsBJPdE8Z5jOPwYALyvofgq1Ws+kg==",
+ "requires": {
+ "@babel/runtime": "^7.6.3",
+ "@redux-saga/deferred": "^1.1.2",
+ "@redux-saga/delay-p": "^1.1.2",
+ "@redux-saga/is": "^1.1.2",
+ "@redux-saga/symbols": "^1.1.2",
+ "@redux-saga/types": "^1.1.0",
+ "redux": "^4.0.4",
+ "typescript-tuple": "^2.2.1"
+ }
+ },
+ "@redux-saga/deferred": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.1.2.tgz",
+ "integrity": "sha512-908rDLHFN2UUzt2jb4uOzj6afpjgJe3MjICaUNO3bvkV/kN/cNeI9PMr8BsFXB/MR8WTAZQq/PlTq8Kww3TBSQ=="
+ },
+ "@redux-saga/delay-p": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.1.2.tgz",
+ "integrity": "sha512-ojc+1IoC6OP65Ts5+ZHbEYdrohmIw1j9P7HS9MOJezqMYtCDgpkoqB5enAAZrNtnbSL6gVCWPHaoaTY5KeO0/g==",
+ "requires": {
+ "@redux-saga/symbols": "^1.1.2"
+ }
+ },
+ "@redux-saga/is": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.2.tgz",
+ "integrity": "sha512-OLbunKVsCVNTKEf2cH4TYyNbbPgvmZ52iaxBD4I1fTif4+MTXMa4/Z07L83zW/hTCXwpSZvXogqMqLfex2Tg6w==",
+ "requires": {
+ "@redux-saga/symbols": "^1.1.2",
+ "@redux-saga/types": "^1.1.0"
+ }
+ },
+ "@redux-saga/symbols": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.2.tgz",
+ "integrity": "sha512-EfdGnF423glv3uMwLsGAtE6bg+R9MdqlHEzExnfagXPrIiuxwr3bdiAwz3gi+PsrQ3yBlaBpfGLtDG8rf3LgQQ=="
+ },
+ "@redux-saga/types": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz",
+ "integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg=="
+ },
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -8034,6 +8081,14 @@
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==",
"dev": true
},
+ "redux-saga": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz",
+ "integrity": "sha512-RkSn/z0mwaSa5/xH/hQLo8gNf4tlvT18qXDNvedihLcfzh+jMchDgaariQoehCpgRltEm4zHKJyINEz6aqswTw==",
+ "requires": {
+ "@redux-saga/core": "^1.1.3"
+ }
+ },
"reflect.ownkeys": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
@@ -9014,6 +9069,14 @@
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
"dev": true
},
+ "typescript-compare": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
+ "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==",
+ "requires": {
+ "typescript-logic": "^0.0.0"
+ }
+ },
"typescript-fsa": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/typescript-fsa/-/typescript-fsa-3.0.0.tgz",
@@ -9024,6 +9087,19 @@
"resolved": "https://registry.npmjs.org/typescript-fsa-reducers/-/typescript-fsa-reducers-1.2.1.tgz",
"integrity": "sha512-Qgn7zEnAU5n3YEWEL5ooEmIWZl9B4QyXD4Y/0DqpUzF0YuTrcsLa7Lht0gFXZ+xqLJXQwo3fEiTfQTDF1fBnMg=="
},
+ "typescript-logic": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz",
+ "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q=="
+ },
+ "typescript-tuple": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz",
+ "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==",
+ "requires": {
+ "typescript-compare": "^0.0.2"
+ }
+ },
"unfetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz",
diff --git a/package.json b/package.json
index ca6b426..b9dd241 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"react-dom": "^16.12.0",
"react-redux": "^7.1.3",
"redux": "^4.0.4",
+ "redux-saga": "^1.1.3",
"typescript-fsa": "^3.0.0",
"typescript-fsa-reducers": "^1.2.1"
},
diff --git a/pages/api/input.tsx b/pages/api/input.tsx
new file mode 100644
index 0000000..801a135
--- /dev/null
+++ b/pages/api/input.tsx
@@ -0,0 +1,26 @@
+import { NextApiRequest, NextApiResponse } from "next"
+import { InputResponseModel } from "../../model"
+
+export default (req: NextApiRequest, res: NextApiResponse) => {
+ const inputValue = req.query["value"]
+ const responseBody: InputResponseModel = {
+ input: inputValue,
+ timestamp: getCurrentTimestamp(),
+ }
+ res.status(200).json(responseBody)
+}
+
+const getCurrentTimestamp = () => {
+ const padding = (value: number, num: number): string =>
+ String(value).padStart(num, "0")
+ const d = new Date()
+ const year = d.getFullYear()
+ const month = padding(d.getMonth() + 1, 2)
+ const date = padding(d.getDate(), 2)
+ const hour = padding(d.getHours(), 2)
+ const minute = padding(d.getMinutes(), 2)
+ const second = padding(d.getSeconds(), 2)
+ const microSecond = padding(d.getMilliseconds(), 3)
+
+ return `${year}/${month}/${date} ${hour}:${minute}:${second}.${microSecond}`
+}
diff --git a/pages/redux-saga.tsx b/pages/redux-saga.tsx
new file mode 100644
index 0000000..004a0d4
--- /dev/null
+++ b/pages/redux-saga.tsx
@@ -0,0 +1,102 @@
+import { Typography } from "@material-ui/core"
+import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
+import React from "react"
+import { useDispatch, useSelector } from "react-redux"
+import { AppContext } from "../components/AppContext"
+import { SpacingPaper } from "../components/atoms"
+import {
+ HeaderArticleContainer,
+ ReduxSagaSample,
+} from "../components/organisms"
+import { Layout } from "../components/templates"
+import { Page, SagaSetting } from "../constants"
+import { IPagePayload, PageActions } from "../store/page"
+import {
+ ReduxSagaActions,
+ reduxSagaDebounceSelector,
+ reduxSagaThrottleSelector,
+} from "../store/redux-saga"
+
+const useStyles = makeStyles((_: Theme) =>
+ createStyles({
+ root: {},
+ })
+)
+
+type Props = {}
+
+function ReduxSaga(props: Props) {
+ const {} = props
+ const classes = useStyles(props)
+ const dispatch = useDispatch()
+ const reduxSagaDebounceState = useSelector(reduxSagaDebounceSelector)
+ const reduxSagaThrottleState = useSelector(reduxSagaThrottleSelector)
+
+ const onDebounce = (inputValue: string) => {
+ dispatch(
+ ReduxSagaActions.fetchDebounce({
+ input: inputValue,
+ })
+ )
+ }
+
+ const onThrottle = (inputValue: string) => {
+ dispatch(
+ ReduxSagaActions.fetchThrottle({
+ input: inputValue,
+ })
+ )
+ }
+
+ return (
+
+
+
+
+
+ Open DevTools of Google Chrome, open the network tab, and
+ check the execution frequency and timing of api.
+
+ >
+ }
+ storeState={reduxSagaDebounceState}
+ responseResultMax={10}
+ interval={SagaSetting.DEBOUNCE_INTERVAL}
+ onChange={onDebounce}
+ />
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Server side rendering
+ */
+ReduxSaga.getInitialProps = async (ctx: AppContext): Promise => {
+ const { store } = ctx
+
+ const pagePayload: IPagePayload = {
+ selectedPage: Page.REDUX_SAGAA,
+ }
+ store.dispatch({
+ type: PageActions.changePage.toString(),
+ payload: pagePayload,
+ })
+ return {}
+}
+
+export default ReduxSaga
diff --git a/store/api/InputApi.ts b/store/api/InputApi.ts
new file mode 100644
index 0000000..d91c993
--- /dev/null
+++ b/store/api/InputApi.ts
@@ -0,0 +1,9 @@
+import { InputResponseModel } from "../../model"
+import { IReduxSagaFetchPayload } from "../redux-saga"
+
+export const fetchInputApi = (
+ payload: IReduxSagaFetchPayload
+): Promise => {
+ const url = `http://localhost:3000/api/input?value=${payload.input}`
+ return fetch(url).then(response => response.json())
+}
diff --git a/store/api/index.ts b/store/api/index.ts
new file mode 100644
index 0000000..f8a21ca
--- /dev/null
+++ b/store/api/index.ts
@@ -0,0 +1 @@
+export * from "./InputApi"
diff --git a/store/configureStore.development.ts b/store/configureStore.development.ts
index 37ceb04..ddb6e95 100644
--- a/store/configureStore.development.ts
+++ b/store/configureStore.development.ts
@@ -1,12 +1,18 @@
import { applyMiddleware, createStore } from "redux"
import { composeWithDevTools } from "redux-devtools-extension"
+import createSagaMiddleware from "redux-saga"
import { InitialState } from "../store/states"
import { combinedReducers } from "./reducers"
+import { rootSaga } from "./sagas"
+
+const sagaMiddleware = createSagaMiddleware()
export function configureStore(initialState = InitialState) {
- return createStore(
+ const store = createStore(
combinedReducers,
initialState,
- composeWithDevTools(applyMiddleware())
+ composeWithDevTools(applyMiddleware(sagaMiddleware))
)
+ sagaMiddleware.run(rootSaga)
+ return store
}
diff --git a/store/configureStore.production.ts b/store/configureStore.production.ts
index 41827fe..effb918 100644
--- a/store/configureStore.production.ts
+++ b/store/configureStore.production.ts
@@ -1,7 +1,17 @@
import { applyMiddleware, createStore } from "redux"
+import createSagaMiddleware from "redux-saga"
import { InitialState } from "../store/states"
import { combinedReducers } from "./reducers"
+import { rootSaga } from "./sagas"
+
+const sagaMiddleware = createSagaMiddleware()
export function configureStore(initialState = InitialState) {
- return createStore(combinedReducers, initialState, applyMiddleware())
+ const store = createStore(
+ combinedReducers,
+ initialState,
+ applyMiddleware(sagaMiddleware)
+ )
+ sagaMiddleware.run(rootSaga)
+ return store
}
diff --git a/store/reducers.ts b/store/reducers.ts
index b19b0c1..821c2e3 100644
--- a/store/reducers.ts
+++ b/store/reducers.ts
@@ -1,9 +1,15 @@
import { combineReducers } from "redux"
import { countReducer } from "./counter/reducers"
import { pageReducer } from "./page/reducers"
+import {
+ reduxSagaDebounceReducer,
+ reduxSagaThrottleReducer,
+} from "./redux-saga/reducers"
import { IInitialState } from "./states"
export const combinedReducers = combineReducers({
counter: countReducer,
page: pageReducer,
+ reduxSagaDebounce: reduxSagaDebounceReducer,
+ reduxSagaThrottle: reduxSagaThrottleReducer,
})
diff --git a/store/redux-saga/actions.ts b/store/redux-saga/actions.ts
new file mode 100644
index 0000000..8f4061f
--- /dev/null
+++ b/store/redux-saga/actions.ts
@@ -0,0 +1,59 @@
+import actionCreatorFactory from "typescript-fsa"
+
+const actionCreator = actionCreatorFactory("redux-saga")
+
+export interface IReduxSagaFetchPayload {
+ input: string
+}
+
+//-------------------------------------------------------
+// debounce
+//-------------------------------------------------------
+export interface IReduxSagaDebounceSuccessPayload {
+ input: string
+ timestamp: string
+}
+
+export interface IReduxSagaDebounceFailurePayload {
+ error: Error
+}
+
+//-------------------------------------------------------
+// throttle
+//-------------------------------------------------------
+export interface IReduxSagaThrottleSuccessPayload {
+ input: string
+ timestamp: string
+}
+
+export interface IReduxSagaThrottleFailurePayload {
+ error: Error
+}
+
+export const ReduxSagaActions = {
+ // debounce
+ fetchDebounce: actionCreator("fetch debounce"),
+ debounceSuccess: actionCreator(
+ "debounce success"
+ ),
+ debounceFailure: actionCreator(
+ "debounce failure"
+ ),
+
+ // throttle
+ fetchThrottle: actionCreator("fetch throttle"),
+ throttleSuccess: actionCreator(
+ "throttle success"
+ ),
+ throttleFailure: actionCreator(
+ "throttle failure"
+ ),
+}
+
+export type ReduxSagaActionTypes =
+ | ReturnType
+ | ReturnType
+ | ReturnType
+ | ReturnType
+ | ReturnType
+ | ReturnType
diff --git a/store/redux-saga/index.ts b/store/redux-saga/index.ts
new file mode 100644
index 0000000..237eb2a
--- /dev/null
+++ b/store/redux-saga/index.ts
@@ -0,0 +1,3 @@
+export * from "./actions"
+export * from "./selectors"
+export * from "./states"
diff --git a/store/redux-saga/reducers.ts b/store/redux-saga/reducers.ts
new file mode 100644
index 0000000..747a763
--- /dev/null
+++ b/store/redux-saga/reducers.ts
@@ -0,0 +1,80 @@
+import produce from "immer"
+import { reducerWithInitialState } from "typescript-fsa-reducers"
+import {
+ IReduxSagaDebounceFailurePayload,
+ IReduxSagaDebounceSuccessPayload,
+ IReduxSagaThrottleFailurePayload,
+ IReduxSagaThrottleSuccessPayload,
+ ReduxSagaActions,
+} from "./actions"
+import { IReduxSagaState, ReduxSagaInitialState } from "./states"
+
+export const reduxSagaDebounceReducer = reducerWithInitialState(
+ ReduxSagaInitialState
+)
+ .case(
+ ReduxSagaActions.fetchDebounce,
+ (state: Readonly): IReduxSagaState => {
+ return state
+ }
+ )
+ .case(
+ ReduxSagaActions.debounceSuccess,
+ (
+ state: Readonly,
+ payload: IReduxSagaDebounceSuccessPayload
+ ): IReduxSagaState => {
+ const { input, timestamp } = payload
+ return produce(state, (draft: IReduxSagaState) => {
+ draft.input = input
+ draft.timestamp = timestamp
+ })
+ }
+ )
+ .case(
+ ReduxSagaActions.debounceFailure,
+ (
+ state: Readonly,
+ payload: IReduxSagaDebounceFailurePayload
+ ): IReduxSagaState => {
+ const { error } = payload
+ return produce(state, (draft: IReduxSagaState) => {
+ draft.error = error
+ })
+ }
+ )
+
+export const reduxSagaThrottleReducer = reducerWithInitialState(
+ ReduxSagaInitialState
+)
+ .case(
+ ReduxSagaActions.fetchThrottle,
+ (state: Readonly): IReduxSagaState => {
+ return state
+ }
+ )
+ .case(
+ ReduxSagaActions.throttleSuccess,
+ (
+ state: Readonly,
+ payload: IReduxSagaThrottleSuccessPayload
+ ): IReduxSagaState => {
+ const { input, timestamp } = payload
+ return produce(state, (draft: IReduxSagaState) => {
+ draft.input = input
+ draft.timestamp = timestamp
+ })
+ }
+ )
+ .case(
+ ReduxSagaActions.throttleFailure,
+ (
+ state: Readonly,
+ payload: IReduxSagaThrottleFailurePayload
+ ): IReduxSagaState => {
+ const { error } = payload
+ return produce(state, (draft: IReduxSagaState) => {
+ draft.error = error
+ })
+ }
+ )
diff --git a/store/redux-saga/sagas.ts b/store/redux-saga/sagas.ts
new file mode 100644
index 0000000..32331f5
--- /dev/null
+++ b/store/redux-saga/sagas.ts
@@ -0,0 +1,99 @@
+import { call, debounce, put, throttle } from "redux-saga/effects"
+import { SagaSetting } from "../../constants"
+import { InputResponseModel } from "../../model"
+import { fetchInputApi } from "../api"
+import {
+ IReduxSagaDebounceFailurePayload,
+ IReduxSagaDebounceSuccessPayload,
+ IReduxSagaFetchPayload,
+ IReduxSagaThrottleFailurePayload,
+ IReduxSagaThrottleSuccessPayload,
+ ReduxSagaActions,
+ ReduxSagaActionTypes,
+} from "./actions"
+
+/**
+ * Monitor specific redux-debounce-action fire when detected
+ */
+export const watchFetchDebounce = function*() {
+ yield debounce(
+ SagaSetting.DEBOUNCE_INTERVAL,
+ ReduxSagaActions.fetchDebounce,
+ executeFetchDebounce
+ )
+}
+
+function* executeFetchDebounce(action: ReduxSagaActionTypes) {
+ const fetchPayload = action.payload as IReduxSagaFetchPayload
+
+ try {
+ // call api
+ const fetchResult: InputResponseModel = yield call(
+ fetchInputApi,
+ fetchPayload
+ )
+
+ // Pack the fetch result into a redux successful action,
+ // and the caller gets the result from there
+ const successPayload: IReduxSagaDebounceSuccessPayload = {
+ input: fetchResult.input,
+ timestamp: fetchResult.timestamp,
+ }
+ yield put({
+ type: ReduxSagaActions.debounceSuccess.toString(),
+ payload: successPayload,
+ })
+ } catch (e) {
+ console.error(e)
+
+ // Pack exception error object into failure action, caller gets it
+ const failurePayload: IReduxSagaDebounceFailurePayload = {
+ error: e,
+ }
+ yield put({
+ type: ReduxSagaActions.debounceFailure.toString(),
+ payload: failurePayload,
+ })
+ }
+}
+
+/**
+ * Monitor specific redux-throttle-action fire when detected
+ */
+export const watchFetchThrottle = function*() {
+ yield throttle(
+ SagaSetting.THROTTLE_INTERVAL,
+ ReduxSagaActions.fetchThrottle,
+ executeFetchThrottle
+ )
+}
+
+function* executeFetchThrottle(action: ReduxSagaActionTypes) {
+ const fetchPayload = action.payload as IReduxSagaFetchPayload
+
+ try {
+ const fetchResult: InputResponseModel = yield call(
+ fetchInputApi,
+ fetchPayload
+ )
+
+ const successPayload: IReduxSagaThrottleSuccessPayload = {
+ input: fetchResult.input,
+ timestamp: fetchResult.timestamp,
+ }
+ yield put({
+ type: ReduxSagaActions.throttleSuccess.toString(),
+ payload: successPayload,
+ })
+ } catch (e) {
+ console.error(e)
+
+ const failurePayload: IReduxSagaThrottleFailurePayload = {
+ error: e,
+ }
+ yield put({
+ type: ReduxSagaActions.throttleFailure.toString(),
+ payload: failurePayload,
+ })
+ }
+}
diff --git a/store/redux-saga/selectors.ts b/store/redux-saga/selectors.ts
new file mode 100644
index 0000000..87118d0
--- /dev/null
+++ b/store/redux-saga/selectors.ts
@@ -0,0 +1,10 @@
+import { IInitialState } from "../states"
+import { IReduxSagaState } from "./states"
+
+export const reduxSagaDebounceSelector = (
+ state: IInitialState
+): IReduxSagaState => state.reduxSagaDebounce
+
+export const reduxSagaThrottleSelector = (
+ state: IInitialState
+): IReduxSagaState => state.reduxSagaThrottle
diff --git a/store/redux-saga/states.ts b/store/redux-saga/states.ts
new file mode 100644
index 0000000..456b4dc
--- /dev/null
+++ b/store/redux-saga/states.ts
@@ -0,0 +1,12 @@
+/**
+ * redux-saga
+ */
+export interface IReduxSagaState {
+ input: string
+ timestamp: string
+ error?: Error
+}
+export const ReduxSagaInitialState: IReduxSagaState = {
+ input: undefined,
+ timestamp: undefined,
+}
diff --git a/store/sagas.ts b/store/sagas.ts
new file mode 100644
index 0000000..0db751e
--- /dev/null
+++ b/store/sagas.ts
@@ -0,0 +1,6 @@
+import { all, fork } from "redux-saga/effects"
+import { watchFetchDebounce, watchFetchThrottle } from "./redux-saga/sagas"
+
+export const rootSaga = function* root() {
+ yield all([fork(watchFetchDebounce), fork(watchFetchThrottle)])
+}
diff --git a/store/states.ts b/store/states.ts
index 054bae3..b0934b1 100644
--- a/store/states.ts
+++ b/store/states.ts
@@ -1,5 +1,6 @@
import { CounterInitialState, ICounterState } from "./counter"
import { IPageState, PageInitialState } from "./page"
+import { IReduxSagaState, ReduxSagaInitialState } from "./redux-saga"
/**
* Initial state tree interface
@@ -7,6 +8,8 @@ import { IPageState, PageInitialState } from "./page"
export interface IInitialState {
counter: Readonly
page: Readonly
+ reduxSagaDebounce: Readonly
+ reduxSagaThrottle: Readonly
}
/**
@@ -15,4 +18,6 @@ export interface IInitialState {
export const InitialState: IInitialState = {
counter: CounterInitialState,
page: PageInitialState,
+ reduxSagaDebounce: ReduxSagaInitialState,
+ reduxSagaThrottle: ReduxSagaInitialState,
}