diff --git a/README.md b/README.md index f75faad3..981f7b12 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,8 @@ -# Playground Start - -This is the starter code for the playground project we use in part 2 of my React course. - -## Getting Started - -To get started, follow these steps: - -1. Clone this repository to your local machine. -2. Run `npm install` to install the required dependencies. -3. Run `npm run dev` to start the web server. - -## About the Course - -This repository belongs to part 2 of my React course covering intermediate-level topics. - -- Fetching and updating data with React Query -- All about reducers, context, and providers -- Managing application state with Zustand -- Routing with React Router - -You can find the course at https://codewithmosh.com +# React Advanced Topics +This repo is divided into three parts. +# First part- React Query +This repo is fully dedicated to react-query for fetching data from API from scratch to more complicated functions. +# Second part- Global State Management + +## How to use +You need only follow the commits diff --git a/package-lock.json b/package-lock.json index 26e55338..e92f5b6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,18 @@ "name": "react-app-starter", "version": "0.0.0", "dependencies": { + "@tanstack/react-query": "4.28", + "@tanstack/react-query-devtools": "4.28", "axios": "^1.3.4", "bootstrap": "^5.2.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.10.0", + "simple-zustand-devtools": "^1.1.0", + "zustand": "^4.4.1" }, "devDependencies": { + "@types/node": "^20.5.9", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@vitejs/plugin-react": "^3.1.0", @@ -786,17 +792,98 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", + "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz", + "integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==", + "dependencies": { + "remove-accents": "0.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kentcdodds" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.27.0.tgz", + "integrity": "sha512-sm+QncWaPmM73IPwFlmWSKPqjdTXZeFf/7aEmWh00z7yl2FjqophPt0dE1EHW9P1giMC5rMviv7OUbSDmWzXXA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.28.0.tgz", + "integrity": "sha512-8cGBV5300RHlvYdS4ea+G1JcZIt5CIuprXYFnsWggkmGoC0b5JaqG0fIX3qwDL9PTNkKvG76NGThIWbpXivMrQ==", + "dependencies": { + "@tanstack/query-core": "4.27.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.28.0.tgz", + "integrity": "sha512-1SnoMw1CWn8FdPEIHvlAzmMBX3heXJo11fyBtt+FzYAHj5yFC8P67Kpgi0HpLkY7SLnd6QK/7qFkpeH4AQbgZg==", + "dependencies": { + "@tanstack/match-sorter-utils": "^8.7.0", + "superjson": "^1.10.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "4.28.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.5.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", + "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.0.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -807,7 +894,6 @@ "version": "18.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", - "dev": true, "dependencies": { "@types/react": "*" } @@ -815,8 +901,7 @@ "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "node_modules/@vitejs/plugin-react": { "version": "3.1.0", @@ -972,11 +1057,24 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "node_modules/debug": { "version": "4.3.4", @@ -1167,6 +1265,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz", + "integrity": "sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1344,6 +1453,41 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", + "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "dependencies": { + "@remix-run/router": "1.5.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", + "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "dependencies": { + "@remix-run/router": "1.5.0", + "react-router": "6.10.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -1394,6 +1538,18 @@ "semver": "bin/semver.js" } }, + "node_modules/simple-zustand-devtools": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-zustand-devtools/-/simple-zustand-devtools-1.1.0.tgz", + "integrity": "sha512-Axfcfr9L3YL3kto7aschCQLY2VUlXXMnIVtaTe9Y0qWbNmPsX/y7KsNprmxBZoB0pww5ZGs1u/ohcrvQ3tE6jA==", + "peerDependencies": { + "@types/react": ">=18.0.0", + "@types/react-dom": ">=18.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0", + "zustand": ">=1.0.2" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -1403,6 +1559,17 @@ "node": ">=0.10.0" } }, + "node_modules/superjson": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.1.tgz", + "integrity": "sha512-AVH2eknm9DEd3qvxM4Sq+LTCkSXE2ssfh1t11MHMXyYXFQyQ1HLgVvV+guLTsaQnJU3gnaVo34TohHPulY/wLg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1475,6 +1642,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", @@ -1529,6 +1704,33 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "node_modules/zustand": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz", + "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index c0724f30..94ed6d85 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,18 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "4.28", + "@tanstack/react-query-devtools": "4.28", "axios": "^1.3.4", "bootstrap": "^5.2.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.10.0", + "simple-zustand-devtools": "^1.1.0", + "zustand": "^4.4.1" }, "devDependencies": { + "@types/node": "^20.5.9", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@vitejs/plugin-react": "^3.1.0", diff --git a/src/App.tsx b/src/App.tsx index 08f92be1..8b748e1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,17 @@ -import './App.css'; +import "./App.css"; +import { TasksProvider } from "./state-management/tasks"; +import HomePage from "./state-management/HomePage"; +import NavBar from "./state-management/NavBar"; +import Counter from "./state-management/counter/Counter"; function App() { - return

React Starter Project

; + return ( + + + + + + ); } export default App; diff --git a/src/main.tsx b/src/main.tsx index c7edb443..6682c2fe 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,35 @@ -import 'bootstrap/dist/css/bootstrap.css'; -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './index.css'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import App from "./App"; +import "bootstrap/dist/css/bootstrap.css"; +import "./index.css"; +import { RouterProvider } from "react-router-dom"; +import router from "./routing/routes"; -ReactDOM.createRoot( - document.getElementById('root') as HTMLElement -).render( +// On the below line that I create a new queryClient, I can pass a configuration object +const queryClient = new QueryClient({ + defaultOptions: { + // in this object I can override the default settings for queries globally + queries: { + retry: 3, //retries 3 times after failing + cacheTime: 300_000, //5m => if query has no observer, or in other word, no component is using that query is considered inactive. The result of inactive queries is remove from the cache after five minutes. THIS IS CALLED GARBAGE COLLECTION. The default value make sense for a lot of applications but I can always customize this depending on the requirements. + staleTime: 0, //How long data is considered fresh. Zero means the moment that I get a piece of data it is treated as old. So the next time that I need the data, RQ fetch the fresh data from the backend. + //All three below option is for RQ AutoRefresh and their default value is true. + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + {/* Instead of using a specific component like App, we render RouterProvider and let react-router decide what component should be rendered depending on the users location. This is the idea of routing. */} + + + ); diff --git a/src/react-query/PostList.tsx b/src/react-query/PostList.tsx index d4158947..13e90015 100644 --- a/src/react-query/PostList.tsx +++ b/src/react-query/PostList.tsx @@ -1,34 +1,37 @@ -import axios from 'axios'; -import { useEffect, useState } from 'react'; - -interface Post { - id: number; - title: string; - body: string; - userId: number; -} +import React from "react"; +import usePosts from "./hooks/usePosts"; +import { useState } from "react"; const PostList = () => { - const [posts, setPosts] = useState([]); - const [error, setError] = useState(''); - - useEffect(() => { - axios - .get('https://jsonplaceholder.typicode.com/posts') - .then((res) => setPosts(res.data)) - .catch((error) => setError(error)); - }, []); - - if (error) return

{error}

; + const pageSize = 10; + const { data, error, isLoading, fetchNextPage, isFetchingNextPage } = + usePosts({ pageSize }); + if (isLoading) return

Loading...

; + if (error) return

{error.message}

; return ( - + <> + + + + ); }; diff --git a/src/react-query/TodoForm.tsx b/src/react-query/TodoForm.tsx index 511cc688..799d9e9f 100644 --- a/src/react-query/TodoForm.tsx +++ b/src/react-query/TodoForm.tsx @@ -1,17 +1,39 @@ -import { useRef } from 'react'; +import { useRef } from "react"; +import useAddTodo from "./hooks/useAddTodo"; const TodoForm = () => { const ref = useRef(null); - + const addTodo = useAddTodo(() => { + if (ref.current) ref.current.value = ""; + }); return ( -
-
- -
-
- -
-
+ <> + {addTodo.error && ( +
{addTodo.error.message}
+ )} +
{ + event.preventDefault(); + + if (ref.current && ref.current.value) + addTodo.mutate({ + id: 0, + // Compilation error==>'ref.current' is possibly 'null'.==> So I should use optional chaining. + title: ref.current?.value, + completed: false, + userId: 1, + }); + }} + > +
+ +
+
+ +
+
+ ); }; diff --git a/src/react-query/TodoList.tsx b/src/react-query/TodoList.tsx index e397595d..3db578f7 100644 --- a/src/react-query/TodoList.tsx +++ b/src/react-query/TodoList.tsx @@ -1,29 +1,15 @@ -import axios from 'axios'; -import React, { useEffect, useState } from 'react'; - -interface Todo { - id: number; - title: string; - userId: number; - completed: boolean; -} +import useTodos from "./hooks/useTodos"; const TodoList = () => { - const [todos, setTodos] = useState([]); - const [error, setError] = useState(''); - - useEffect(() => { - axios - .get('https://jsonplaceholder.typicode.com/todos') - .then((res) => setTodos(res.data)) - .catch((error) => setError(error)); - }, []); - - if (error) return

{error}

; + const { data: todos, error, isLoading } = useTodos(); + // Now this component only has single responsibility is purely concerned with markup. It doesn't know how data should be fetched. + // The other benefit is that if tomorrow I need access to these data in another component, I should only call this new hook in that component. So I use the concept of reusability and modularity. + if (isLoading) return

Loading...

; + if (error) return

{error.message}

; return (
    - {todos.map((todo) => ( + {todos?.map((todo) => (
  • {todo.title}
  • diff --git a/src/react-query/constant.ts b/src/react-query/constant.ts new file mode 100644 index 00000000..dda4c60b --- /dev/null +++ b/src/react-query/constant.ts @@ -0,0 +1 @@ +export const CACHE_KEY_TODOS = ["todos"]; diff --git a/src/react-query/hooks/useAddTodo.ts b/src/react-query/hooks/useAddTodo.ts new file mode 100644 index 00000000..4f2907a6 --- /dev/null +++ b/src/react-query/hooks/useAddTodo.ts @@ -0,0 +1,44 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CACHE_KEY_TODOS } from "../constant"; +import todoService, { Todo } from "../services/todoService"; +interface AddTodoContext { + previousTodos: Todo[]; +} + +const useAddTodo = (onAdd: () => void) => { + const QueryClient = useQueryClient(); + + return useMutation({ + mutationFn: todoService.post, + onMutate: (newTodo: Todo) => { + const previousTodos = + QueryClient.getQueryData([CACHE_KEY_TODOS]) || []; + QueryClient.setQueryData([CACHE_KEY_TODOS], (todos = []) => [ + newTodo, + ...todos, + ]); + // Inside this hook I only want the logic part for adding todo to backend and update the cache. This hook is all about data management and should focus on that aspect only. So, I should not insert UI related logic to the consumer of this hook(TodoForm component) + // if (ref.current) ref.current.value = ""; + // I can pass onAdd into this hook and then do the same logic as above line in the TodoForm component. + onAdd(); + return { previousTodos }; + }, + + onSuccess: (savedTodo, newTodo) => { + QueryClient.setQueryData([CACHE_KEY_TODOS], (todos) => + todos?.map((todo) => (todo === newTodo ? savedTodo : todo)) + ); + }, + + onError: (error, newTodo, context) => { + if (!context) return; + + QueryClient.setQueryData( + [CACHE_KEY_TODOS], + context.previousTodos + ); + }, + }); +}; + +export default useAddTodo; diff --git a/src/react-query/hooks/usePosts.ts b/src/react-query/hooks/usePosts.ts new file mode 100644 index 00000000..b05c691b --- /dev/null +++ b/src/react-query/hooks/usePosts.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import { useInfiniteQuery } from "@tanstack/react-query"; +interface Post { + id: number; + title: string; + body: string; + userId: number; +} +interface PostQuery { + pageSize: number; +} + +const usePosts = (query: PostQuery) => { + return useInfiniteQuery({ + queryKey: ["posts", query], + queryFn: ({ pageParam = 1 }) => + axios + .get("https://jsonplaceholder.typicode.com/posts", { + params: { + _start: (pageParam - 1) * query.pageSize, + _limit: query.pageSize, + }, + }) + .then((res) => res.data), + staleTime: 1 * 60 * 1000, //1m + // With setting keepPreviousData property to true, I have a nicer UX as user goes to the next page, there is no jumping up and down. As we go in and out of the loading state, the screen jump up and down. I can improve the UX by keeping the data from current page while the user is waiting for new data. When the new data is available, I can seamless swap the data for the current page ==> + keepPreviousData: true, + getNextPageParam: (lastPage, allPages) => { + // 1 -> 2 + // json placeholder has allPages and lastPage + return lastPage.length > 0 ? allPages.length + 1 : undefined; + }, + }); +}; +export default usePosts; diff --git a/src/react-query/hooks/useTodos.ts b/src/react-query/hooks/useTodos.ts new file mode 100644 index 00000000..cc26b62e --- /dev/null +++ b/src/react-query/hooks/useTodos.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { CACHE_KEY_TODOS } from "../constant"; +import todoService, { Todo } from "../services/todoService"; + +const useTodos = () => { + return useQuery({ + queryKey: [CACHE_KEY_TODOS], + // Now I just reference that method not call that( not use parentheses .getAll() ) + queryFn: todoService.getAll, + staleTime: 10 * 1000, //10s it is not globally + }); +}; +export default useTodos; diff --git a/src/react-query/services/apiClient.ts b/src/react-query/services/apiClient.ts new file mode 100644 index 00000000..ea27bf64 --- /dev/null +++ b/src/react-query/services/apiClient.ts @@ -0,0 +1,31 @@ +import axios from "axios"; + +// Make default axios instance +const axiosInstance = axios.create({ + baseURL: "https://jsonplaceholder.typicode.com", +}); + +class APIClient { + endpoint: string; + // initialize it once in the constructor + constructor(endpoint: string) { + this.endpoint = endpoint; + } + + // In front of getAll and post I should use generic type description, but I can do that for my class to stop duplication in my code. + + getAll = () => { + return ( + axiosInstance + // getAll method first return a promise of any BUT I WANNA work with typed objects. So, after .get I provide a generic type argument. Because I wanna this method be generic I only add T[] + .get(this.endpoint) + .then((res) => res.data) + ); + }; + + post = (data: T) => { + return axiosInstance.post(this.endpoint, data).then((res) => res.data); + }; +} + +export default APIClient; diff --git a/src/react-query/services/todoService.ts b/src/react-query/services/todoService.ts new file mode 100644 index 00000000..48043c76 --- /dev/null +++ b/src/react-query/services/todoService.ts @@ -0,0 +1,10 @@ +// Create a single instance of our client +import APIClient from "./apiClient"; + +export interface Todo { + id: number; + title: string; + userId: number; + completed: boolean; +} +export default new APIClient("/todos"); diff --git a/src/routing/ContactPage.tsx b/src/routing/ContactPage.tsx index f7451054..e04cf1d3 100644 --- a/src/routing/ContactPage.tsx +++ b/src/routing/ContactPage.tsx @@ -1,9 +1,15 @@ +import { useNavigate } from "react-router-dom"; + const ContactPage = () => { + // I want to redirect the user to the home page after submitting a form + // I should use Navigate hook + const navigate = useNavigate(); return (
    { event.preventDefault(); // Redirect the user to the home page + navigate("/"); }} > diff --git a/src/routing/ErrorPage.tsx b/src/routing/ErrorPage.tsx index fc6b34d3..bb2d143e 100644 --- a/src/routing/ErrorPage.tsx +++ b/src/routing/ErrorPage.tsx @@ -1,8 +1,12 @@ +import { isRouteErrorResponse, useRouteError } from "react-router-dom"; + const ErrorPage = () => { + const error = useRouteError(); + return ( <>

    Oops...

    -

    Sorry, an unexpected error has occurred.

    +

    {isRouteErrorResponse(error) ? "Invalid page" : "Unexpected error"}

    ); }; diff --git a/src/routing/HomePage.tsx b/src/routing/HomePage.tsx index 65bcd36a..888bc274 100644 --- a/src/routing/HomePage.tsx +++ b/src/routing/HomePage.tsx @@ -1,11 +1,19 @@ +import { Link } from "react-router-dom"; + const HomePage = () => { + throw new Error("Something Failed"); return ( <>

    - Lorem ipsum dolor sit amet consectetur, adipisicing elit. - Incidunt, mollitia! + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Incidunt, + mollitia!

    - Users + {/* Anchor element cause refreshing full page reload So instead we want only replace the content in that area. So the page load partially */} + {/* Users */} +
    + Users + Contact us +
    ); }; diff --git a/src/routing/Layout.tsx b/src/routing/Layout.tsx index 15042050..afc00dfb 100644 --- a/src/routing/Layout.tsx +++ b/src/routing/Layout.tsx @@ -1,10 +1,14 @@ -import NavBar from './NavBar'; +import { Outlet } from "react-router-dom"; +import NavBar from "./NavBar"; const Layout = () => { return ( <> -
    +
    + {/* Outlet is like a Placeholder for child component. Different components render inside this component */} + +
    ); }; diff --git a/src/routing/NavBar.tsx b/src/routing/NavBar.tsx index 26df5122..1e24ead0 100644 --- a/src/routing/NavBar.tsx +++ b/src/routing/NavBar.tsx @@ -1,24 +1,32 @@ +import { NavLink } from "react-router-dom"; + const NavBar = () => { return (