├── .eslintignore
├── slides
├── 02.mdx
├── 05.mov
├── 05.png
├── 06.mdx
├── 03.mdx
├── 08.mdx
├── 07.mdx
├── 01.mdx
└── 04.mdx
├── .prettierignore
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── .prettierrc
├── .gitignore
├── src
├── index.js
├── firebase.js
├── old-app.js
└── app.js
├── README.md
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | coverage
4 | public
5 | .docz
6 |
--------------------------------------------------------------------------------
/slides/02.mdx:
--------------------------------------------------------------------------------
1 | ## Please Stand! ️️🏋
2 |
3 | > If you're physically able 💛 ♿️
4 |
--------------------------------------------------------------------------------
/slides/05.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/why-react-hooks/HEAD/slides/05.mov
--------------------------------------------------------------------------------
/slides/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/why-react-hooks/HEAD/slides/05.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | coverage
4 | public
5 | .docz
6 | package-lock.json
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kentcdodds/why-react-hooks/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/slides/06.mdx:
--------------------------------------------------------------------------------
1 | # Demo time!
2 |
3 | Checkout `src/app.js` and compare it to `src/old-app.js`
4 |
--------------------------------------------------------------------------------
/slides/03.mdx:
--------------------------------------------------------------------------------
1 | # What this talk is
2 |
3 | - The why of hooks
4 | - Code examples
5 |
6 | # What this talk is not
7 |
8 | - An introduction to hooks
9 |
--------------------------------------------------------------------------------
/slides/08.mdx:
--------------------------------------------------------------------------------
1 | # Thank you!
2 |
3 | 👋
4 |
5 | 🐦 @kentcdodds
6 |
7 | 💌 https://kentcdodds.com/subscribe
8 |
9 | Slides: https://github.com/kentcdodds/why-react-hooks
10 |
--------------------------------------------------------------------------------
/slides/07.mdx:
--------------------------------------------------------------------------------
1 | # Resources
2 |
3 | - 📑 https://reactjs.org/hooks
4 | - 🥚 https://kcd.im/hooks-and-suspense
5 | - 🏚 ➡ 🏠 https://kcd.im/refactor-react
6 | - 🤓 https://github.com/sw-yx/fresh-concurrent-react
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": false,
4 | "jsxBracketSameLine": false,
5 | "printWidth": 80,
6 | "proseWrap": "always",
7 | "semi": false,
8 | "singleQuote": true,
9 | "tabWidth": 2,
10 | "trailingComma": "all",
11 | "useTabs": false
12 | }
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/slides/01.mdx:
--------------------------------------------------------------------------------
1 | # Why React Hooks
2 |
3 | > Simplify your app with React Hooks
4 |
5 | 👋 I'm Kent C. Dodds
6 |
7 | - 🏡 Utah
8 | - 👩 👧 👦 👦 👦 🐕
9 | - 🏢 kentcdodds.com
10 | - 🐦/🐙 @kentcdodds
11 | - 🏆 testingjavascript.com
12 | - 🥚 kcd.im/egghead
13 | - 🥋 kcd.im/fem
14 | - 💌 kcd.im/news
15 | - 📝 kcd.im/blog
16 | - 📺 kcd.im/devtips
17 | - 💻 kcd.im/coding
18 | - 📽 kcd.im/youtube
19 | - 🎙 kcd.im/3-mins
20 | - ❓ kcd.im/ama
21 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'milligram'
2 | import React, {Suspense} from 'react'
3 | import ErrorBoundary from 'react-error-boundary'
4 | import ReactDOM from 'react-dom'
5 | import App from './app'
6 | // import App from './old-app'
7 |
8 | function ErrorFallback(props) {
9 | console.error(props)
10 | return 'there was an error'
11 | }
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 |
18 | ,
19 | document.getElementById('root'),
20 | )
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Why React Hooks?
2 |
3 | This is for my talk
4 | [Why React Hooks?](https://kentcdodds.com/talks/#why-react-hooks)
5 | which is intended to establish a case for why React Hooks
6 | are so useful and you should dedicate some time to learn
7 | about them.
8 |
9 | It consists of "slides" in the `./slides` directory and an
10 | example app that's implemented in both classes and hooks.
11 |
12 | You can find a recording of the talk
13 | [here](https://www.youtube.com/watch?v=zWsZcBiwgVE&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf)
14 | and you can find the deployed app
15 | [here](https://geo-chat.netlify.com/).
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "why-react-hooks",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@firebase/app-types": "0.4.6",
7 | "@firebase/util": "^0.2.30",
8 | "firebase": "7.2.1",
9 | "milligram": "1.3.0",
10 | "react": "16.10.2",
11 | "react-dom": "16.10.2",
12 | "react-error-boundary": "^1.2.5",
13 | "react-fns": "1.4.0",
14 | "react-scripts": "3.2.0"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": [
25 | ">0.2%",
26 | "not dead",
27 | "not ie <= 11",
28 | "not op_mini all"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/src/firebase.js:
--------------------------------------------------------------------------------
1 | import * as firebase from 'firebase/app'
2 | import 'firebase/database'
3 |
4 | try {
5 | firebase.initializeApp({
6 | apiKey: 'AIzaSyCXh3HpbB_jhMNuxc1vOsDkUUCIlRBNYm0',
7 | authDomain: 'geo-chat-7d7c6.firebaseapp.com',
8 | databaseURL: 'https://geo-chat-7d7c6.firebaseio.com',
9 | projectId: 'geo-chat-7d7c6',
10 | storageBucket: '',
11 | messagingSenderId: '453185349176',
12 | })
13 | } catch (e) {}
14 |
15 | function addMessage({latitude, longitude, content, username}) {
16 | const locationId = getLocationId({latitude, longitude})
17 | firebase
18 | .database()
19 | .ref(`messages/${locationId}/posts`)
20 | .push({
21 | date: Date.now(),
22 | content,
23 | username,
24 | latitude,
25 | longitude,
26 | })
27 | }
28 |
29 | function subscribe({latitude, longitude}, callback) {
30 | const locationId = getLocationId({latitude, longitude})
31 | const ref = firebase.database().ref(`messages/${locationId}/posts`)
32 | console.log('registering with locationid of ', locationId)
33 | ref.on('value', snapshot =>
34 | callback(
35 | Object.entries(snapshot.val() || {}).map(([id, data]) => ({id, ...data})),
36 | ),
37 | )
38 | return () => ref.off('value', callback)
39 | }
40 |
41 | const getLocationId = ({latitude, longitude}) =>
42 | `${(latitude * 10).toFixed()}_${(longitude * 10).toFixed()}`
43 |
44 | export {subscribe, addMessage}
45 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/slides/04.mdx:
--------------------------------------------------------------------------------
1 | # What makes React so hard?
2 |
3 | - 📦 JavaScript
4 | - 🔄 Lifecycles
5 | - ⬛️ Logic Reuse (patterns)
6 |
7 |
22 |
23 | ## 📦 JavaScript
24 |
25 | ```javascript
26 | // find the 🐛
27 | import React from 'react'
28 |
29 | class Counter extends React.Component {
30 | constructor() {
31 | super()
32 | this.state = {count: this.props.initialCount}
33 | }
34 | increment() {
35 | this.setState(({count}) => ({count: count + 1}))
36 | }
37 | render() {
38 | return (
39 |
42 | )
43 | }
44 | }
45 | ```
46 |
47 | TRICK QUESTION! THERE WERE TWO 🐛 🐜! DID YOU FIND THEM?
48 |
49 | ## 🔄 Lifecycles
50 |
51 | ```javascript
52 | import React from 'react'
53 | import UserContext from '../user-context'
54 |
55 | class ChatFeed extends React.Component {
56 | static contextType = UserContext
57 | state = {isScrolledToBottom: true}
58 | componentDidMount() {
59 | this.subscribeToFeed()
60 | this.setDocumentTitle()
61 | this.subscribeToOnlineStatus()
62 | this.subscribeToGeoLocation()
63 | }
64 | componentWillUnmount() {
65 | this.unsubscribeFromFeed()
66 | this.restoreDocumentTitle()
67 | this.unsubscribeFromOnlineStatus()
68 | this.unsubscribeFromGeoLocation()
69 | }
70 | componentDidUpdate(prevProps, prevState) {
71 | // ... compare props and re-subscribe etc.
72 | }
73 | render() {
74 | // ...
75 | }
76 | }
77 | ```
78 |
79 | ## ⬛️ Logic Reuse
80 |
81 | ```javascript
82 | import React from 'react'
83 | import UserContext from '../user-context'
84 |
85 | function ChatFeed({feedId}) {
86 | return (
87 |
88 | {user => (
89 |
90 | {posts => (
91 | <>
92 |
93 |
94 | {isOnline => (
95 |
96 | {location => (
97 | <>
98 |
99 |
100 |
101 |
102 | >
103 | )}
104 |
105 | )}
106 |
107 | >
108 | )}
109 |
110 | )}
111 |
112 | )
113 | }
114 | ```
115 |
116 | ... LOL 😂🤣 ... 😢😭😭😭😭
117 |
--------------------------------------------------------------------------------
/src/old-app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {withGeoPosition} from 'react-fns'
3 | import * as firebase from './firebase'
4 |
5 | function checkInView(element, container = element.parentElement) {
6 | const cTop = container.scrollTop
7 | const cBottom = cTop + container.clientHeight
8 | const eTop = element.offsetTop - container.offsetTop
9 | const eBottom = eTop + element.clientHeight
10 | const isTotal = eTop >= cTop && eBottom <= cBottom
11 | const isPartial =
12 | (eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom)
13 | return isTotal || isPartial
14 | }
15 |
16 | class App extends React.Component {
17 | static defaultProps = {
18 | latitude: 40,
19 | longitude: -111,
20 | }
21 | state = {
22 | username: window.localStorage.getItem('geo-chat:username'),
23 | messages: [],
24 | seenNodes: [],
25 | isStuck: true,
26 | }
27 | messagesContainerRef = React.createRef()
28 | sendMessage = e => {
29 | e.preventDefault()
30 | firebase.addMessage({
31 | latitude: this.props.latitude,
32 | longitude: this.props.longitude,
33 | username: this.state.username || 'anonymous',
34 | content: e.target.elements.message.value,
35 | })
36 | e.target.elements.message.value = ''
37 | e.target.elements.message.focus()
38 | }
39 | componentDidMount() {
40 | this.trackVisibleChildren()
41 | this.trackStuck()
42 | this.updateDocumentTitle()
43 | this.subscribeToFirebase()
44 | }
45 | componentDidUpdate(prevProps, prevState) {
46 | this.trackVisibleChildren()
47 | if (
48 | this.state.messages.length !== prevState.messages &&
49 | this.state.isStuck
50 | ) {
51 | this.stickContainer()
52 | }
53 | this.updateDocumentTitle()
54 | if (
55 | prevProps.longitude !== this.props.longitude ||
56 | prevProps.latitude !== this.props.latitude
57 | ) {
58 | this.unsubscribeFromFirebase()
59 | this.subscribeToFirebase()
60 | }
61 | }
62 | updateDocumentTitle() {
63 | const unreadCount = this.state.messages.length - this.state.seenNodes.length
64 | document.title = unreadCount ? `Unread: ${unreadCount}` : 'All read'
65 | console.log(document.title)
66 | }
67 | stickContainer() {
68 | this.messagesContainerRef.current.scrollTop = this.messagesContainerRef.current.scrollHeight
69 | }
70 | subscribeToFirebase() {
71 | const {latitude, longitude} = this.props
72 | this.unsubscribeFromFirebase = firebase.subscribe(
73 | {latitude, longitude},
74 | messages => {
75 | this.setState({messages: messages})
76 | },
77 | )
78 | }
79 | trackStuck() {
80 | const handleScroll = () => {
81 | const {
82 | clientHeight,
83 | scrollTop,
84 | scrollHeight,
85 | } = this.messagesContainerRef.current
86 | const partialPixelBuffer = 10
87 | const scrolledUp =
88 | clientHeight + scrollTop < scrollHeight - partialPixelBuffer
89 | console.log(scrolledUp)
90 | this.setState({isStuck: !scrolledUp})
91 | }
92 | this.messagesContainerRef.current.addEventListener('scroll', handleScroll)
93 | this.untrackStuck = () =>
94 | this.messagesContainerRef.current.removeEventListener(
95 | 'scroll',
96 | handleScroll,
97 | )
98 | }
99 | trackVisibleChildren() {
100 | const newVisibleChildren = Array.from(
101 | this.messagesContainerRef.current.children,
102 | )
103 | .filter(n => !this.state.seenNodes.includes(n))
104 | .filter(n => checkInView(n, this.messagesContainerRef.current))
105 | if (newVisibleChildren.length) {
106 | this.setState(({seenNodes}) => ({
107 | seenNodes: Array.from(new Set([...seenNodes, ...newVisibleChildren])),
108 | }))
109 | }
110 | }
111 | handleUsernameChange = e => {
112 | const username = e.target.value
113 | this.setState({username})
114 | window.localStorage.setItem('geo-chat:username', username)
115 | }
116 | componentWillUnmount() {
117 | this.untrackStuck()
118 | this.unsubscribeFromFirebase()
119 | }
120 | render() {
121 | const {latitude, longitude} = this.props
122 | return (
123 |
124 |
125 |
131 |
136 |
{JSON.stringify({latitude, longitude}, null, 2)}
137 |
148 | {this.state.messages.map(message => (
149 |
150 | {message.username}: {message.content}
151 |
152 | ))}
153 |
154 |
155 | )
156 | }
157 | }
158 |
159 | export default withGeoPosition(({isLoading, coords, error}) =>
160 | isLoading ? 'loading...' : error ? 'there was an error' : ,
161 | )
162 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as firebase from './firebase'
3 |
4 | function App() {
5 | const {
6 | coords: {latitude, longitude},
7 | } = useGeoPosition()
8 | const messagesContainerRef = React.useRef()
9 | const [messages, setMessages] = React.useState([])
10 | const [username, setUsername] = useLocalStorageState('', {
11 | key: 'geo-chat:username',
12 | })
13 | useStickyScrollContainer(messagesContainerRef, [messages.length])
14 | const visibleNodes = useVisibilityCounter(messagesContainerRef)
15 | const unreadCount = messages.length - visibleNodes.length
16 |
17 | React.useEffect(() => {
18 | const unsubscribe = firebase.subscribe({latitude, longitude}, messages => {
19 | setMessages(messages)
20 | })
21 | return () => {
22 | unsubscribe()
23 | }
24 | }, [latitude, longitude])
25 |
26 | React.useEffect(() => {
27 | document.title = unreadCount ? `Unread: ${unreadCount}` : 'All read'
28 | }, [unreadCount])
29 |
30 | const initialDocumentTitle = React.useRef(document.title)
31 | React.useEffect(() => {
32 | return () => {
33 | document.title = initialDocumentTitle
34 | }
35 | }, [])
36 |
37 | function sendMessage(e) {
38 | e.preventDefault()
39 | firebase.addMessage({
40 | latitude,
41 | longitude,
42 | username: username || 'anonymous',
43 | content: e.target.elements.message.value,
44 | })
45 | e.target.elements.message.value = ''
46 | e.target.elements.message.focus()
47 | }
48 |
49 | function handleUsernameChange(e) {
50 | const username = e.target.value
51 | setUsername(username)
52 | }
53 |
54 | return (
55 |
56 |
57 |
63 |
68 |
{JSON.stringify({latitude, longitude}, null, 2)}
69 |
80 | {messages.map(message => (
81 |
82 | {message.username}: {message.content}
83 |
84 | ))}
85 |
86 |
87 | )
88 | }
89 |
90 | function useLocalStorageState(
91 | initialValue,
92 | {key, serializer = v => v, deserializer = v => v},
93 | ) {
94 | const [state, setState] = React.useState(
95 | () => deserializer(window.localStorage.getItem(key)) || initialValue,
96 | )
97 |
98 | const serializedState = serializer(state)
99 | React.useEffect(() => {
100 | window.localStorage.setItem(key, serializedState)
101 | }, [key, serializedState])
102 |
103 | return [state, setState]
104 | }
105 |
106 | function useGeoPosition(options) {
107 | const [position, setPosition] = React.useState(getInitialPosition(options))
108 |
109 | const setError = useErrorBoundaryError()
110 |
111 | React.useEffect(() => {
112 | const watch = navigator.geolocation.watchPosition(
113 | setPosition,
114 | setError,
115 | options,
116 | )
117 |
118 | return () => navigator.geolocation.clearWatch(watch)
119 | }, [options, setError])
120 |
121 | return position
122 | }
123 |
124 | function useStickyScrollContainer(scrollContainerRef, inputs = []) {
125 | const [isStuck, setStuck] = React.useState(true)
126 | React.useEffect(() => {
127 | const scrollContainer = scrollContainerRef.current
128 | function handleScroll() {
129 | const {clientHeight, scrollTop, scrollHeight} = scrollContainer
130 | const partialPixelBuffer = 10
131 | const scrolledUp =
132 | clientHeight + scrollTop < scrollHeight - partialPixelBuffer
133 | setStuck(!scrolledUp)
134 | }
135 | scrollContainer.addEventListener('scroll', handleScroll)
136 | return () => scrollContainer.removeEventListener('scroll', handleScroll)
137 | }, [scrollContainerRef])
138 |
139 | const scrollHeight = scrollContainerRef.current
140 | ? scrollContainerRef.current.scrollHeight
141 | : 0
142 |
143 | React.useEffect(() => {
144 | const scrollContainer = scrollContainerRef.current
145 | if (isStuck) {
146 | scrollContainer.scrollTop = scrollHeight
147 | }
148 | // ignoring this rule is dangerous and not recommended
149 | // but sometimes it's the only way you can create the API you want.
150 | // 99% of the time you should not disable this rule.
151 | // eslint-disable-next-line react-hooks/exhaustive-deps
152 | }, [isStuck, scrollContainerRef, scrollHeight, ...inputs])
153 |
154 | return isStuck
155 | }
156 |
157 | function checkInView(element, container = element.parentElement) {
158 | const cTop = container.scrollTop
159 | const cBottom = cTop + container.clientHeight
160 | const eTop = element.offsetTop - container.offsetTop
161 | const eBottom = eTop + element.clientHeight
162 | const isTotal = eTop >= cTop && eBottom <= cBottom
163 | const isPartial =
164 | (eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom)
165 | return isTotal || isPartial
166 | }
167 |
168 | function useVisibilityCounter(containerRef) {
169 | const [seenNodes, setSeenNodes] = React.useState([])
170 |
171 | React.useEffect(() => {
172 | const newVisibleChildren = Array.from(containerRef.current.children)
173 | .filter(n => !seenNodes.includes(n))
174 | .filter(n => checkInView(n, containerRef.current))
175 | if (newVisibleChildren.length) {
176 | setSeenNodes(seen =>
177 | Array.from(new Set([...seen, ...newVisibleChildren])),
178 | )
179 | }
180 | }, [containerRef, seenNodes])
181 |
182 | return seenNodes
183 | }
184 |
185 | let initialPosition
186 | function getInitialPosition(options) {
187 | if (!initialPosition) {
188 | // supsense magic...
189 | throw new Promise((resolve, reject) => {
190 | navigator.geolocation.getCurrentPosition(
191 | position => {
192 | initialPosition = position
193 | resolve(position)
194 | },
195 | error => reject(error),
196 | options,
197 | )
198 | })
199 | }
200 | return initialPosition
201 | }
202 |
203 | function useErrorBoundaryError() {
204 | const [error, setError] = React.useState(null)
205 |
206 | if (error) {
207 | // clear out the error
208 | setError(null)
209 | // let the error boundary catch this
210 | throw error
211 | }
212 |
213 | return setError
214 | }
215 |
216 | export default App
217 |
--------------------------------------------------------------------------------