├── .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 | 
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 |
--------------------------------------------------------------------------------