├── .babelrc ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .prettierrc ├── README.md ├── db └── index.js ├── next.config.js ├── package.json ├── pages ├── 404.js ├── _app.js ├── _document.js ├── api │ └── posts │ │ ├── [postId].js │ │ └── index.js └── index.js ├── public ├── favicon.ico └── zeit.svg ├── src ├── components │ ├── GlobalLoader.js │ ├── PostForm.js │ ├── Sidebar.js │ └── styled.js ├── hooks │ ├── useCreatePost.js │ ├── useDeletePost.js │ ├── usePost.js │ ├── usePosts.js │ └── useSavePost.js ├── index.js └── screens │ ├── admin │ ├── Post.js │ └── index.js │ └── blog │ ├── Post.js │ └── index.js ├── store.json ├── utils.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["styled-components"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["react-app", "prettier"], 4 | "env": { 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "jsx-a11y/anchor-is-valid": 0, 12 | "eqeqeq": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Learn With Jason 4 | 5 |

6 |

7 | All About React Query (with Tanner Linsley) 8 |

9 |

10 | This app was built live on Learn With Jason and it was super fun and I’m sad you weren’t there. 11 |

12 |

13 | But don’t worry! You can still: 14 | watch the video · 15 | see upcoming episodes 16 |

17 | 18 | > **NOTE:** This repo was forked from [tannerlinsley/react-summit-2020-react-query](https://github.com/tannerlinsley/react-summit-2020-react-query) 19 | 20 |   21 | 22 | There‘s a lot of buzz about React Query and how much it can simplify your development workflow. In this episode, we’ll learn all about it from the #TanStack creator himself! 23 | 24 |   25 | 26 | ## More Information 27 | 28 | - [Watch this app get built live + see links and additional resources][episode] 29 | - [Follow _Learn With Jason_ on Twitch][twitch] to watch future episodes live 30 | - [Add the _Learn With Jason_ schedule to your Google Calendar][cal] 31 | 32 | [episode]: https://www.learnwithjason.dev/all-about-react-query 33 | [twitch]: https://jason.af/twitch 34 | [cal]: https://jason.af/lwj/cal -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | const storeLocation = path.resolve(process.cwd(), 'store.json') 5 | 6 | export default { 7 | set, 8 | get, 9 | } 10 | 11 | async function set(updater) { 12 | const file = await fs.readJSON(storeLocation) 13 | const newFile = updater(file) 14 | await fs.writeJSON(storeLocation, newFile) 15 | } 16 | 17 | function get() { 18 | return fs.readJSON(storeLocation) 19 | } 20 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const Module = require('module') 2 | const path = require('path') 3 | const resolveFrom = require('resolve-from') 4 | 5 | const node_modules = path.resolve(__dirname, 'node_modules') 6 | 7 | const originalRequire = Module.prototype.require 8 | 9 | // The following ensures that there is always only a single (and same) 10 | // copy of React in an app at any given moment. 11 | Module.prototype.require = function (modulePath) { 12 | // Only redirect resolutions to non-relative and non-absolute modules 13 | if ( 14 | ['/react/', '/react-dom/', '/react-query/'].some((d) => { 15 | try { 16 | return require.resolve(modulePath).includes(d) 17 | } catch (err) { 18 | return false 19 | } 20 | }) 21 | ) { 22 | try { 23 | modulePath = resolveFrom(node_modules, modulePath) 24 | } catch (err) { 25 | // 26 | } 27 | } 28 | 29 | return originalRequire.call(this, modulePath) 30 | } 31 | 32 | module.exports = { 33 | target: 'serverless', 34 | async rewrites() { 35 | return [ 36 | { 37 | source: '/api/:any*', 38 | destination: '/api/:any*', 39 | }, 40 | { 41 | source: '/:any*', 42 | destination: '/', 43 | }, 44 | ] 45 | }, 46 | webpack: (config) => { 47 | config.resolve = { 48 | ...config.resolve, 49 | alias: { 50 | ...config.resolve.alias, 51 | react$: resolveFrom(path.resolve('node_modules'), 'react'), 52 | 'react-query$': resolveFrom( 53 | path.resolve('node_modules'), 54 | 'react-query' 55 | ), 56 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'), 57 | }, 58 | } 59 | return config 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.20.0", 12 | "fs-extra": "^9.0.1", 13 | "history": "^5.0.0", 14 | "next": "9.5.4", 15 | "react": "16.13.1", 16 | "react-dom": "16.13.1", 17 | "react-icons": "^3.11.0", 18 | "react-query": "^2.23.1", 19 | "react-query-devtools": "^2.5.1", 20 | "react-router-dom": "^6.0.0-beta.0", 21 | "shortid": "^2.2.15", 22 | "styled-components": "^5.2.0", 23 | "styled-normalize": "^8.0.7" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.26.3", 27 | "babel-eslint": "10.1.0", 28 | "eslint": "6.6.0", 29 | "eslint-config-prettier": "^6.7.0", 30 | "eslint-config-react-app": "^5.0.2", 31 | "eslint-config-standard": "^14.1.0", 32 | "eslint-config-standard-react": "^9.2.0", 33 | "eslint-plugin-flowtype": "4.4.1", 34 | "eslint-plugin-import": "2.18.2", 35 | "eslint-plugin-jsx-a11y": "6.2.3", 36 | "eslint-plugin-node": "^10.0.0", 37 | "eslint-plugin-prettier": "^3.1.1", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-react": "7.16.0", 40 | "eslint-plugin-react-hooks": "2.3.0", 41 | "eslint-plugin-standard": "^4.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => ( 4 |
9 |

15 | 404 16 |

17 |

22 | Page not found 23 |

24 |
25 | ) 26 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import App from 'next/app' 3 | import { createGlobalStyle } from 'styled-components' 4 | import normalize from 'styled-normalize' 5 | 6 | // 7 | 8 | const GlobalStyles = createGlobalStyle` 9 | ${normalize}; 10 | html, body, body, [data-reactroot] { 11 | min-height: 100%; 12 | max-width: 100%; 13 | } 14 | 15 | html, body { 16 | width: 100%; 17 | font-size: 16px; 18 | font-family: "Helvetica", "Georgia", sans-serif; 19 | } 20 | 21 | * { 22 | box-sizing: border-box; 23 | } 24 | 25 | input { 26 | max-width: 100%; 27 | } 28 | 29 | a { 30 | text-decoration: none; 31 | cursor: pointer; 32 | } 33 | ` 34 | 35 | export default class MyApp extends App { 36 | render() { 37 | const { Component, pageProps } = this.props 38 | return ( 39 | <> 40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Document, { Html, Head, Main, NextScript } from 'next/document' 3 | import { ServerStyleSheet } from 'styled-components' 4 | 5 | export default class MyDocument extends Document { 6 | static async getInitialProps(ctx) { 7 | const sheet = new ServerStyleSheet() 8 | const originalRenderPage = ctx.renderPage 9 | 10 | try { 11 | ctx.renderPage = () => 12 | originalRenderPage({ 13 | enhanceApp: (App) => (props) => 14 | sheet.collectStyles(), 15 | }) 16 | 17 | const initialProps = await Document.getInitialProps(ctx) 18 | 19 | return { 20 | ...initialProps, 21 | styles: ( 22 | <> 23 | {initialProps.styles} 24 | {sheet.getStyleElement()} 25 | 26 | ), 27 | } 28 | } finally { 29 | sheet.seal() 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 | 37 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pages/api/posts/[postId].js: -------------------------------------------------------------------------------- 1 | import db from '../../../db' 2 | import { sleep } from '../../../utils' 3 | 4 | const deleteFailureRate = 0 5 | 6 | export default async (req, res) => { 7 | await sleep(1000) 8 | 9 | try { 10 | if (req.method === 'GET') { 11 | return await GET(req, res) 12 | } else if (req.method === 'PATCH') { 13 | return await PATCH(req, res) 14 | } else if (req.method === 'DELETE') { 15 | return await DELETE(req, res) 16 | } 17 | } catch (err) { 18 | console.error(err) 19 | res.status(500) 20 | res.json({ message: 'An unknown error occurred!' }) 21 | } 22 | } 23 | 24 | async function GET(req, res) { 25 | const { 26 | query: { postId }, 27 | } = req 28 | 29 | const row = (await db.get()).posts.find((d) => d.id == postId) 30 | 31 | if (!row) { 32 | res.status(404) 33 | return res.send('Not found') 34 | } 35 | 36 | res.json(row) 37 | } 38 | 39 | async function PATCH(req, res) { 40 | const { 41 | query: { postId }, 42 | body, 43 | } = req 44 | 45 | if (body.body.includes('fail')) { 46 | res.status(500) 47 | res.json({ message: 'An unknown error occurred!' }) 48 | return 49 | } 50 | 51 | const row = (await db.get()).posts.find((d) => d.id == postId) 52 | 53 | if (!row) { 54 | res.status(404) 55 | return res.send('Not found') 56 | } 57 | 58 | delete body.id 59 | 60 | const newRow = { 61 | ...row, 62 | ...body, 63 | } 64 | 65 | await db.set((old) => { 66 | return { 67 | ...old, 68 | posts: old.posts.map((d) => (d.id == postId ? newRow : d)), 69 | } 70 | }) 71 | 72 | res.json(newRow) 73 | } 74 | 75 | async function DELETE(req, res) { 76 | const { 77 | query: { postId }, 78 | } = req 79 | 80 | if (Math.random() < deleteFailureRate) { 81 | res.status(500) 82 | res.json({ message: 'An unknown error occurred!' }) 83 | return 84 | } 85 | 86 | const row = (await db.get()).posts.find((d) => d.id == postId) 87 | 88 | if (!row) { 89 | res.status(404) 90 | return res.send('Not found') 91 | } 92 | 93 | await db.set((old) => { 94 | return { 95 | ...old, 96 | posts: old.posts.filter((d) => d.id != postId), 97 | } 98 | }) 99 | 100 | res.status(200) 101 | res.send('Resource Deleted') 102 | } 103 | -------------------------------------------------------------------------------- /pages/api/posts/index.js: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid' 2 | import db from '../../../db' 3 | import { sleep } from '../../../utils' 4 | 5 | // 6 | const failureRate = 0 7 | 8 | export default async (req, res) => { 9 | await sleep(1000) 10 | 11 | try { 12 | if (req.method === 'GET') { 13 | return await GET(req, res) 14 | } else if (req.method === 'POST') { 15 | return await POST(req, res) 16 | } 17 | } catch (err) { 18 | console.error(err) 19 | res.status(500) 20 | res.json({ message: 'An unknown error occurred!' }) 21 | } 22 | } 23 | 24 | async function GET(req, res) { 25 | const { 26 | query: { pageOffset, pageSize }, 27 | } = req 28 | 29 | const posts = (await db.get()).posts.map((d) => ({ 30 | ...d, 31 | body: d.body.substring(0, 50) + (d.body.length > 100 ? '...' : ''), // Don't return full body in list calls 32 | })) 33 | 34 | if (Number(pageSize)) { 35 | const start = Number(pageSize) * Number(pageOffset) 36 | const end = start + Number(pageSize) 37 | const page = posts.slice(start, end) 38 | 39 | return res.json({ 40 | items: page, 41 | nextPageOffset: posts.length > end ? Number(pageOffset) + 1 : undefined, 42 | }) 43 | } 44 | 45 | res.json(posts) 46 | } 47 | 48 | async function POST(req, res) { 49 | if (Math.random() < failureRate) { 50 | res.status(500) 51 | res.json({ message: 'An unknown error occurred!' }) 52 | return 53 | } 54 | 55 | const row = { 56 | id: shortid.generate(), 57 | ...req.body, 58 | } 59 | 60 | await db.set((old) => { 61 | return { 62 | ...old, 63 | posts: [...old.posts, row], 64 | } 65 | }) 66 | 67 | res.json(row) 68 | } 69 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import App from '../src/' 2 | 3 | export default App 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnwithjason/all-about-react-query/36b8edb0393158ad6855ea796aba894f5fd061d4/public/favicon.ico -------------------------------------------------------------------------------- /public/zeit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/GlobalLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Loader } from './styled' 3 | import { useIsFetching } from 'react-query' 4 | 5 | export default function GlobalLoader() { 6 | const isFetching = useIsFetching() 7 | 8 | return ( 9 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/PostForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const defaultFormValues = { 4 | title: '', 5 | body: '', 6 | } 7 | 8 | export default function PostForm({ 9 | onSubmit, 10 | initialValues = defaultFormValues, 11 | submitText, 12 | clearOnSubmit, 13 | }) { 14 | const [values, setValues] = React.useState(initialValues) 15 | 16 | const setValue = (field, value) => 17 | setValues((old) => ({ ...old, [field]: value })) 18 | 19 | const handleSubmit = (e) => { 20 | if (clearOnSubmit) { 21 | setValues(defaultFormValues) 22 | } 23 | e.preventDefault() 24 | onSubmit(values) 25 | } 26 | 27 | React.useEffect(() => { 28 | setValues(initialValues) 29 | }, [initialValues]) 30 | 31 | return ( 32 |
33 | 34 |
35 | setValue('title', e.target.value)} 40 | required 41 | /> 42 |
43 |
44 | 45 |
46 |