add tanstack/react-query

This commit is contained in:
smanylov
2025-12-02 17:11:53 +07:00
parent d89cc35fca
commit 6d47fa7fbb
15 changed files with 209 additions and 94 deletions
+27
View File
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query": "^5.90.11",
"clsx": "^2.1.1",
"jose": "^6.1.2",
"next": "16.0.3",
@@ -1884,6 +1885,32 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.90.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.11.tgz",
"integrity": "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.90.11",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz",
"integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.90.11"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+2 -1
View File
@@ -5,11 +5,12 @@
"scripts": {
"dev": "next dev -p 2999",
"build": "next build",
"start": "next start",
"start": "next start -p 2999",
"lint": "eslint"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query": "^5.90.11",
"clsx": "^2.1.1",
"jose": "^6.1.2",
"next": "16.0.3",
+7 -7
View File
@@ -1,14 +1,14 @@
'use server'
import { SignupFormSchema, loginFormSchema } from '@/app/lib/definitions';
import { createSession, deleteSession, getSessionToken } from '@/app/lib/session';
import { createSession, deleteSession, getSessionData } from '@/app/lib/session';
import { redirect } from 'next/navigation'
export async function logout() {
const token = await getSessionToken();
const token = await getSessionData('token');
try {
await fetch('http://localhost:8080/api/auth/logout', {
await fetch('http://localhost/api/auth/logout', {
method: 'POST',
headers: {
"Content-Type": "application/json",
@@ -39,7 +39,7 @@ export async function authorization(
try {
const { email, password } = validatedFields.data;
const response = await fetch('http://localhost:8080/api/auth/login', {
const response = await fetch('http://localhost/api/auth/login', {
method: 'POST',
body: JSON.stringify({
email: email,
@@ -52,7 +52,7 @@ export async function authorization(
});
if (response.ok) {
let parsed = await response.json();
await createSession(parsed.token);
await createSession(parsed.token, parsed.email);
} else {
throw new Error('Something went wrong. from server');
}
@@ -96,7 +96,7 @@ export async function registration(
try {
const { full_name, email, password } = validatedFields.data;
const response = await fetch('http://localhost:8080/api/auth/register', {
const response = await fetch('http://localhost/api/auth/register', {
method: 'POST',
body: JSON.stringify({ fullName: full_name, email, password }),
headers: {
@@ -107,7 +107,7 @@ export async function registration(
if (response.ok) {
let parsed = await response.json();
await createSession(parsed.token);
await createSession(parsed.token, email);
} else {
throw new Error('Something went wrong. from server');
}
-43
View File
@@ -1,43 +0,0 @@
'use client'
import { createContext, useContext, ReactNode } from 'react'
interface Data {
id: number
name: string
family: string
}
interface DataContextType {
dataContext: Data | null
isLoading: boolean
error: string | null
}
const DataContext = createContext<DataContextType | undefined>(undefined)
export function DataProvider({
children,
dataContext
}: {
children: ReactNode
dataContext: any
}) {
return (
<DataContext.Provider value={{
dataContext,
isLoading: false,
error: null
}}>
{children}
</DataContext.Provider>
)
}
export function useDataContext() {
const context = useContext(DataContext)
if (context === undefined) {
throw new Error('useDataContext must be used within a DataProvider')
}
return context
}
+4 -1
View File
@@ -1,3 +1,4 @@
import Providers from './providers/providers'
import type { Metadata } from "next";
import "./styles/globals.css";
import "./styles/global-styles.scss"
@@ -15,7 +16,9 @@ export default function RootLayout({
return (
<html lang="ru">
<body className={``}>
{children}
<Providers>
{children}
</Providers>
</body>
</html>
);
+13 -14
View File
@@ -1,4 +1,5 @@
'use server'
import { getSessionData } from '@/app/lib/session';
const FRUITS_URL = 'https://www.fruityvice.com/api/fruit/';
const ALL = 'all'
@@ -25,26 +26,24 @@ export async function fetchFruit(id: number) {
}
}
export async function testConnect() {
export async function getUserData() {
const userEmail = await getSessionData('email');
const token = await getSessionData('token');
try {
const response = await fetch('http://localhost:8080/api/auth/register', {
method: 'POST',
body: JSON.stringify({
"fullName": "Vladislav Sergevich",
"email": "s2@e.ru",
"password": "1121231412"
}),
const response = await fetch(`http://localhost/api/user?email=${userEmail}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
"Accept": "application/json",
"Authorization": `Bearer ${token}`,
}
});
let parsed = await response.json();
return parsed;
if (response.ok) {
return await response.json();
}
} catch (error) {
throw error;
console.error('error');
}
}
+9 -6
View File
@@ -25,9 +25,13 @@ export async function decrypt(session: string | undefined = '') {
}
}
export async function createSession(token: string) {
export async function createSession(token: string, email: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const session = await encrypt({ token, expiresAt });
const session = await encrypt({
token,
email,
expiresAt
});
const cookieStore = await cookies();
cookieStore.set('session', session, {
@@ -39,7 +43,7 @@ export async function createSession(token: string) {
})
}
export async function getSessionToken(): Promise<string | null> {
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value;
@@ -49,15 +53,14 @@ export async function getSessionToken(): Promise<string | null> {
const payload = await decrypt(sessionCookie);
if (payload && typeof payload === 'object' && 'token' in payload) {
return payload.token as string;
if (payload && typeof payload === 'object' && type in payload) {
return payload[type] as string;
}
return null;
}
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
+1 -1
View File
@@ -11,7 +11,7 @@ export default function Home() {
<div className={styles.page}>
<main className={styles.main}>
<div className={styles.intro}>
Home page11
Home page
</div>
<Link key={'dashboard'}
href={'/pages/dashboard'}
+4
View File
@@ -1,4 +1,5 @@
import { Metadata } from 'next';
import Test from '@/app/ui/test';
export const metadata: Metadata = {
title: 'emptypage',
@@ -11,6 +12,9 @@ export default function Home() {
<div>
emptypage
</div>
<div>
<Test />
</div>
</main>
</div>
);
+14 -9
View File
@@ -1,18 +1,23 @@
import NavLinks from '@/app/ui/nav-links'
import styles from '@/app/page.module.scss'
import HeaderPanel from '../ui/header/headerPanel';
import { DataProvider } from '@/app/context/data-context';
import { fetchFruit } from '@/app/lib/action';
import { getSessionToken } from '@/app/lib/session';
import { getQueryClient } from '@/app/providers/getQueryClient';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getUserData } from '@/app/lib/action';
export default async function Layout({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
const fruitsData = await fetchFruit(100);
const token = await getSessionToken();
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['userData'],
queryFn: () => getUserData()
})
])
return (
<DataProvider dataContext={fruitsData}>
<div className="flex">
<div className="flex">
<HydrationBoundary state={dehydrate(queryClient)}>
<NavLinks />
<div className={`${styles.mainContainter}`}>
<HeaderPanel />
@@ -20,7 +25,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
{children}
</main>
</div>
</div>
</DataProvider>
</HydrationBoundary>
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'
const getQueryClientDefaultConfig = () => ({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
},
},
})
export function makeQueryClient() {
return new QueryClient(getQueryClientDefaultConfig())
}
export const getQueryClient = cache(() => {
if (typeof window === 'undefined') {
return new QueryClient(getQueryClientDefaultConfig())
} else {
// @ts-ignore
if (!globalThis.__APP_QUERY_CLIENT__) {
// @ts-ignore
globalThis.__APP_QUERY_CLIENT__ = makeQueryClient()
}
// @ts-ignore
return globalThis.__APP_QUERY_CLIENT__
}
})
+36
View File
@@ -0,0 +1,36 @@
'use client'
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
return makeQueryClient()
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
+1 -3
View File
@@ -1,9 +1,7 @@
'use client'
import { testConnect } from '@/app/lib/action';
async function clickHandler() {
testConnect();
console.log('click')
}
export default function ButtonTest() {
+30 -9
View File
@@ -3,12 +3,23 @@
import { useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link';
import { useDataContext } from '@/app/context/data-context';
import { getUserData } from '@/app/lib/action';
import { useQuery } from '@tanstack/react-query';
export default function UserMenuButton() {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const { dataContext } = useDataContext()
const {
data: userData,
isLoading,
isError,
error
} = useQuery({
queryKey: ['userData'],
queryFn: () => {
return getUserData();
}
})
useClickOutside(menuRef, () => {
@@ -17,7 +28,6 @@ export default function UserMenuButton() {
const handleButtonClick = () => {
if (!isOpen) {
console.log(dataContext);
setIsOpen(true);
} else {
setIsOpen(false);
@@ -30,18 +40,29 @@ export default function UserMenuButton() {
onClick={handleButtonClick}
ref={menuRef}
>
<div className="user-avatar" id="user-menu-btn" title="Валерка">
User
<div
className="user-avatar"
id="user-menu-btn"
title={userData?.fullName ? userData.fullName : ''}
>
{userData?.fullName ? userData.fullName : ''}
</div>
<div className={`user-dropdown ${isOpen ? 'show' : ''}`} id="user-dropdown">
<div className="user-info-header">
<div className="user-avatar-large">User</div>
<div className="user-avatar-large">
{userData?.fullName ? userData.fullName : ''}
</div>
<div className="user-details">
<div className="user-name">Валерка</div>
<div className="user-email">va@no-copy.ru</div>
<div className="user-name">
{userData?.fullName ? userData.fullName : ''}
</div>
<div className="user-email">
{userData?.email ? userData.email : ''}
</div>
<div className="user-subscription">
<span className="subscription-badge">
СТАРТ</span>
{userData?.subscriptionType ? userData.subscriptionType : ''}
</span>
</div>
</div>
</div>
+30
View File
@@ -0,0 +1,30 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { getUserData } from '@/app/lib/action';
export default function HomePage() {
const {
data: userData,
isLoading,
isError,
error,
} = useQuery({
queryKey: ['userData'],
queryFn: () => {
return getUserData();
},
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<div>
<h1>user email</h1>
<span>
{userData.email}
</span>
</div>
);
}