diff --git a/package-lock.json b/package-lock.json index 26e55338..1d0714ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "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", @@ -776,14 +778,73 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", - "peer": true, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz", + "integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "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/prop-types": { @@ -972,6 +1033,20 @@ "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", @@ -1167,6 +1242,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "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 +1430,11 @@ "node": ">=0.10.0" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -1403,6 +1494,17 @@ "node": ">=0.10.0" } }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "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 +1577,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", diff --git a/package.json b/package.json index c0724f30..640713ba 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "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", diff --git a/src/App.tsx b/src/App.tsx index 08f92be1..17976e64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,15 @@ -import './App.css'; +import "./App.css"; +import PostList from "./react-query/PostList"; +import TodoForm from "./react-query/TodoForm"; +import TodoList from "./react-query/TodoList"; function App() { - return

React Starter Project

; + return ( + <> + + + + ); } export default App; diff --git a/src/main.tsx b/src/main.tsx index c7edb443..717e1b36 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,26 @@ -import 'bootstrap/dist/css/bootstrap.css'; -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './index.css'; +import "bootstrap/dist/css/bootstrap.css"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import App from "./App"; +import "./index.css"; -ReactDOM.createRoot( - document.getElementById('root') as HTMLElement -).render( +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 3, + cacheTime: 300_000, //5 min + staleTime: 10 * 1000, //10S + }, + }, +}); + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + + + ); diff --git a/src/react-query/PostList.tsx b/src/react-query/PostList.tsx index d4158947..cb4e0bda 100644 --- a/src/react-query/PostList.tsx +++ b/src/react-query/PostList.tsx @@ -1,34 +1,43 @@ -import axios from 'axios'; -import { useEffect, useState } from 'react'; - -interface Post { - id: number; - title: string; - body: string; - userId: number; -} +import axios from "axios"; +import { useEffect, useState } from "react"; +import usePosts from "./hooks/usePosts"; +import React from "react"; const PostList = () => { - const [posts, setPosts] = useState([]); - const [error, setError] = useState(''); + const pageSize = 10; - useEffect(() => { - axios - .get('https://jsonplaceholder.typicode.com/posts') - .then((res) => setPosts(res.data)) - .catch((error) => setError(error)); - }, []); + const { + data: posts, + error, + isLoading, + fetchNextPage, + isFetchingNextPage, + } = usePosts({ pageSize }); - if (error) return

{error}

; + if (error) return

{error.message}

; return ( -
    - {posts.map((post) => ( -
  • - {post.title} -
  • - ))} -
+ <> +
    + {posts?.pages.map((page, index) => ( + + {page?.map((post) => ( +
  • + {post.title} +
  • + ))} +
    + ))} +
+ + + ); }; diff --git a/src/react-query/TodoForm.tsx b/src/react-query/TodoForm.tsx index 511cc688..419cc9a1 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, + title: ref.current?.value, + completed: false, + userId: 1, + }); + }} + > +
+ +
+
+ +
+
+ ); }; diff --git a/src/react-query/TodoList.tsx b/src/react-query/TodoList.tsx index e397595d..0a44f621 100644 --- a/src/react-query/TodoList.tsx +++ b/src/react-query/TodoList.tsx @@ -1,6 +1,4 @@ -import axios from 'axios'; -import React, { useEffect, useState } from 'react'; - +import useTodos from "./hooks/useTodos"; interface Todo { id: number; title: string; @@ -9,21 +7,14 @@ interface Todo { } 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)); - }, []); + const { data: todos, error, isLoading } = useTodos(); - if (error) return

{error}

; + if (isLoading) return

Loding...

; + if (error) return

{error.message}

; return (
    - {todos.map((todo) => ( + {todos?.map((todo) => (
  • {todo.title}
  • diff --git a/src/react-query/constants.ts b/src/react-query/constants.ts new file mode 100644 index 00000000..5fc6b7de --- /dev/null +++ b/src/react-query/constants.ts @@ -0,0 +1 @@ +export const CACHE_KEY_TODO = ["todos"]; diff --git a/src/react-query/hooks/useAddTodo.ts b/src/react-query/hooks/useAddTodo.ts new file mode 100644 index 00000000..ebb8c238 --- /dev/null +++ b/src/react-query/hooks/useAddTodo.ts @@ -0,0 +1,44 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CACHE_KEY_TODO } from "../constants"; +import APIClient from "../services/apiClient"; +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_TODO) || []; + queryClient.setQueriesData(CACHE_KEY_TODO, (todos = []) => [ + newTodo, + ...todos, + ]); + return { previousTodos }; + }, + onSuccess: (savedTodo, newTodo) => { + //Approach 1: Invalidating the cache + // queryClient.invalidateQueries({ + // queryKey:['todos'] + // }) + + //Approach 2: Updating the data in cache + queryClient.setQueriesData(CACHE_KEY_TODO, (todos) => + todos?.map((todo) => (todo === newTodo ? savedTodo : todo)) + ); + onAdd(); + // + }, + onError: (error, newTodo, context) => { + if (!context) return; + + queryClient.setQueriesData(CACHE_KEY_TODO, 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..cf64e128 --- /dev/null +++ b/src/react-query/hooks/usePosts.ts @@ -0,0 +1,33 @@ +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import axios from "axios"; +interface Post { + id: number; + title: string; + body: string; + userId: number; +} +interface Props { + pageSize: number; +} +const usePosts = (query: Props) => { + const fetchPosts = ({ pageParam = 1 }) => + axios + .get("https://jsonplaceholder.typicode.com/posts", { + params: { + _start: (pageParam - 1) * query.pageSize, + _limit: query.pageSize, + }, + }) + .then((res) => res.data); + + return useInfiniteQuery({ + queryKey: ["posts", query], + queryFn: fetchPosts, + keepPreviousData: true, + getNextPageParam(lastPage, allPages) { + 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..3b031d5a --- /dev/null +++ b/src/react-query/hooks/useTodos.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { CACHE_KEY_TODO } from "../constants"; +import APIClient from "../services/apiClient"; +import todoService, { Todo } from "../services/todoService"; + +const apiClient = new APIClient("/todos"); +const useTodos = () => { + return useQuery({ + queryKey: CACHE_KEY_TODO, + queryFn: todoService.getAll, + }); +}; +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..d99c0625 --- /dev/null +++ b/src/react-query/services/apiClient.ts @@ -0,0 +1,23 @@ +import axios from "axios"; +import AppRouter from "next/dist/client/components/app-router"; + +const apiInstance = axios.create({ + baseURL: "https://jsonplaceholder.typicode.com", +}); + +class APIClient { + endpoint: string; + constructor(endpoint: string) { + this.endpoint = endpoint; + } + + getAll = () => { + return apiInstance.get(this.endpoint).then((res) => res.data); + }; + + post = (data: T) => { + return apiInstance.post(this.endpoint).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..48e28938 --- /dev/null +++ b/src/react-query/services/todoService.ts @@ -0,0 +1,8 @@ +import APIClient from "./apiClient"; +export interface Todo { + id: number; + title: string; + userId: number; + completed: boolean; +} +export default new APIClient("/todos");