From 43ef64ba7789776c7a8d27cb25be7d08f35a9d75 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 17 Dec 2019 23:39:57 +0900 Subject: [PATCH 1/5] feat: add redux-saga --- package-lock.json | 76 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 77 insertions(+) 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" }, From 0091ea22568b4292524ecdd013adb6079a684535 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 17 Dec 2019 23:40:15 +0900 Subject: [PATCH 2/5] refactor: migrate new prettier setting --- .vscode/settings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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, From 08de18ae16fb6f74e1f74e468f9489a9fab76002 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 17 Dec 2019 23:41:13 +0900 Subject: [PATCH 3/5] feat: add redux-saga samples --- components/molecules/ReduxSagaResponse.tsx | 30 +++++ components/molecules/index.ts | 1 + components/organisms/ReduxSagaSample.tsx | 124 +++++++++++++++++++++ components/organisms/index.ts | 1 + constants/Page.ts | 26 +++-- constants/SagaSetting.ts | 4 + constants/index.ts | 1 + model/InputResponseModel.ts | 5 + model/index.ts | 1 + pages/api/input.tsx | 26 +++++ pages/redux-saga.tsx | 102 +++++++++++++++++ store/api/InputApi.ts | 9 ++ store/api/index.ts | 1 + store/configureStore.development.ts | 10 +- store/configureStore.production.ts | 12 +- store/reducers.ts | 6 + store/redux-saga/actions.ts | 58 ++++++++++ store/redux-saga/index.ts | 3 + store/redux-saga/reducers.ts | 80 +++++++++++++ store/redux-saga/sagas.ts | 99 ++++++++++++++++ store/redux-saga/selectors.ts | 10 ++ store/redux-saga/states.ts | 12 ++ store/sagas.ts | 6 + store/states.ts | 5 + 24 files changed, 620 insertions(+), 12 deletions(-) create mode 100644 components/molecules/ReduxSagaResponse.tsx create mode 100644 components/organisms/ReduxSagaSample.tsx create mode 100644 constants/SagaSetting.ts create mode 100644 model/InputResponseModel.ts create mode 100644 model/index.ts create mode 100644 pages/api/input.tsx create mode 100644 pages/redux-saga.tsx create mode 100644 store/api/InputApi.ts create mode 100644 store/api/index.ts create mode 100644 store/redux-saga/actions.ts create mode 100644 store/redux-saga/index.ts create mode 100644 store/redux-saga/reducers.ts create mode 100644 store/redux-saga/sagas.ts create mode 100644 store/redux-saga/selectors.ts create mode 100644 store/redux-saga/states.ts create mode 100644 store/sagas.ts 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/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..8bb72b2 --- /dev/null +++ b/store/redux-saga/actions.ts @@ -0,0 +1,58 @@ +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 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, } From 5b9639e21dd303d76558d16d42ca1bb4016605a9 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 17 Dec 2019 23:42:06 +0900 Subject: [PATCH 4/5] docs: add redux-saga dependency and screen shot --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 desktop](https://user-images.githubusercontent.com/12574048/46964420-f9fb9180-d0e2-11e8-9c05-e1594c533947.png) +![For desktop 1](https://user-images.githubusercontent.com/12574048/46964420-f9fb9180-d0e2-11e8-9c05-e1594c533947.png) +![For desktop 2](https://user-images.githubusercontent.com/12574048/71005010-3337f300-2126-11ea-844c-d113f5d87255.png) ### 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 From ae69eaf197f645629bdcf13a0d03debf2be4e117 Mon Sep 17 00:00:00 2001 From: username Date: Tue, 17 Dec 2019 23:52:12 +0900 Subject: [PATCH 5/5] fix: add missing type definitions --- store/redux-saga/actions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/store/redux-saga/actions.ts b/store/redux-saga/actions.ts index 8bb72b2..8f4061f 100644 --- a/store/redux-saga/actions.ts +++ b/store/redux-saga/actions.ts @@ -54,5 +54,6 @@ export type ReduxSagaActionTypes = | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType