├── .gitignore
├── README.md
├── db.sample.json
├── docs
└── images
│ └── logoasl.png
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── counter
│ ├── actions
│ │ └── index.ts
│ ├── api
│ │ └── index.ts
│ ├── components
│ │ └── counter.tsx
│ ├── reducers
│ │ └── index.ts
│ ├── sagas
│ │ └── index.ts
│ └── types
│ │ └── index.ts
├── index.tsx
├── loader
│ ├── components
│ │ └── loader.tsx
│ ├── reducers
│ │ └── index.ts
│ └── types
│ │ └── index.ts
├── react-app-env.d.ts
├── reducer.ts
├── saga.ts
├── serviceWorker.ts
├── store.ts
└── types.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | db.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Taller de React+Redux+Saga con Typescript
2 |
3 |
4 |

5 |
6 |
7 |
8 |
9 |   
10 |
11 |
12 |
13 | Este taller forma parte de las actividades del [Aula de Software Libre de la
14 | Universidad de Córdoba](https://www.uco.es/aulasoftwarelibre).
15 |
16 | ## Desarrollo del taller
17 |
18 | El taller está etiquetado en once pasos:
19 |
20 | - **step0**: Commit inicial
21 | - **step1**: Instalación de react-semantic-ui
22 | - **step2**: Desarrollo de un componente contador básico (no interactivo)
23 | - **step3**: Contador implementado con una clase con estado
24 | - **step4**: Implementación del contador con componente (función pura) y con contenedor
25 | - **step5**: Refactorizado a un componente funcional con hooks
26 | - **step6**: Introducción a High order components
27 | - **step7**: Refactorización con Redux: contenedor+componente
28 | - **step8**: Refactorización con Redux y hooks: componente sin contenedor
29 | - **step9**: Refactorización son Saga: acciones asíncronas
30 | - **step10**: Conexión con APIs usando Saga
31 | - **step11**: Componente de carga y error
32 |
33 | Puedes cambiar a un nuevo paso ejecutando `git checkout stepNN`, siendo NN el número del paso.
34 |
35 | > **ATENCIÓN**: Recuerda hacer `yarn install` después de hacer checkout a un nuevo _step_, ya que puede haber nuevas dependencias. Después es recomendable reiniciar la aplicación y volver a ejecutar `yarn start`. A partir del _step10_ se necesita iniciar el servidor de la API. Ejecuta `yarn mock:api` en otra terminal.
36 |
--------------------------------------------------------------------------------
/db.sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "counters": [
3 | {
4 | "id": 1,
5 | "value": 0
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/docs/images/logoasl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aulasoftwarelibre/taller-de-react-con-typescript-codigo/67f99141152b5855b3f19d9696eabfac3e9fe5fa/docs/images/logoasl.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "saga",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/jest": "24.0.17",
7 | "@types/node": "12.7.1",
8 | "@types/react": "16.9.1",
9 | "@types/react-dom": "16.8.5",
10 | "axios": "^0.19.0",
11 | "react": "^16.9.0",
12 | "react-dom": "^16.9.0",
13 | "react-redux": "^7.1.0",
14 | "react-scripts": "3.1.0",
15 | "redux": "^4.0.4",
16 | "redux-saga": "^1.0.5",
17 | "semantic-ui-css": "^2.4.1",
18 | "semantic-ui-react": "^0.87.3",
19 | "styled-components": "^4.3.2",
20 | "typescript": "^3"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject",
27 | "mock:api": "cp db.sample.json db.json ; json-server --watch db.json --port 4000"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "devDependencies": {
45 | "@types/react-redux": "^7.1.1",
46 | "@types/styled-components": "^4.1.18",
47 | "json-server": "^0.15.0",
48 | "redux-devtools-extension": "^2.13.8"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aulasoftwarelibre/taller-de-react-con-typescript-codigo/67f99141152b5855b3f19d9696eabfac3e9fe5fa/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aulasoftwarelibre/taller-de-react-con-typescript-codigo/67f99141152b5855b3f19d9696eabfac3e9fe5fa/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aulasoftwarelibre/taller-de-react-con-typescript-codigo/67f99141152b5855b3f19d9696eabfac3e9fe5fa/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow: /static/
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Container } from "semantic-ui-react";
3 | import styled from "styled-components";
4 |
5 | import Counter from "./counter/components/counter";
6 | import Loader from "./loader/components/loader";
7 |
8 | const App: React.FunctionComponent = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | const Body = styled(Container)`
19 | margin-top: 64px;
20 | `;
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/src/counter/actions/index.ts:
--------------------------------------------------------------------------------
1 | import { ActionCreator } from "redux";
2 |
3 | import {
4 | DECREMENT_COUNTER,
5 | DECREMENT_COUNTER_ASYNC,
6 | DecrementCounterAction,
7 | DecrementCounterAsyncAction,
8 | INCREMENT_COUNTER,
9 | INCREMENT_COUNTER_ASYNC,
10 | IncrementCounterAction,
11 | IncrementCounterAsyncAction,
12 | INIT_COUNTER_FAILURE,
13 | INIT_COUNTER_REQUEST,
14 | INIT_COUNTER_SUCCESS,
15 | InitCounterFailureAction,
16 | InitCounterRequestAction,
17 | InitCounterSuccessAction,
18 | RESET_COUNTER,
19 | RESET_COUNTER_ASYNC,
20 | ResetCounterAction,
21 | ResetCounterAsyncAction,
22 | } from "../types";
23 |
24 | export const incrementCounter: ActionCreator = (
25 | step: number
26 | ) => ({
27 | type: INCREMENT_COUNTER,
28 | payload: {
29 | step
30 | }
31 | });
32 |
33 | export const incrementCounterAsync: ActionCreator<
34 | IncrementCounterAsyncAction
35 | > = (step: number) => ({
36 | type: INCREMENT_COUNTER_ASYNC,
37 | payload: {
38 | step
39 | }
40 | });
41 |
42 | export const decrementCounter: ActionCreator = (
43 | step: number
44 | ) => ({
45 | type: DECREMENT_COUNTER,
46 | payload: {
47 | step
48 | }
49 | });
50 |
51 | export const decrementCounterAsync: ActionCreator<
52 | DecrementCounterAsyncAction
53 | > = (step: number) => ({
54 | type: DECREMENT_COUNTER_ASYNC,
55 | payload: {
56 | step
57 | }
58 | });
59 |
60 | export const resetCounter: ActionCreator = () => ({
61 | type: RESET_COUNTER
62 | });
63 |
64 | export const resetCounterAsync: ActionCreator<
65 | ResetCounterAsyncAction
66 | > = () => ({
67 | type: RESET_COUNTER_ASYNC
68 | });
69 |
70 | export const initCounterRequest: ActionCreator<
71 | InitCounterRequestAction
72 | > = () => ({
73 | type: INIT_COUNTER_REQUEST
74 | });
75 |
76 | export const initCounterSuccess: ActionCreator = (
77 | value: number
78 | ) => ({
79 | type: INIT_COUNTER_SUCCESS,
80 | payload: {
81 | value
82 | }
83 | });
84 |
85 | export const initCounterFailure: ActionCreator<
86 | InitCounterFailureAction
87 | > = () => ({
88 | type: INIT_COUNTER_FAILURE
89 | });
90 |
--------------------------------------------------------------------------------
/src/counter/api/index.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | export const readCounter = () => axios.get("http://localhost:4000/counters/1");
4 |
5 | export const updateCounter = (value: number) =>
6 | axios.put("http://localhost:4000/counters/1", { value });
7 |
--------------------------------------------------------------------------------
/src/counter/components/counter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Button, Card, Icon } from "semantic-ui-react";
4 |
5 | import {
6 | decrementCounter,
7 | decrementCounterAsync,
8 | incrementCounter,
9 | incrementCounterAsync,
10 | resetCounter,
11 | resetCounterAsync,
12 | } from "../actions";
13 | import { getCounterState } from "../reducers";
14 | import { CounterState } from "../types";
15 |
16 | export type Props = { step: number };
17 |
18 | const Counter: React.FunctionComponent = ({ step }) => {
19 | const dispatch = useDispatch();
20 | const { value: counter }: CounterState = useSelector(getCounterState);
21 |
22 | return (
23 |
24 |
25 | Contador
26 | El contador vale {counter}
27 |
28 |
29 |
30 |
40 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default Counter;
85 |
--------------------------------------------------------------------------------
/src/counter/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 |
3 | import {
4 | CounterActions,
5 | CounterState,
6 | CounterStoredState,
7 | DECREMENT_COUNTER,
8 | INCREMENT_COUNTER,
9 | INIT_COUNTER_SUCCESS,
10 | RESET_COUNTER,
11 | } from "../types";
12 |
13 | const initialState: CounterState = {
14 | value: 0
15 | };
16 |
17 | export const getCounterState = (store: CounterStoredState): CounterState =>
18 | store.counter;
19 |
20 | const reducer: Reducer = (
21 | state = initialState,
22 | action
23 | ) => {
24 | switch (action.type) {
25 | case INCREMENT_COUNTER:
26 | return {
27 | ...state,
28 | value: state.value + action.payload.step
29 | };
30 | case DECREMENT_COUNTER:
31 | return {
32 | ...state,
33 | value: state.value - action.payload.step
34 | };
35 | case RESET_COUNTER:
36 | return {
37 | ...state,
38 | value: 0
39 | };
40 | case INIT_COUNTER_SUCCESS:
41 | return {
42 | ...state,
43 | value: action.payload.value
44 | };
45 | default:
46 | return state;
47 | }
48 | };
49 |
50 | export default reducer;
51 |
--------------------------------------------------------------------------------
/src/counter/sagas/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | all,
3 | delay,
4 | fork,
5 | put,
6 | select,
7 | takeEvery,
8 | takeLatest,
9 | } from "redux-saga/effects";
10 |
11 | import {
12 | decrementCounter,
13 | incrementCounter,
14 | initCounterFailure,
15 | initCounterRequest,
16 | initCounterSuccess,
17 | } from "../actions";
18 | import { readCounter, updateCounter } from "../api";
19 | import { getCounterState } from "../reducers";
20 | import {
21 | CounterState,
22 | DECREMENT_COUNTER,
23 | DECREMENT_COUNTER_ASYNC,
24 | DecrementCounterAsyncAction,
25 | INCREMENT_COUNTER,
26 | INCREMENT_COUNTER_ASYNC,
27 | IncrementCounterAsyncAction,
28 | RESET_COUNTER,
29 | RESET_COUNTER_ASYNC,
30 | } from "../types";
31 |
32 | function* handleResetCounterAsync() {
33 | while (true) {
34 | let { value: counter }: CounterState = yield select(getCounterState);
35 |
36 | if (counter < 0) {
37 | yield put(incrementCounter(1));
38 | } else if (counter > 0) {
39 | yield put(decrementCounter(1));
40 | } else {
41 | break;
42 | }
43 |
44 | yield delay(200);
45 | }
46 | }
47 |
48 | function* watchHandleResetCounterAsync() {
49 | yield takeLatest(RESET_COUNTER_ASYNC, handleResetCounterAsync);
50 | }
51 |
52 | function* handleIncrementCounterAsync({
53 | payload: { step }
54 | }: IncrementCounterAsyncAction) {
55 | yield delay(1000);
56 |
57 | yield put(incrementCounter(step));
58 | }
59 |
60 | function* watchIncrementCounterAsync() {
61 | yield takeEvery(INCREMENT_COUNTER_ASYNC, handleIncrementCounterAsync);
62 | }
63 |
64 | function* handleDecrementCounterAsync({
65 | payload: { step }
66 | }: DecrementCounterAsyncAction) {
67 | yield delay(1000);
68 |
69 | yield put(decrementCounter(step));
70 | }
71 |
72 | function* watchDecrementCounterAsync() {
73 | yield takeEvery(DECREMENT_COUNTER_ASYNC, handleDecrementCounterAsync);
74 | }
75 |
76 | function* handleUpdateCounter() {
77 | try {
78 | const { value: counter }: CounterState = yield select(getCounterState);
79 |
80 | yield updateCounter(counter);
81 | } catch (e) {}
82 | }
83 |
84 | function* watchUpdateCounter() {
85 | yield takeLatest(
86 | [INCREMENT_COUNTER, DECREMENT_COUNTER, RESET_COUNTER],
87 | handleUpdateCounter
88 | );
89 | }
90 |
91 | function* handleInitCounter() {
92 | yield put(initCounterRequest());
93 | yield delay(1000);
94 |
95 | try {
96 | const {
97 | data: { value }
98 | }: { data: { value: number } } = yield readCounter();
99 |
100 | yield put(initCounterSuccess(value));
101 | } catch (e) {
102 | yield put(initCounterFailure());
103 | }
104 | }
105 |
106 | export function* saga() {
107 | yield all([
108 | fork(watchIncrementCounterAsync),
109 | fork(watchDecrementCounterAsync),
110 | fork(watchHandleResetCounterAsync),
111 | fork(watchUpdateCounter),
112 | fork(handleInitCounter)
113 | ]);
114 | }
115 |
--------------------------------------------------------------------------------
/src/counter/types/index.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "redux";
2 |
3 | export const INCREMENT_COUNTER = "@@counter/INCREMENT_COUNTER";
4 | export const INCREMENT_COUNTER_ASYNC = "@@counter/INCREMENT_COUNTER_ASYNC";
5 | export const DECREMENT_COUNTER = "@@counter/DECREMENT_COUNTER";
6 | export const DECREMENT_COUNTER_ASYNC = "@@counter/DECREMENT_COUNTER_ASYNC";
7 | export const RESET_COUNTER = "@@counter/RESET_COUNTER";
8 | export const RESET_COUNTER_ASYNC = "@@counter/RESET_COUNTER_ASYNC";
9 | export const INIT_COUNTER = "@@counter/INIT_COUNTER";
10 | export const INIT_COUNTER_REQUEST = "@@counter/INIT_COUNTER_REQUEST";
11 | export const INIT_COUNTER_SUCCESS = "@@counter/INIT_COUNTER_SUCCESS";
12 | export const INIT_COUNTER_FAILURE = "@@counter/INIT_COUNTER_FAILURE";
13 |
14 | export interface CounterState {
15 | value: number;
16 | }
17 |
18 | export type CounterStoredState = {
19 | readonly counter: CounterState;
20 | };
21 |
22 | export interface IncrementCounterAction extends Action {
23 | type: typeof INCREMENT_COUNTER;
24 | payload: {
25 | step: number;
26 | };
27 | }
28 |
29 | export interface IncrementCounterAsyncAction extends Action {
30 | type: typeof INCREMENT_COUNTER_ASYNC;
31 | payload: {
32 | step: number;
33 | };
34 | }
35 |
36 | export interface DecrementCounterAction extends Action {
37 | type: typeof DECREMENT_COUNTER;
38 | payload: {
39 | step: number;
40 | };
41 | }
42 |
43 | export interface DecrementCounterAsyncAction extends Action {
44 | type: typeof DECREMENT_COUNTER_ASYNC;
45 | payload: {
46 | step: number;
47 | };
48 | }
49 |
50 | export interface ResetCounterAction extends Action {
51 | type: typeof RESET_COUNTER;
52 | }
53 |
54 | export interface ResetCounterAsyncAction extends Action {
55 | type: typeof RESET_COUNTER_ASYNC;
56 | }
57 |
58 | export interface InitCounterRequestAction extends Action {
59 | type: typeof INIT_COUNTER_REQUEST;
60 | }
61 |
62 | export interface InitCounterSuccessAction extends Action {
63 | type: typeof INIT_COUNTER_SUCCESS;
64 | payload: {
65 | value: number;
66 | };
67 | }
68 |
69 | export interface InitCounterFailureAction extends Action {
70 | type: typeof INIT_COUNTER_FAILURE;
71 | }
72 |
73 | export type CounterActions =
74 | | IncrementCounterAction
75 | | IncrementCounterAsyncAction
76 | | DecrementCounterAction
77 | | DecrementCounterAsyncAction
78 | | ResetCounterAction
79 | | ResetCounterAsyncAction
80 | | InitCounterRequestAction
81 | | InitCounterSuccessAction
82 | | InitCounterFailureAction;
83 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import "semantic-ui-css/semantic.min.css";
2 |
3 | import React from "react";
4 | import ReactDOM from "react-dom";
5 | import { Provider } from "react-redux";
6 |
7 | import App from "./App";
8 | import * as serviceWorker from "./serviceWorker";
9 | import store from "./store";
10 |
11 | ReactDOM.render(
12 |
13 |
14 | ,
15 | document.getElementById("root")
16 | );
17 |
18 | // If you want your app to work offline and load faster, you can change
19 | // unregister() to register() below. Note this comes with some pitfalls.
20 | // Learn more about service workers: https://bit.ly/CRA-PWA
21 | serviceWorker.unregister();
22 |
--------------------------------------------------------------------------------
/src/loader/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useSelector } from "react-redux";
3 | import { Dimmer, Header, Icon, Loader as SUILoader } from "semantic-ui-react";
4 |
5 | import { getLoaderState } from "../reducers";
6 | import { LoaderState } from "../types";
7 |
8 | const Loader: React.FunctionComponent = ({ children }) => {
9 | const { hasError, isLoading }: LoaderState = useSelector(getLoaderState);
10 |
11 | return (
12 |
13 | {children}
14 |
15 |
16 |
17 |
18 |
19 |
20 | Error al cargar el contador
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default Loader;
28 |
--------------------------------------------------------------------------------
/src/loader/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from "redux";
2 |
3 | import {
4 | INIT_COUNTER_FAILURE,
5 | INIT_COUNTER_REQUEST,
6 | INIT_COUNTER_SUCCESS,
7 | } from "../../counter/types";
8 | import { LoaderActions, LoaderState, LoaderStoredState } from "../types";
9 |
10 | const initialState: LoaderState = {
11 | isLoading: false,
12 | hasError: false
13 | };
14 |
15 | export const getLoaderState = (store: LoaderStoredState): LoaderState =>
16 | store.loader;
17 |
18 | const reducer: Reducer = (
19 | state = initialState,
20 | action
21 | ) => {
22 | switch (action.type) {
23 | case INIT_COUNTER_REQUEST:
24 | return {
25 | ...state,
26 | isLoading: true,
27 | hasError: false
28 | };
29 | case INIT_COUNTER_SUCCESS:
30 | return {
31 | ...state,
32 | isLoading: false
33 | };
34 | case INIT_COUNTER_FAILURE:
35 | return {
36 | ...state,
37 | isLoading: false,
38 | hasError: true
39 | };
40 | default:
41 | return state;
42 | }
43 | };
44 |
45 | export default reducer;
46 |
--------------------------------------------------------------------------------
/src/loader/types/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InitCounterFailureAction,
3 | InitCounterRequestAction,
4 | InitCounterSuccessAction,
5 | } from "../../counter/types";
6 |
7 | export interface LoaderState {
8 | readonly isLoading: boolean;
9 | readonly hasError: boolean;
10 | }
11 |
12 | export type LoaderStoredState = {
13 | readonly loader: LoaderState;
14 | };
15 |
16 | export type LoaderActions =
17 | | InitCounterFailureAction
18 | | InitCounterRequestAction
19 | | InitCounterSuccessAction;
20 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import counterReducer from "./counter/reducers";
4 | import loaderReducer from "./loader/reducers";
5 | import { Actions, State } from "./types";
6 |
7 | const reducer = combineReducers({
8 | counter: counterReducer,
9 | loader: loaderReducer
10 | });
11 |
12 | export default reducer;
13 |
--------------------------------------------------------------------------------
/src/saga.ts:
--------------------------------------------------------------------------------
1 | import createSagaMiddleware from "@redux-saga/core";
2 | import { all, fork } from "redux-saga/effects";
3 |
4 | import { saga as counterSaga } from "./counter/sagas";
5 |
6 | export const sagaMiddleware = createSagaMiddleware();
7 |
8 | export function* saga() {
9 | yield all([fork(counterSaga)]);
10 | }
11 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, Store } from "redux";
2 | import { composeWithDevTools } from "redux-devtools-extension";
3 |
4 | import reducer from "./reducer";
5 | import { saga, sagaMiddleware } from "./saga";
6 | import { Actions, State } from "./types";
7 |
8 | const initialState: State = {
9 | counter: {
10 | value: 0
11 | },
12 | loader: {
13 | isLoading: false,
14 | hasError: false
15 | }
16 | };
17 |
18 | const store: Store = createStore(
19 | reducer,
20 | initialState,
21 | composeWithDevTools(applyMiddleware(sagaMiddleware))
22 | );
23 |
24 | sagaMiddleware.run(saga);
25 |
26 | export default store;
27 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { CounterActions, CounterStoredState } from "./counter/types";
2 | import { LoaderActions, LoaderStoredState } from "./loader/types";
3 |
4 | export type Actions = CounterActions & LoaderActions;
5 |
6 | export type State = CounterStoredState & LoaderStoredState;
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------