├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── bundle.js └── bundle.js.map ├── images └── image01.png ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── App.tsx │ ├── Map.tsx │ └── Weather.tsx ├── index.tsx ├── shared │ ├── models │ │ └── Weather.ts │ ├── services │ │ └── Api.ts │ └── store │ │ ├── actions │ │ ├── __test__ │ │ │ └── index.spes.ts │ │ └── index.ts │ │ ├── constants │ │ └── index.ts │ │ ├── epics │ │ ├── WeatherEpic.ts │ │ ├── __test__ │ │ │ └── WeatherEpic.spec.ts │ │ └── index.ts │ │ ├── index.ts │ │ └── reducers │ │ ├── MapReducer.ts │ │ ├── WeatherReducer.ts │ │ ├── __test__ │ │ └── WeatherReducer.spec.ts │ │ └── index.ts └── style │ └── style.css ├── test └── __mocks__ │ ├── fileMock.js │ ├── shim.js │ └── styleMock.js ├── tsconfig.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 2018, 21 | "sourceType": "module" 22 | }, 23 | "plugins": [ 24 | "react", 25 | "@typescript-eslint" 26 | ], 27 | "rules": { 28 | "semi": ["error", "always"], 29 | "quotes": ["error", "double"], 30 | "no-unused-vars": "off", 31 | "react/prop-types": "off", 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | __coverage__ 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # IDE 65 | .idea 66 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mitsuru Ogawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-observable-typescript-sample 2 | 3 | A sample application for React + redux-observable + TypeScript 4 | 5 | ![image](./images/image01.png) 6 | 7 | > 👴 Old branch to see the version for redux-observable 0.18. 8 | > - https://github.com/mitsuruog/react-redux-observable-typescript-sample/tree/0.18 9 | 10 | ## Demo 11 | 12 | - https://mitsuruog.github.io/react-redux-observable-typescript-sample 13 | 14 | ## Blog 15 | 16 | - [React \+ Redux \+ redux\-observable \+ TypeScriptの実践的サンプル \| I am mitsuruog](https://blog.mitsuruog.info/2018/03/react-redux-observable-typescript) 17 | 18 | ## Installing / Getting started 19 | 20 | ``` 21 | npm start 22 | ``` 23 | 24 | Now open up `index.html` in your favorite browser and everything should be ready to use! 25 | ## Licensing 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /images/image01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuruog/react-redux-observable-typescript-sample/89d3ef5d63500f2a13b0eec77f2a1ead1f2c335a/images/image01.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Weather Map:React + redux-observable + TypeScript sample 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: ".", 3 | preset: "ts-jest", 4 | coverageDirectory: "/test/__coverage__/", 5 | setupFiles: ["/test/__mocks__/shim.js"], 6 | roots: ["/src/"], 7 | moduleNameMapper: { 8 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", 9 | "\\.(css|scss|less)$": "/test/__mocks__/styleMock.js", 10 | }, 11 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 12 | transformIgnorePatterns: ["/node_modules/"], 13 | testMatch: [ 14 | "/src/**/__test__/**/*.{js,jsx,ts,tsx}", 15 | "/src/**/*.{spec,test}.{js,jsx,ts,tsx}", 16 | ], 17 | moduleDirectories: ["node_modules"], 18 | globals: {}, 19 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "jest --coverage", 5 | "build": "./node_modules/.bin/webpack", 6 | "start": "./node_modules/.bin/webpack-dev-server --port 9000 --open --hot --inline" 7 | }, 8 | "engines": { 9 | "node": ">=12.16.3" 10 | }, 11 | "dependencies": { 12 | "@types/googlemaps": "3.39.13", 13 | "@types/react": "16.9.49", 14 | "@types/react-dom": "16.9.8", 15 | "@types/react-redux": "7.1.9", 16 | "jest": "^26.4.2", 17 | "react": "16.13.1", 18 | "react-dom": "16.13.1", 19 | "react-redux": "7.2.1", 20 | "redux": "4.0.5", 21 | "redux-observable": "1.2.0", 22 | "rxjs": "6.6.3", 23 | "ts-jest": "^26.3.0", 24 | "typesafe-actions": "4.4.2" 25 | }, 26 | "devDependencies": { 27 | "@typescript-eslint/eslint-plugin": "4.1.0", 28 | "@typescript-eslint/parser": "4.1.0", 29 | "eslint": "7.8.1", 30 | "eslint-loader": "4.0.2", 31 | "eslint-plugin-react": "7.20.6", 32 | "source-map-loader": "1.1.0", 33 | "ts-loader": "8.0.3", 34 | "typescript": "3.9.7", 35 | "webpack": "4.44.1", 36 | "webpack-cli": "3.3.12", 37 | "webpack-dev-server": "3.11.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import { Map } from "./Map"; 5 | import { Weather } from "./Weather"; 6 | import { RootState } from "../shared/store/reducers"; 7 | 8 | export interface AppProps {} 9 | 10 | export const App: React.SFC = () => { 11 | const { ready } = useSelector((state: RootState) => state.map); 12 | return ( 13 |
14 | {!ready &&
} 15 |
16 |

Weather Map

17 | 18 | (This sample application = React + redux-observable + TypeScript) 19 | 20 |
21 |
22 | 23 | 24 |
25 |
(c) 2020 mitsuru ogawa
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Map.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { actions } from "../shared/store"; 4 | 5 | declare global { 6 | interface Window { 7 | initMap: Function; 8 | } 9 | } 10 | 11 | export interface MapProps {} 12 | 13 | export const Map: React.SFC = () => { 14 | const dispatch = useDispatch(); 15 | const mapReady = React.useCallback(() => dispatch(actions.mapReadyAction()), [dispatch]); 16 | const getWeather = React.useCallback( 17 | (lat: number, lng: number) => dispatch(actions.weatherGetAction(lat, lng)), 18 | [dispatch] 19 | ); 20 | const mapRef = React.useCallback((node: HTMLDivElement) => { 21 | if (node !== null) { 22 | const onLoaded = () => { 23 | const map = new google.maps.Map(node, { 24 | center: { lat: 35.6811673, lng: 139.7648629 }, // default is Tokyo station!! 25 | zoom: 8, 26 | mapTypeControl: false, 27 | disableDoubleClickZoom: false, 28 | fullscreenControl: false, 29 | keyboardShortcuts: false, 30 | streetViewControl: false, 31 | scaleControl: false, 32 | rotateControl: false, 33 | panControl: false, 34 | }); 35 | map.addListener("click", (e) => { 36 | getWeather(e.latLng.lat(), e.latLng.lng()); 37 | }); 38 | mapReady(); 39 | }; 40 | // remove this key when you run it on your localhost. 41 | const script = document.createElement("script"); 42 | script.type = "text/javascript"; 43 | script.src = 44 | "https://maps.googleapis.com/maps/api/js?key=AIzaSyB5o5wtvz2sf_ckQm9rciFuJxc4pp2Sx-o&callback=initMap"; 45 | script.async = true; 46 | document.body.appendChild(script); 47 | window.initMap = onLoaded; 48 | } 49 | }, []); 50 | return
; 51 | }; -------------------------------------------------------------------------------- /src/components/Weather.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | import { RootState } from "../shared/store/reducers"; 5 | 6 | export interface WeatherProps {} 7 | 8 | export const Weather: React.SFC = () => { 9 | const { weather } = useSelector((state: RootState) => state.weather); 10 | if (!weather) { 11 | return null; 12 | } 13 | return ( 14 |
15 |

{weather.name}

16 |
17 |
Weather
18 |
19 | {weather.weather[0].main} 20 | ({weather.weather[0].description}) 21 |
22 |
Temperature(Max/Min)
23 |
24 | {weather.main.temp_max}℃ / {weather.main.temp_min}℃ 25 |
26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | 5 | import { App } from "./components/App"; 6 | 7 | /** 8 | * Redux store setup 9 | */ 10 | import { store } from "./shared/store"; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById("root") 17 | ); 18 | -------------------------------------------------------------------------------- /src/shared/models/Weather.ts: -------------------------------------------------------------------------------- 1 | export default class Weather { 2 | public coord: { 3 | lon: number; 4 | lat: number; 5 | }; 6 | public weather: Array<{ 7 | id: number; 8 | main: string; 9 | description: string; 10 | icon: string; 11 | }>; 12 | public base: string; 13 | public main: { 14 | temp: number; 15 | pressure: number; 16 | humidity: number; 17 | temp_min: number; 18 | temp_max: number; 19 | sea_level: number; 20 | grnd_level: number; 21 | }; 22 | public wind: { 23 | speed: number; 24 | deg: number; 25 | }; 26 | public clouds: { 27 | all: number; 28 | }; 29 | public dt: number; 30 | public sys: { 31 | message: number; 32 | country: string; 33 | sunrise: number; 34 | sunset: number; 35 | }; 36 | public id: number; 37 | public name: string; 38 | public cod: number; 39 | 40 | constructor(args?: {}) { 41 | Object.assign(this, args); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/services/Api.ts: -------------------------------------------------------------------------------- 1 | import Weather from "../models/Weather"; 2 | 3 | const getWeather = (lat: number, lon: number): Promise => { 4 | return fetch( 5 | `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=metric&APPID=49f8541c5e9d0758175574596d1f532e` 6 | ).then((response) => response.json()); 7 | }; 8 | 9 | export { getWeather }; 10 | -------------------------------------------------------------------------------- /src/shared/store/actions/__test__/index.spes.ts: -------------------------------------------------------------------------------- 1 | import { actions } from "../.."; 2 | import { getType } from "typesafe-actions"; 3 | import Weather from "../../../models/Weather"; 4 | 5 | test("should create an action to get a weather", () => { 6 | const expectedAction = { 7 | type: getType(actions.weatherGetAction), 8 | payload: { lat: 1, lng: 2 }, 9 | }; 10 | expect(actions.weatherGetAction(1, 2)).toEqual(expectedAction); 11 | }); 12 | 13 | test("should create an action to set a weather", () => { 14 | const expectedAction = { 15 | type: getType(actions.weatherSetAction), 16 | payload: { id: 1 }, 17 | }; 18 | expect(actions.weatherSetAction(new Weather({ id: 1 }))).toEqual( 19 | expectedAction 20 | ); 21 | }); 22 | 23 | test("should create an action to set an error", () => { 24 | const expectedAction = { 25 | type: getType(actions.weatherErrorAction), 26 | payload: new Error("error"), 27 | }; 28 | expect(actions.weatherErrorAction(new Error("error"))).toEqual( 29 | expectedAction 30 | ); 31 | }); 32 | -------------------------------------------------------------------------------- /src/shared/store/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | import Weather from "../../models/Weather"; 3 | import { 4 | WEATHER_GET, 5 | WEATHER_SET, 6 | MAP_READY, 7 | WEATHER_ERROR, 8 | } from "../constants"; 9 | 10 | export const weatherGetAction = createAction( 11 | WEATHER_GET, 12 | (resolve) => (lat: number, lng: number) => resolve({ lat, lng }) 13 | ); 14 | 15 | export const weatherSetAction = createAction( 16 | WEATHER_SET, 17 | (resolve) => (weather: Weather) => resolve(weather) 18 | ); 19 | 20 | export const weatherErrorAction = createAction( 21 | WEATHER_ERROR, 22 | (resolve) => (error: Error) => resolve(error) 23 | ); 24 | 25 | export const mapReadyAction = createAction(MAP_READY); 26 | -------------------------------------------------------------------------------- /src/shared/store/constants/index.ts: -------------------------------------------------------------------------------- 1 | // NOTE 2 | // DO NOT USE dynamic string operations(like template string) as action type property. 3 | // see more details: https://github.com/piotrwitek/typesafe-actions#--the-actions 4 | export const MAP_READY = "@@map/READY"; 5 | export const WEATHER_GET = "@@weather/GET"; 6 | export const WEATHER_SET = "@@weather/SET"; 7 | export const WEATHER_ERROR = "@@weather/ERROR"; 8 | -------------------------------------------------------------------------------- /src/shared/store/epics/WeatherEpic.ts: -------------------------------------------------------------------------------- 1 | import { Epic } from "redux-observable"; 2 | import { from, of } from "rxjs"; 3 | import { exhaustMap, filter, map, catchError } from "rxjs/operators"; 4 | import { isActionOf } from "typesafe-actions"; 5 | 6 | import { RootState } from "../reducers"; 7 | import { actions, ActionsType } from ".."; 8 | import * as API from "../../services/Api"; 9 | 10 | export const weatherGetEpic: Epic< 11 | ActionsType, 12 | ActionsType, 13 | RootState, 14 | typeof API 15 | > = (action$, store, { getWeather }) => 16 | action$.pipe( 17 | filter(isActionOf(actions.weatherGetAction)), 18 | exhaustMap((action) => 19 | from(getWeather(action.payload.lat, action.payload.lng)).pipe( 20 | map(actions.weatherSetAction), 21 | catchError((error) => of(actions.weatherErrorAction(error))) 22 | ) 23 | ) 24 | ); 25 | 26 | export default [weatherGetEpic]; 27 | -------------------------------------------------------------------------------- /src/shared/store/epics/__test__/WeatherEpic.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestScheduler } from "rxjs/testing"; 2 | import { actions } from "../.."; 3 | import * as epics from "../WeatherEpic"; 4 | import { getType } from "typesafe-actions"; 5 | 6 | const testScheduler = new TestScheduler((actual, expected) => { 7 | return expect(actual).toEqual(expected); 8 | }); 9 | 10 | describe("Test WeatherEpic", () => { 11 | beforeEach(() => { 12 | testScheduler.frame = 0; 13 | }); 14 | describe("should handle actions.weatherGetAction", () => { 15 | test("success case", () => { 16 | testScheduler.run(({ hot, cold, expectObservable }) => { 17 | const action$ = hot("-a", { 18 | a: actions.weatherGetAction(1, 2), 19 | }); 20 | const state$ = {}; 21 | const dependencies = { 22 | getWeather: () => cold("--a", { a: { id: 1 } }), 23 | }; 24 | const output$ = epics.weatherGetEpic( 25 | // @ts-expect-error HotObservable can't pass into the ActionsObservable 26 | action$, 27 | state$, 28 | dependencies 29 | ); 30 | expectObservable(output$).toBe("---a", { 31 | a: { type: getType(actions.weatherSetAction), payload: { id: 1 } }, 32 | }); 33 | }); 34 | }); 35 | test("failure case", () => { 36 | testScheduler.run(({ hot, cold, expectObservable }) => { 37 | const action$ = hot("-a", { 38 | a: actions.weatherGetAction(1, 2), 39 | }); 40 | const state$ = {}; 41 | const dependencies = { 42 | getWeather: () => cold("--#"), 43 | }; 44 | const output$ = epics.weatherGetEpic( 45 | // @ts-expect-error HotObservable can't pass into the ActionsObservable 46 | action$, 47 | state$, 48 | dependencies 49 | ); 50 | expectObservable(output$).toBe("---a", { 51 | a: { 52 | type: getType(actions.weatherErrorAction), 53 | payload: "error", 54 | }, 55 | }); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/shared/store/epics/index.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics } from "redux-observable"; 2 | 3 | import weatherEpic from "./WeatherEpic"; 4 | 5 | const epics = combineEpics(...weatherEpic); 6 | 7 | export default epics; 8 | -------------------------------------------------------------------------------- /src/shared/store/index.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from "redux"; 2 | import { createEpicMiddleware } from "redux-observable"; 3 | import { ActionType } from "typesafe-actions"; 4 | 5 | import * as API from "../services/Api"; 6 | import * as actions from "./actions"; 7 | import epics from "./epics"; 8 | import reducers, { RootState } from "./reducers"; 9 | 10 | export type RootStateType = RootState; 11 | export type ActionsType = ActionType; 12 | 13 | declare global { 14 | interface Window { 15 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: Function; 16 | } 17 | } 18 | 19 | const epicMiddleware = createEpicMiddleware< 20 | ActionsType, 21 | ActionsType, 22 | RootState 23 | >({ 24 | dependencies: API, 25 | }); 26 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 27 | 28 | // Create store 29 | function configureStore(initialState?: RootStateType) { 30 | // configure middlewares 31 | const middlewares = [epicMiddleware]; 32 | // compose enhancers 33 | const enhancer = composeEnhancers(applyMiddleware(...middlewares)); 34 | // create store 35 | return createStore(reducers, initialState, enhancer); 36 | } 37 | 38 | const store = configureStore(); 39 | 40 | epicMiddleware.run(epics); 41 | 42 | export { store, actions }; 43 | -------------------------------------------------------------------------------- /src/shared/store/reducers/MapReducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from "typesafe-actions"; 2 | import { ActionsType, actions } from ".."; 3 | 4 | export interface MapState { 5 | readonly ready: boolean; 6 | } 7 | 8 | const initialState = { 9 | ready: false, 10 | }; 11 | 12 | export const mapReducer = createReducer( 13 | initialState 14 | ).handleAction(actions.mapReadyAction, (state) => ({ ...state, ready: true })); 15 | -------------------------------------------------------------------------------- /src/shared/store/reducers/WeatherReducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from "typesafe-actions"; 2 | 3 | import { actions, ActionsType } from ".."; 4 | import Weather from "../../models/Weather"; 5 | 6 | export interface WeatherState { 7 | weather?: Weather; 8 | } 9 | 10 | export const initialState = {}; 11 | 12 | export const weatherReducer = createReducer( 13 | initialState 14 | ) 15 | .handleAction(actions.weatherSetAction, (state, action) => ({ 16 | ...state, 17 | weather: new Weather(action.payload), 18 | })) 19 | .handleAction(actions.weatherErrorAction, (state, action) => { 20 | console.error(action.payload); 21 | return state; 22 | }); 23 | -------------------------------------------------------------------------------- /src/shared/store/reducers/__test__/WeatherReducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { actions } from "../.."; 2 | import { weatherReducer as reducer, initialState } from "../WeatherReducer"; 3 | import Weather from "../../../models/Weather"; 4 | 5 | describe("Test WeatherReducer", () => { 6 | test("should return the initial state", () => { 7 | // @ts-expect-error set null action 8 | expect(reducer(undefined, {})).toEqual({}); 9 | }); 10 | test("should handle actions.userGetSuccessAction", () => { 11 | expect( 12 | reducer(initialState, actions.weatherSetAction(new Weather({ id: 1 }))) 13 | ).toEqual({ 14 | weather: new Weather({ id: 1 }), 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/shared/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import { weatherReducer, WeatherState } from "./WeatherReducer"; 4 | import { mapReducer, MapState } from "./MapReducer"; 5 | 6 | export type RootState = { 7 | weather: WeatherState; 8 | map: MapState; 9 | }; 10 | 11 | const reducers = combineReducers({ 12 | weather: weatherReducer, 13 | map: mapReducer, 14 | }); 15 | 16 | export default reducers; 17 | -------------------------------------------------------------------------------- /src/style/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | #map { 7 | height: 100vh; 8 | width: 100vw; 9 | } 10 | 11 | .header { 12 | position: fixed; 13 | top: 1rem; 14 | left: 1rem; 15 | background: #fff; 16 | padding: 1rem; 17 | z-index: 1; 18 | } 19 | 20 | .header h1 { 21 | margin: 0; 22 | } 23 | 24 | .footer { 25 | position: fixed; 26 | bottom: 1rem; 27 | left: 1rem; 28 | padding: 1rem; 29 | background: #fff; 30 | } 31 | 32 | .weather { 33 | position: fixed; 34 | top: 1rem; 35 | right: 1rem; 36 | background: #fff; 37 | padding: 0 1rem; 38 | z-index: 100; 39 | } 40 | 41 | .loading { 42 | position: fixed; 43 | top: 0; 44 | right: 0; 45 | bottom: 0; 46 | left: 0; 47 | background: rgba(0, 0, 0, 0.5); 48 | z-index: 100; 49 | } 50 | -------------------------------------------------------------------------------- /test/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = "test-file-stub"; -------------------------------------------------------------------------------- /test/__mocks__/shim.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuruog/react-redux-observable-typescript-sample/89d3ef5d63500f2a13b0eec77f2a1ead1f2c335a/test/__mocks__/shim.js -------------------------------------------------------------------------------- /test/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "lib": ["es6", "dom", "es2017"], 7 | "module": "commonjs", 8 | "target": "es5", 9 | "jsx": "react" 10 | }, 11 | "include": [ 12 | "./src/**/*" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/index.tsx", 3 | output: { 4 | filename: "bundle.js", 5 | path: __dirname + "/dist" 6 | }, 7 | 8 | mode: "development", 9 | 10 | // Enable sourcemaps for debugging webpack"s output. 11 | devtool: "source-map", 12 | 13 | resolve: { 14 | // Add ".ts" and ".tsx" as resolvable extensions. 15 | extensions: [".ts", ".tsx", ".js", ".json"] 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | { enforce: "pre", test: /\.tsx?$/, exclude: /node_modules/, loader: "eslint-loader", options: { fix: true } }, 21 | 22 | // All files with a ".ts" or ".tsx" extension will be handled by "awesome-typescript-loader". 23 | { test: /\.tsx?$/, loader: "ts-loader" }, 24 | 25 | // All output ".js" files will have any sourcemaps re-processed by "source-map-loader". 26 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" } 27 | ] 28 | }, 29 | 30 | }; 31 | --------------------------------------------------------------------------------