diff --git a/package-lock.json b/package-lock.json index 26e55338..c7a74dfa 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", @@ -786,6 +788,79 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", + "license": "MIT", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -972,6 +1047,21 @@ "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==", + "license": "MIT", + "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 +1257,18 @@ "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==", + "license": "MIT", + "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 +1446,12 @@ "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==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -1403,6 +1511,18 @@ "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==", + "license": "MIT", + "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 +1595,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.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..c8115d14 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,13 @@ import './App.css'; +import PostList from './react-query/PostList'; +import TodoList from './react-query/TodoList'; function App() { - return

React Starter Project

; + return <> +

React Starter Project

+ {/* */} + + } export default App; diff --git a/src/main.tsx b/src/main.tsx index c7edb443..c4b7f4e1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,28 @@ import 'bootstrap/dist/css/bootstrap.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 './index.css'; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 3, + cacheTime: 300000, + staleTime: 10000 + } + } +}); ReactDOM.createRoot( document.getElementById('root') as HTMLElement ).render( + + + + ); diff --git a/src/react-query/PostList.tsx b/src/react-query/PostList.tsx index d4158947..96a020fc 100644 --- a/src/react-query/PostList.tsx +++ b/src/react-query/PostList.tsx @@ -1,34 +1,59 @@ -import axios from 'axios'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; +import usePosts from '../routing/hooks/usePosts'; -interface Post { - id: number; - title: string; - body: string; - userId: number; -} 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 [userId, setUserId] = useState(); +const [page, setPage] = useState(1); +const {data: posts, error, isLoading} = usePosts(userId, page); +if (isLoading) return

Loading...

+if (error) return

{error.message}

; +console.log("data.length", posts.length) +console.log("page,page)", page) +const POSTS_PER_PAGE = 10; // Same as the _limit in the API request return ( -
    - {posts.map((post) => ( + <> + +
      + {posts?.map((post) => (
    • {post.title}
    • ))}
    + +
    + + Page {page} + +
    + + ); }; diff --git a/src/react-query/TodoList.tsx b/src/react-query/TodoList.tsx index e397595d..14b52cb2 100644 --- a/src/react-query/TodoList.tsx +++ b/src/react-query/TodoList.tsx @@ -1,29 +1,35 @@ -import axios from 'axios'; -import React, { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import useTodos from '../routing/hooks/useTodos'; -interface Todo { - id: number; - title: string; - userId: number; - completed: boolean; -} -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 TodoList = ()=> { - if (error) return

    {error}

    ; + // const fetchTodos = () => + // axios + // .get('https://jsonplaceholder.typicode.com/todos') + // .then((res) => + // res.data); + // query gets back an object with properties like data, error, isLoading, etc. + // we get autore fetch, auto retry and caching + const {data: todos, error, isLoading} = useTodos(); + + // 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 (isLoading) return

    Loading...

    ; + if (error?.message) return

    {error.message}

    ; return (
      - {todos.map((todo) => ( + {todos?.map((todo) => (
    • {todo.title}
    • diff --git a/src/routing/hooks/usePosts.ts b/src/routing/hooks/usePosts.ts new file mode 100644 index 00000000..72c25659 --- /dev/null +++ b/src/routing/hooks/usePosts.ts @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; +interface Post { + userId: number; + id: number; + title: string; + body: string; +} + +const usePosts = (userId: number | undefined, page: number) => { + const fetchPosts = async (): Promise => { + const url = new URL('https://jsonplaceholder.typicode.com/posts') + + // Add query parameters + if (userId !== undefined) { + url.searchParams.append('userId', userId.toString()); + } + + // pagination + url.searchParams.append('_page', page.toString()); + url.searchParams.append('_limit', '10'); // Limit to 10 posts per page + + const res = await fetch(url.toString()); + + if (!res.ok) { + // console.log("failed") + throw new Error(`Failed to fetch todos: ${res.statusText}`); + } + const data = await res.json(); + // console.log(data) + return data as Post[]; + }; + + return useQuery({ + // need to take out + queryKey:['users',userId,'posts', page], + queryFn: fetchPosts, + // keepPreviousData: true, // Keep previous data while fetching new data + }) +} + +export default usePosts; \ No newline at end of file diff --git a/src/routing/hooks/useTodos.ts b/src/routing/hooks/useTodos.ts new file mode 100644 index 00000000..2ce89a83 --- /dev/null +++ b/src/routing/hooks/useTodos.ts @@ -0,0 +1,28 @@ +import { useQuery } from "@tanstack/react-query"; + +interface Todo { + id: number; + title: string; + userId: number; + completed: boolean; + } +const useTodos = () => { + const fetchTodos = async (): Promise => { + const res = await fetch('https://jsonplaceholder.typicode.com/todos'); + if (!res.ok) { + console.log("failed") + throw new Error(`Failed to fetch todos: ${res.statusText}`); + } + const data = await res.json(); + console.log(data) + return data as Todo[]; + }; + + return useQuery({ + queryKey:['todos'], + queryFn: fetchTodos, + }) + +} + +export default useTodos; \ No newline at end of file