├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ ├── card │ │ ├── card.component.jsx │ │ └── card.css │ ├── post │ │ └── post.component.jsx │ ├── sagas-example │ │ └── sagas-example.component.jsx │ ├── use-effect-example │ │ └── use-effect-example.component.jsx │ ├── use-state-example │ │ └── use-state-example.component.jsx │ └── user │ │ └── user.component.jsx ├── effects │ └── use-fetch.effect.js ├── index.css ├── index.js ├── logo.svg ├── redux │ ├── app.reducer.js │ ├── app.saga.js │ ├── root-reducer.js │ └── store.js └── serviceWorker.js └── 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Custom hook example 2 | 3 | A small application showing how to write a custom hook to help us fetch data 4 | 5 | # How to fork and clone 6 | 7 | One quick note about cloning this project. If you wish to make commits and push the code up after cloning this repo, you should fork the project first. In order to own your own copy of this repository, you have to fork it so you get your own copy on your own profile! 8 | 9 | You can see the fork button in the top right corner of every GitHub project; click it and a copy of the project will be added to your GitHub profile under the same name as the original project. 10 | 11 | ![alt text](https://i.ibb.co/1YN7SJ6/Screen-Shot-2019-07-01-at-2-02-40-AM.png "image to fork button") 12 | 13 | After forking the project, simply clone it the way you would from the new forked project in your own GitHub repository and you can commit and push to it freely! 14 | 15 | 16 | # After you fork and clone: 17 | 18 | ## Install dependencies 19 | 20 | In your terminal after you clone your project down, remember to run either `yarn` or `npm install` to build all the dependencies in the project. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-redux": "7.0.3", 9 | "redux": "4.0.1", 10 | "redux-logger": "3.0.6", 11 | "redux-saga": "1.0.2", 12 | "styled-components": "4.2.0" 13 | }, 14 | "devDependencies": { 15 | "react-scripts": "3.0.1" 16 | }, 17 | "scripts": { 18 | "start": "PORT=4000 react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangMYihua/custom-hook-example/945834ee5201dfbbc3210da094970f2e7ba52f7a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | height: 100vh; 7 | } 8 | 9 | .App > .card { 10 | margin-top: 25px; 11 | } 12 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import User from './components/user/user.component'; 4 | import Post from './components/post/post.component'; 5 | 6 | import './App.css'; 7 | 8 | const App = props => { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /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/card/card.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './card.css'; 4 | 5 | const Card = ({ children }) =>
{children}
; 6 | 7 | export default Card; 8 | -------------------------------------------------------------------------------- /src/components/card/card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: lightblue; 3 | min-width: 300px; 4 | max-width: 600px; 5 | min-height: 180px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | border-radius: 10px; 10 | flex-direction: column; 11 | padding: 30px; 12 | font-size: 20px; 13 | } 14 | 15 | .card > button { 16 | background-color: white; 17 | border: 1px solid black; 18 | cursor: pointer; 19 | border-radius: 5px; 20 | min-width: 90px; 21 | min-height: 30px; 22 | font-size: 16px; 23 | margin: 10px 0; 24 | padding: 10px; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/post/post.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Card from '../card/card.component'; 4 | 5 | import useFetch from '../../effects/use-fetch.effect'; 6 | 7 | const Post = ({ postId }) => { 8 | const post = useFetch( 9 | `https://jsonplaceholder.typicode.com/posts?id=${postId}` 10 | ); 11 | 12 | return ( 13 | 14 | {post ? ( 15 |
16 |

{post.title}

17 |

{post.body}

18 |
19 | ) : ( 20 |

No post found

21 | )} 22 |
23 | ); 24 | }; 25 | 26 | export default Post; 27 | -------------------------------------------------------------------------------- /src/components/sagas-example/sagas-example.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import Card from '../card/card.component'; 5 | 6 | const SagasExample = ({ increment, decrement, value }) => ( 7 | 8 | {value} 9 | 10 | 11 | 12 | ); 13 | 14 | const mapStateToProps = state => ({ 15 | value: state.app.value 16 | }); 17 | 18 | const mapDispatchToProps = dispatch => ({ 19 | increment: () => dispatch({ type: 'INCREMENT' }), 20 | decrement: () => dispatch({ type: 'DECREMENT' }) 21 | }); 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(SagasExample); 27 | -------------------------------------------------------------------------------- /src/components/use-effect-example/use-effect-example.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import Card from '../card/card.component'; 4 | 5 | const UseEffectExample = () => { 6 | const [user, setUser] = useState(null); 7 | const [searchQuery, setSearchQuery] = useState('Bret'); 8 | 9 | useEffect(() => { 10 | const fetchFunc = async () => { 11 | const response = await fetch( 12 | `https://jsonplaceholder.typicode.com/users?username=${searchQuery}` 13 | ); 14 | const resJson = await response.json(); 15 | setUser(resJson[0]); 16 | }; 17 | 18 | fetchFunc(); 19 | }, [searchQuery]); 20 | 21 | return ( 22 | 23 | setSearchQuery(event.target.value)} 27 | /> 28 | {user ? ( 29 |
30 |

{user.name}

31 |

{user.username}

32 |

{user.email}

33 |
34 | ) : ( 35 |

No user found

36 | )} 37 |
38 | ); 39 | }; 40 | 41 | export default UseEffectExample; 42 | -------------------------------------------------------------------------------- /src/components/use-state-example/use-state-example.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Card from '../card/card.component'; 4 | 5 | const UseStateExample = () => { 6 | const [name, setName] = useState('Yihua'); 7 | const [address, setAddress] = useState('Amsterdam'); 8 | 9 | return ( 10 | 11 |

{name}

12 |

{address}

13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export class StateClassComponent extends React.Component { 20 | constructor() { 21 | super(); 22 | 23 | this.state = { 24 | name: 'Yihua', 25 | address: 'Canada' 26 | }; 27 | } 28 | 29 | render() { 30 | return ( 31 | 32 |

{this.state.name}

33 | 36 | 39 |
40 | ); 41 | } 42 | } 43 | 44 | export default UseStateExample; 45 | -------------------------------------------------------------------------------- /src/components/user/user.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Card from '../card/card.component'; 4 | 5 | import useFetch from '../../effects/use-fetch.effect'; 6 | 7 | const User = ({ userId }) => { 8 | const user = useFetch( 9 | `https://jsonplaceholder.typicode.com/users?id=${userId}` 10 | ); 11 | 12 | return ( 13 | 14 | {user ? ( 15 |
16 |

{user.username}

17 |

{user.name}

18 |
19 | ) : ( 20 |

User not found

21 | )} 22 |
23 | ); 24 | }; 25 | 26 | export default User; 27 | -------------------------------------------------------------------------------- /src/effects/use-fetch.effect.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useFetch = url => { 4 | const [data, setData] = useState(null); 5 | 6 | useEffect(() => { 7 | const fetchData = async () => { 8 | const res = await fetch(url); 9 | const dataArray = await res.json(); 10 | setData(dataArray[0]); 11 | }; 12 | 13 | fetchData(); 14 | }, [url]); 15 | 16 | return data; 17 | }; 18 | 19 | export default useFetch; 20 | -------------------------------------------------------------------------------- /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 { Provider } from 'react-redux'; 4 | 5 | import './index.css'; 6 | 7 | import App from './App'; 8 | 9 | import { store } from './redux/store'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/redux/app.reducer.js: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { 2 | value: 0 3 | }; 4 | 5 | const appReducer = (state = INITIAL_STATE, action) => { 6 | switch (action.type) { 7 | case 'INCREMENT_FROM_SAGA': 8 | return { 9 | ...state, 10 | value: state.value + 1 11 | }; 12 | case 'DECREMENT': 13 | return { 14 | ...state, 15 | value: state.value - 1 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default appReducer; 23 | -------------------------------------------------------------------------------- /src/redux/app.saga.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, delay, put } from 'redux-saga/effects'; 2 | 3 | export function* onIncrement() { 4 | yield console.log('I am incremented'); 5 | yield delay(3000); 6 | yield put({ type: 'INCREMENT_FROM_SAGA' }); 7 | } 8 | 9 | export function* incrementSaga() { 10 | yield takeLatest('INCREMENT', onIncrement); 11 | } 12 | -------------------------------------------------------------------------------- /src/redux/root-reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import appReducer from './app.reducer'; 4 | 5 | const rootReducer = combineReducers({ 6 | app: appReducer 7 | }); 8 | 9 | export default rootReducer; 10 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import logger from 'redux-logger'; 4 | 5 | import rootReducer from './root-reducer'; 6 | 7 | import { incrementSaga } from './app.saga'; 8 | 9 | const sagaMiddleware = createSagaMiddleware(); 10 | 11 | const middlewares = [logger, sagaMiddleware]; 12 | 13 | export const store = createStore(rootReducer, applyMiddleware(...middlewares)); 14 | 15 | sagaMiddleware.run(incrementSaga); 16 | 17 | export default store; 18 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------