diff --git a/apps/react-vite/__mocks__/zustand.ts b/apps/react-vite/__mocks__/zustand.ts index a07195c5..ef8fb9e8 100644 --- a/apps/react-vite/__mocks__/zustand.ts +++ b/apps/react-vite/__mocks__/zustand.ts @@ -1,4 +1,5 @@ import { act } from '@testing-library/react'; +import { afterEach, vi } from 'vitest'; import * as zustand from 'zustand'; const { create: actualCreate, createStore: actualCreateStore } = @@ -18,8 +19,6 @@ const createUncurried = (stateCreator: zustand.StateCreator) => { // when creating a store, we get its initial state, create a reset function and add it in the set export const create = ((stateCreator: zustand.StateCreator) => { - console.log('zustand create mock'); - // to support curried version of create return typeof stateCreator === 'function' ? createUncurried(stateCreator) @@ -37,8 +36,6 @@ const createStoreUncurried = (stateCreator: zustand.StateCreator) => { // when creating a store, we get its initial state, create a reset function and add it in the set export const createStore = ((stateCreator: zustand.StateCreator) => { - console.log('zustand createStore mock'); - // to support curried version of createStore return typeof stateCreator === 'function' ? createStoreUncurried(stateCreator) diff --git a/apps/react-vite/src/app/index.tsx b/apps/react-vite/src/app/index.tsx index c9fce409..16d1b020 100644 --- a/apps/react-vite/src/app/index.tsx +++ b/apps/react-vite/src/app/index.tsx @@ -1,24 +1,10 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useMemo } from 'react'; -import { RouterProvider } from 'react-router-dom'; +import { AppProvider } from './provider'; +import { AppRouter } from './router'; -import { AppProvider } from './main-provider'; -import { createRouter } from './routes'; - -const AppRouter = () => { - const queryClient = useQueryClient(); - - const router = useMemo(() => createRouter(queryClient), [queryClient]); - - return ; -}; - -function App() { +export const App = () => { return ( ); -} - -export default App; +}; diff --git a/apps/react-vite/src/app/main-provider.tsx b/apps/react-vite/src/app/provider.tsx similarity index 84% rename from apps/react-vite/src/app/main-provider.tsx rename to apps/react-vite/src/app/provider.tsx index 5f18185c..d2161b59 100644 --- a/apps/react-vite/src/app/main-provider.tsx +++ b/apps/react-vite/src/app/provider.tsx @@ -1,4 +1,4 @@ -import { QueryClientProvider } from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import * as React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; @@ -8,13 +8,20 @@ import { MainErrorFallback } from '@/components/errors/main'; import { Notifications } from '@/components/ui/notifications'; import { Spinner } from '@/components/ui/spinner'; import { AuthLoader } from '@/lib/auth'; -import { queryClient } from '@/lib/react-query'; +import { queryConfig } from '@/lib/react-query'; type AppProviderProps = { children: React.ReactNode; }; export const AppProvider = ({ children }: AppProviderProps) => { + const [queryClient] = React.useState( + () => + new QueryClient({ + defaultOptions: queryConfig, + }), + ); + return ( +export const createAppRouter = (queryClient: QueryClient) => createBrowserRouter([ { path: '/', lazy: async () => { - const { LandingRoute } = await import('./landing'); + const { LandingRoute } = await import('./routes/landing'); return { Component: LandingRoute }; }, }, { path: '/auth/register', lazy: async () => { - const { RegisterRoute } = await import('./auth/register'); + const { RegisterRoute } = await import('./routes/auth/register'); return { Component: RegisterRoute }; }, }, { path: '/auth/login', lazy: async () => { - const { LoginRoute } = await import('./auth/login'); + const { LoginRoute } = await import('./routes/auth/login'); return { Component: LoginRoute }; }, }, @@ -40,13 +45,13 @@ export const createRouter = (queryClient: QueryClient) => path: 'discussions', lazy: async () => { const { DiscussionsRoute } = await import( - './app/discussions/discussions' + './routes/app/discussions/discussions' ); return { Component: DiscussionsRoute }; }, loader: async () => { const { discussionsLoader } = await import( - './app/discussions/discussions' + './routes/app/discussions/discussions' ); return discussionsLoader(queryClient)(); }, @@ -55,14 +60,14 @@ export const createRouter = (queryClient: QueryClient) => path: 'discussions/:discussionId', lazy: async () => { const { DiscussionRoute } = await import( - './app/discussions/discussion' + './routes/app/discussions/discussion' ); return { Component: DiscussionRoute }; }, loader: async (args: LoaderFunctionArgs) => { const { discussionLoader } = await import( - './app/discussions/discussion' + './routes/app/discussions/discussion' ); return discussionLoader(queryClient)(args); }, @@ -70,26 +75,26 @@ export const createRouter = (queryClient: QueryClient) => { path: 'users', lazy: async () => { - const { UsersRoute } = await import('./app/users'); + const { UsersRoute } = await import('./routes/app/users'); return { Component: UsersRoute }; }, loader: async () => { - const { usersLoader } = await import('./app/users'); + const { usersLoader } = await import('./routes/app/users'); return usersLoader(queryClient)(); }, }, { path: 'profile', lazy: async () => { - const { ProfileRoute } = await import('./app/profile'); + const { ProfileRoute } = await import('./routes/app/profile'); return { Component: ProfileRoute }; }, }, { path: '', lazy: async () => { - const { DashboardRoute } = await import('./app/dashboard'); + const { DashboardRoute } = await import('./routes/app/dashboard'); return { Component: DashboardRoute }; }, }, @@ -98,8 +103,16 @@ export const createRouter = (queryClient: QueryClient) => { path: '*', lazy: async () => { - const { NotFoundRoute } = await import('./not-found'); + const { NotFoundRoute } = await import('./routes/not-found'); return { Component: NotFoundRoute }; }, }, ]); + +export const AppRouter = () => { + const queryClient = useQueryClient(); + + const router = useMemo(() => createAppRouter(queryClient), [queryClient]); + + return ; +}; diff --git a/apps/react-vite/src/lib/react-query.ts b/apps/react-vite/src/lib/react-query.ts index de826434..0ac673c0 100644 --- a/apps/react-vite/src/lib/react-query.ts +++ b/apps/react-vite/src/lib/react-query.ts @@ -1,8 +1,4 @@ -import { - UseMutationOptions, - DefaultOptions, - QueryClient, -} from '@tanstack/react-query'; +import { UseMutationOptions, DefaultOptions } from '@tanstack/react-query'; export const queryConfig = { queries: { @@ -13,10 +9,6 @@ export const queryConfig = { }, } satisfies DefaultOptions; -export const queryClient = new QueryClient({ - defaultOptions: queryConfig, -}); - export type ApiFnReturnType Promise> = Awaited>; diff --git a/apps/react-vite/src/main.tsx b/apps/react-vite/src/main.tsx index 3105f68b..98f9f52a 100644 --- a/apps/react-vite/src/main.tsx +++ b/apps/react-vite/src/main.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; -import App from './app'; +import { App } from './app'; import { enableMocking } from './testing/mocks'; const root = document.getElementById('root'); diff --git a/apps/react-vite/src/testing/setup-tests.ts b/apps/react-vite/src/testing/setup-tests.ts index 02c2a4df..66762180 100644 --- a/apps/react-vite/src/testing/setup-tests.ts +++ b/apps/react-vite/src/testing/setup-tests.ts @@ -1,6 +1,5 @@ import '@testing-library/jest-dom/vitest'; -import { queryClient } from '@/lib/react-query'; import { initializeDb, resetDb } from '@/testing/mocks/db'; import { server } from '@/testing/mocks/server'; @@ -25,5 +24,4 @@ beforeEach(() => { afterEach(() => { server.resetHandlers(); resetDb(); - queryClient.clear(); }); diff --git a/apps/react-vite/src/testing/test-utils.tsx b/apps/react-vite/src/testing/test-utils.tsx index 2dca503b..15ae9d3e 100644 --- a/apps/react-vite/src/testing/test-utils.tsx +++ b/apps/react-vite/src/testing/test-utils.tsx @@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'; import Cookies from 'js-cookie'; import { RouterProvider, createMemoryRouter } from 'react-router-dom'; -import { AppProvider } from '@/app/main-provider'; +import { AppProvider } from '@/app/provider'; import { createDiscussion as generateDiscussion, diff --git a/docs/performance.md b/docs/performance.md index 59472a47..723b84c2 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -6,7 +6,7 @@ Code splitting involves splitting production JavaScript into smaller files to op Ideally, code splitting should be implemented at the routes level, ensuring that only essential code is loaded initially, with additional parts fetched lazily as needed. It's important to avoid excessive code splitting, as this can lead to a performance decline due to the increased number of requests required to fetch all the code chunks. Strategic code splitting, focusing on critical parts of the application, helps balance performance optimization with efficient resource loading. -[Code Splitting Example Code](../apps/react-vite/src/app/routes/index.tsx) +[Code Splitting Example Code](../apps/react-vite/src/app/router.tsx) ### Component and state optimizations diff --git a/docs/project-structure.md b/docs/project-structure.md index 5caaaf58..cb74df99 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -9,7 +9,8 @@ src | | | +-- routes # application routes / can also be called pages +-- app.tsx # main application component - +-- app-provider # application provider that wraps the entire application with global providers + +-- provider.tsx # application provider that wraps the entire application with different global providers + +-- router.tsx # application router configuration +-- assets # assets folder can contain all the static files such as images, fonts, etc. | +-- components # shared components used across the entire application @@ -55,6 +56,10 @@ src/features/awesome-feature NOTE: You don't need all of these folders for every feature. Only include the ones that are necessary for the feature. +In some cases it might be more practical to keep all api calls outside of the feature folders in a dedicated `api` folder where all API calls are defined. This can be useful if you have a lot of shared api calls between features. + +````sh + In the past, it was recommended to use barrel files to export all the files from a feature. However, it can cause issues for Vite to do tree shaking and can lead to performance issues. Therefore, it is recommended to import the files directly. It might not be a good idea to import across the features. Instead, compose different features at the application level. This way, you can ensure that each feature is independent which makes the codebase less convoluted. @@ -98,7 +103,7 @@ To forbid cross-feature imports, you can use ESLint: ], }, ], -``` +```` You might also want to enforce unidirectional codebase architecture. This means that the code should flow in one direction, from shared parts of the code to the application (shared -> features -> app). This is a good practice to follow as it makes the codebase more predictable and easier to understand.