├── .gitignore ├── README.md ├── db └── countries.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── App.test.js ├── components └── CountriesContainer.js ├── index.css ├── index.js ├── redux ├── countries │ ├── countriesActions.js │ └── countriesReducer.js ├── index.js ├── rootReducer.js └── store.js └── serviceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-hooks-redux-thunk-pattern # 2 | 3 | ## Install dependencies 4 | 5 | ~~~ 6 | npm install 7 | ~~~ 8 | 9 | ## Usage 10 | 11 | Start the REST API server (JSON Server): 12 | ~~~ 13 | npm run server 14 | ~~~ 15 | 16 | Then, start the React development server: 17 | ~~~ 18 | npm start 19 | ~~~ -------------------------------------------------------------------------------- /db/countries.json: -------------------------------------------------------------------------------- 1 | { 2 | "countries": [ 3 | {"name": "Afghanistan", "id": "AF"}, 4 | {"name": "Åland Islands", "id": "AX"}, 5 | {"name": "Albania", "id": "AL"}, 6 | {"name": "Algeria", "id": "DZ"}, 7 | {"name": "American Samoa", "id": "AS"}, 8 | {"name": "Andorra", "id": "AD"}, 9 | {"name": "Angola", "id": "AO"}, 10 | {"name": "Anguilla", "id": "AI"}, 11 | {"name": "Antarctica", "id": "AQ"}, 12 | {"name": "Antigua and Barbuda", "id": "AG"}, 13 | {"name": "Argentina", "id": "AR"}, 14 | {"name": "Armenia", "id": "AM"}, 15 | {"name": "Aruba", "id": "AW"}, 16 | {"name": "Australia", "id": "AU"}, 17 | {"name": "Austria", "id": "AT"}, 18 | {"name": "Azerbaijan", "id": "AZ"}, 19 | {"name": "Bahamas", "id": "BS"}, 20 | {"name": "Bahrain", "id": "BH"}, 21 | {"name": "Bangladesh", "id": "BD"}, 22 | {"name": "Barbados", "id": "BB"}, 23 | {"name": "Belarus", "id": "BY"}, 24 | {"name": "Belgium", "id": "BE"}, 25 | {"name": "Belize", "id": "BZ"}, 26 | {"name": "Benin", "id": "BJ"}, 27 | {"name": "Bermuda", "id": "BM"}, 28 | {"name": "Bhutan", "id": "BT"}, 29 | {"name": "Bolivia", "id": "BO"}, 30 | {"name": "Bosnia and Herzegovina", "id": "BA"}, 31 | {"name": "Botswana", "id": "BW"}, 32 | {"name": "Bouvet Island", "id": "BV"}, 33 | {"name": "Brazil", "id": "BR"}, 34 | {"name": "British Indian Ocean Territory", "id": "IO"}, 35 | {"name": "Brunei Darussalam", "id": "BN"}, 36 | {"name": "Bulgaria", "id": "BG"}, 37 | {"name": "Burkina Faso", "id": "BF"}, 38 | {"name": "Burundi", "id": "BI"}, 39 | {"name": "Cambodia", "id": "KH"}, 40 | {"name": "Cameroon", "id": "CM"}, 41 | {"name": "Canada", "id": "CA"}, 42 | {"name": "Cape Verde", "id": "CV"}, 43 | {"name": "Cayman Islands", "id": "KY"}, 44 | {"name": "Central African Republic", "id": "CF"}, 45 | {"name": "Chad", "id": "TD"}, 46 | {"name": "Chile", "id": "CL"}, 47 | {"name": "China", "id": "CN"}, 48 | {"name": "Christmas Island", "id": "CX"}, 49 | {"name": "Cocos (Keeling) Islands", "id": "CC"}, 50 | {"name": "Colombia", "id": "CO"}, 51 | {"name": "Comoros", "id": "KM"}, 52 | {"name": "Congo", "id": "CG"}, 53 | {"name": "Congo, The Democratic Republic of the", "id": "CD"}, 54 | {"name": "Cook Islands", "id": "CK"}, 55 | {"name": "Costa Rica", "id": "CR"}, 56 | {"name": "Cote D\"Ivoire", "id": "CI"}, 57 | {"name": "Croatia", "id": "HR"}, 58 | {"name": "Cuba", "id": "CU"}, 59 | {"name": "Cyprus", "id": "CY"}, 60 | {"name": "Czech Republic", "id": "CZ"}, 61 | {"name": "Denmark", "id": "DK"}, 62 | {"name": "Djibouti", "id": "DJ"}, 63 | {"name": "Dominica", "id": "DM"}, 64 | {"name": "Dominican Republic", "id": "DO"}, 65 | {"name": "Ecuador", "id": "EC"}, 66 | {"name": "Egypt", "id": "EG"}, 67 | {"name": "El Salvador", "id": "SV"}, 68 | {"name": "Equatorial Guinea", "id": "GQ"}, 69 | {"name": "Eritrea", "id": "ER"}, 70 | {"name": "Estonia", "id": "EE"}, 71 | {"name": "Ethiopia", "id": "ET"}, 72 | {"name": "Falkland Islands (Malvinas)", "id": "FK"}, 73 | {"name": "Faroe Islands", "id": "FO"}, 74 | {"name": "Fiji", "id": "FJ"}, 75 | {"name": "Finland", "id": "FI"}, 76 | {"name": "France", "id": "FR"}, 77 | {"name": "French Guiana", "id": "GF"}, 78 | {"name": "French Polynesia", "id": "PF"}, 79 | {"name": "French Southern Territories", "id": "TF"}, 80 | {"name": "Gabon", "id": "GA"}, 81 | {"name": "Gambia", "id": "GM"}, 82 | {"name": "Georgia", "id": "GE"}, 83 | {"name": "Germany", "id": "DE"}, 84 | {"name": "Ghana", "id": "GH"}, 85 | {"name": "Gibraltar", "id": "GI"}, 86 | {"name": "Greece", "id": "GR"}, 87 | {"name": "Greenland", "id": "GL"}, 88 | {"name": "Grenada", "id": "GD"}, 89 | {"name": "Guadeloupe", "id": "GP"}, 90 | {"name": "Guam", "id": "GU"}, 91 | {"name": "Guatemala", "id": "GT"}, 92 | {"name": "Guernsey", "id": "GG"}, 93 | {"name": "Guinea", "id": "GN"}, 94 | {"name": "Guinea-Bissau", "id": "GW"}, 95 | {"name": "Guyana", "id": "GY"}, 96 | {"name": "Haiti", "id": "HT"}, 97 | {"name": "Heard Island and Mcdonald Islands", "id": "HM"}, 98 | {"name": "Holy See (Vatican City State)", "id": "VA"}, 99 | {"name": "Honduras", "id": "HN"}, 100 | {"name": "Hong Kong", "id": "HK"}, 101 | {"name": "Hungary", "id": "HU"}, 102 | {"name": "Iceland", "id": "IS"}, 103 | {"name": "India", "id": "IN"}, 104 | {"name": "Indonesia", "id": "ID"}, 105 | {"name": "Iran, Islamic Republic Of", "id": "IR"}, 106 | {"name": "Iraq", "id": "IQ"}, 107 | {"name": "Ireland", "id": "IE"}, 108 | {"name": "Isle of Man", "id": "IM"}, 109 | {"name": "Israel", "id": "IL"}, 110 | {"name": "Italy", "id": "IT"}, 111 | {"name": "Jamaica", "id": "JM"}, 112 | {"name": "Japan", "id": "JP"}, 113 | {"name": "Jersey", "id": "JE"}, 114 | {"name": "Jordan", "id": "JO"}, 115 | {"name": "Kazakhstan", "id": "KZ"}, 116 | {"name": "Kenya", "id": "KE"}, 117 | {"name": "Kiribati", "id": "KI"}, 118 | {"name": "Korea, Democratic People\"S Republic of", "id": "KP"}, 119 | {"name": "Korea, Republic of", "id": "KR"}, 120 | {"name": "Kuwait", "id": "KW"}, 121 | {"name": "Kyrgyzstan", "id": "KG"}, 122 | {"name": "Lao People\"S Democratic Republic", "id": "LA"}, 123 | {"name": "Latvia", "id": "LV"}, 124 | {"name": "Lebanon", "id": "LB"}, 125 | {"name": "Lesotho", "id": "LS"}, 126 | {"name": "Liberia", "id": "LR"}, 127 | {"name": "Libyan Arab Jamahiriya", "id": "LY"}, 128 | {"name": "Liechtenstein", "id": "LI"}, 129 | {"name": "Lithuania", "id": "LT"}, 130 | {"name": "Luxembourg", "id": "LU"}, 131 | {"name": "Macao", "id": "MO"}, 132 | {"name": "Macedonia, The Former Yugoslav Republic of", "id": "MK"}, 133 | {"name": "Madagascar", "id": "MG"}, 134 | {"name": "Malawi", "id": "MW"}, 135 | {"name": "Malaysia", "id": "MY"}, 136 | {"name": "Maldives", "id": "MV"}, 137 | {"name": "Mali", "id": "ML"}, 138 | {"name": "Malta", "id": "MT"}, 139 | {"name": "Marshall Islands", "id": "MH"}, 140 | {"name": "Martinique", "id": "MQ"}, 141 | {"name": "Mauritania", "id": "MR"}, 142 | {"name": "Mauritius", "id": "MU"}, 143 | {"name": "Mayotte", "id": "YT"}, 144 | {"name": "Mexico", "id": "MX"}, 145 | {"name": "Micronesia, Federated States of", "id": "FM"}, 146 | {"name": "Moldova, Republic of", "id": "MD"}, 147 | {"name": "Monaco", "id": "MC"}, 148 | {"name": "Mongolia", "id": "MN"}, 149 | {"name": "Montserrat", "id": "MS"}, 150 | {"name": "Morocco", "id": "MA"}, 151 | {"name": "Mozambique", "id": "MZ"}, 152 | {"name": "Myanmar", "id": "MM"}, 153 | {"name": "Namibia", "id": "NA"}, 154 | {"name": "Nauru", "id": "NR"}, 155 | {"name": "Nepal", "id": "NP"}, 156 | {"name": "Netherlands", "id": "NL"}, 157 | {"name": "Netherlands Antilles", "id": "AN"}, 158 | {"name": "New Caledonia", "id": "NC"}, 159 | {"name": "New Zealand", "id": "NZ"}, 160 | {"name": "Nicaragua", "id": "NI"}, 161 | {"name": "Niger", "id": "NE"}, 162 | {"name": "Nigeria", "id": "NG"}, 163 | {"name": "Niue", "id": "NU"}, 164 | {"name": "Norfolk Island", "id": "NF"}, 165 | {"name": "Northern Mariana Islands", "id": "MP"}, 166 | {"name": "Norway", "id": "NO"}, 167 | {"name": "Oman", "id": "OM"}, 168 | {"name": "Pakistan", "id": "PK"}, 169 | {"name": "Palau", "id": "PW"}, 170 | {"name": "Palestinian Territory, Occupied", "id": "PS"}, 171 | {"name": "Panama", "id": "PA"}, 172 | {"name": "Papua New Guinea", "id": "PG"}, 173 | {"name": "Paraguay", "id": "PY"}, 174 | {"name": "Peru", "id": "PE"}, 175 | {"name": "Philippines", "id": "PH"}, 176 | {"name": "Pitcairn", "id": "PN"}, 177 | {"name": "Poland", "id": "PL"}, 178 | {"name": "Portugal", "id": "PT"}, 179 | {"name": "Puerto Rico", "id": "PR"}, 180 | {"name": "Qatar", "id": "QA"}, 181 | {"name": "Reunion", "id": "RE"}, 182 | {"name": "Romania", "id": "RO"}, 183 | {"name": "Russian Federation", "id": "RU"}, 184 | {"name": "RWANDA", "id": "RW"}, 185 | {"name": "Saint Helena", "id": "SH"}, 186 | {"name": "Saint Kitts and Nevis", "id": "KN"}, 187 | {"name": "Saint Lucia", "id": "LC"}, 188 | {"name": "Saint Pierre and Miquelon", "id": "PM"}, 189 | {"name": "Saint Vincent and the Grenadines", "id": "VC"}, 190 | {"name": "Samoa", "id": "WS"}, 191 | {"name": "San Marino", "id": "SM"}, 192 | {"name": "Sao Tome and Principe", "id": "ST"}, 193 | {"name": "Saudi Arabia", "id": "SA"}, 194 | {"name": "Senegal", "id": "SN"}, 195 | {"name": "Serbia and Montenegro", "id": "CS"}, 196 | {"name": "Seychelles", "id": "SC"}, 197 | {"name": "Sierra Leone", "id": "SL"}, 198 | {"name": "Singapore", "id": "SG"}, 199 | {"name": "Slovakia", "id": "SK"}, 200 | {"name": "Slovenia", "id": "SI"}, 201 | {"name": "Solomon Islands", "id": "SB"}, 202 | {"name": "Somalia", "id": "SO"}, 203 | {"name": "South Africa", "id": "ZA"}, 204 | {"name": "South Georgia and the South Sandwich Islands", "id": "GS"}, 205 | {"name": "Spain", "id": "ES"}, 206 | {"name": "Sri Lanka", "id": "LK"}, 207 | {"name": "Sudan", "id": "SD"}, 208 | {"name": "Suriname", "id": "SR"}, 209 | {"name": "Svalbard and Jan Mayen", "id": "SJ"}, 210 | {"name": "Swaziland", "id": "SZ"}, 211 | {"name": "Sweden", "id": "SE"}, 212 | {"name": "Switzerland", "id": "CH"}, 213 | {"name": "Syrian Arab Republic", "id": "SY"}, 214 | {"name": "Taiwan, Province of China", "id": "TW"}, 215 | {"name": "Tajikistan", "id": "TJ"}, 216 | {"name": "Tanzania, United Republic of", "id": "TZ"}, 217 | {"name": "Thailand", "id": "TH"}, 218 | {"name": "Timor-Leste", "id": "TL"}, 219 | {"name": "Togo", "id": "TG"}, 220 | {"name": "Tokelau", "id": "TK"}, 221 | {"name": "Tonga", "id": "TO"}, 222 | {"name": "Trinidad and Tobago", "id": "TT"}, 223 | {"name": "Tunisia", "id": "TN"}, 224 | {"name": "Turkey", "id": "TR"}, 225 | {"name": "Turkmenistan", "id": "TM"}, 226 | {"name": "Turks and Caicos Islands", "id": "TC"}, 227 | {"name": "Tuvalu", "id": "TV"}, 228 | {"name": "Uganda", "id": "UG"}, 229 | {"name": "Ukraine", "id": "UA"}, 230 | {"name": "United Arab Emirates", "id": "AE"}, 231 | {"name": "United Kingdom", "id": "GB"}, 232 | {"name": "United States", "id": "US"}, 233 | {"name": "United States Minor Outlying Islands", "id": "UM"}, 234 | {"name": "Uruguay", "id": "UY"}, 235 | {"name": "Uzbekistan", "id": "UZ"}, 236 | {"name": "Vanuatu", "id": "VU"}, 237 | {"name": "Venezuela", "id": "VE"}, 238 | {"name": "Viet Nam", "id": "VN"}, 239 | {"name": "Virgin Islands, British", "id": "VG"}, 240 | {"name": "Virgin Islands, U.S.", "id": "VI"}, 241 | {"name": "Wallis and Futuna", "id": "WF"}, 242 | {"name": "Western Sahara", "id": "EH"}, 243 | {"name": "Yemen", "id": "YE"}, 244 | {"name": "Zambia", "id": "ZM"}, 245 | {"name": "Zimbabwe", "id": "ZW"} 246 | ] 247 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-redux-thunk-pattern", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "json-server": "^0.16.1", 8 | "react": "^16.12.0", 9 | "react-dom": "^16.12.0", 10 | "react-redux": "^7.1.1", 11 | "react-scripts": "3.2.0", 12 | "redux": "^4.0.4", 13 | "redux-logger": "^3.0.6", 14 | "redux-thunk": "^2.3.0" 15 | }, 16 | "scripts": { 17 | "server": "json-server --watch ./db/countries.json --port 3004", 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "engines": { 24 | "node": ">=10.0.0" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "redux-devtools-extension": "^2.13.8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silversoft77/react-hooks-redux-thunk/37d689183053fdf2d9f7b30d2e10da38d49fdab2/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/silversoft77/react-hooks-redux-thunk/37d689183053fdf2d9f7b30d2e10da38d49fdab2/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silversoft77/react-hooks-redux-thunk/37d689183053fdf2d9f7b30d2e10da38d49fdab2/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 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import store from './redux/store' 4 | import { CountriesContainer } from './components/CountriesContainer' 5 | 6 | function App () { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/CountriesContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useSelector, useDispatch } from 'react-redux' 3 | import { fetchCountries } from '../redux' 4 | 5 | export const CountriesContainer = () => { 6 | const loading = useSelector(state => state.countries.loading) 7 | const error = useSelector(state => state.countries.error) 8 | const countries = useSelector(state => state.countries.countries) 9 | const dispatch = useDispatch() 10 | 11 | useEffect(() => { 12 | dispatch(fetchCountries()) 13 | // eslint-disable-next-line react-hooks/exhaustive-deps 14 | }, []) 15 | 16 | return loading ? ( 17 |

Loading...

18 | ) : error ? ( 19 |

{error}

20 | ) : ( 21 |
22 |

Countries

23 |
24 | {countries.map((country, index) =>

 {country.name}

)} 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/redux/countries/countriesActions.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const fetchCountries = () => { 4 | return (dispatch) => { 5 | dispatch(fetchCountriesRequest()) 6 | axios 7 | .get('http://localhost:3004/countries') 8 | .then(response => { 9 | const countries = response.data 10 | setTimeout(() => { // to emulate some network delay 11 | dispatch(fetchCountriesSuccess(countries)) 12 | }, 2000) 13 | }) 14 | .catch(error => { 15 | dispatch(fetchCountriesFailure(error.message)) 16 | }) 17 | } 18 | } 19 | 20 | export const fetchCountriesRequest = () => { 21 | return { 22 | type: 'FETCH_COUNTRIES_REQUEST' 23 | } 24 | } 25 | 26 | export const fetchCountriesSuccess = countries => { 27 | return { 28 | type: 'FETCH_COUNTRIES_SUCCESS', 29 | payload: countries 30 | } 31 | } 32 | 33 | export const fetchCountriesFailure = error => { 34 | return { 35 | type: 'FETCH_COUNTRIES_FAILURE', 36 | payload: error 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/redux/countries/countriesReducer.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | loading: false, 3 | countries: [], 4 | error: '' 5 | } 6 | 7 | const reducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case 'FETCH_COUNTRIES_REQUEST': 10 | return { 11 | ...state, 12 | loading: true 13 | } 14 | case 'FETCH_COUNTRIES_SUCCESS': 15 | return { 16 | loading: false, 17 | countries: action.payload, 18 | error: '' 19 | } 20 | case 'FETCH_COUNTRIES_FAILURE': 21 | return { 22 | loading: false, 23 | countries: [], 24 | error: action.payload 25 | } 26 | default: return state 27 | } 28 | } 29 | 30 | export default reducer 31 | -------------------------------------------------------------------------------- /src/redux/index.js: -------------------------------------------------------------------------------- 1 | export { fetchCountries } from './countries/countriesActions' 2 | -------------------------------------------------------------------------------- /src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import countriesReducer from './countries/countriesReducer' 3 | 4 | const rootReducer = combineReducers({ 5 | countries: countriesReducer 6 | }) 7 | 8 | export default rootReducer 9 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { composeWithDevTools } from 'redux-devtools-extension' 3 | import logger from 'redux-logger' 4 | import thunk from 'redux-thunk' 5 | 6 | import rootReducer from './rootReducer' 7 | 8 | const store = createStore( 9 | rootReducer, 10 | composeWithDevTools(applyMiddleware(logger, thunk)) 11 | ) 12 | 13 | export default store 14 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 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 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------