diff --git a/electives/optimization/optimizing-your-react-node-project.md b/electives/optimization/optimizing-your-react-node-project.md index fa53df2f7..d85c46ccc 100644 --- a/electives/optimization/optimizing-your-react-node-project.md +++ b/electives/optimization/optimizing-your-react-node-project.md @@ -13,7 +13,7 @@ Here are links to lessons that should be completed before this lesson: We learned about **minification** in our previous lesson. Turns out the **React + Vite** tooling is able to do this for us via it's `build` process and we'll take a brief look at that. -As you may have seen already with your Eventonica app, remote API requests can be slow! We're going to apply the caching optimization technique we learned in the previous lesson to help speed things up. We will do this in a couple of different ways based depending on specific use cases and constraints. +As you may have seen already with [the Eventonica app](/projects/eventonica-updated), remote API requests can be slow! We're going to apply the caching optimization technique we learned in the previous lesson to help speed things up. We will do this in a couple of different ways based depending on specific use cases and constraints. While React is pretty fast even if we use it naively, it's important to be aware of some of the details of how React works in order for your UI to stay snappy and responsive even when it becomes deeply nested and complex. We will learn a few techniques to optimize our components and measure the difference using react specific dev tools. @@ -33,7 +33,7 @@ While React is pretty fast even if we use it naively, it's important to be aware - How to measure AJAX request latencies with browser dev tools. - Caching can happen in different layers (e.g. on the browser or on the backend) - How to install and use React dev tools. -- The `shouldComponentUpdate` lifecycle method and `PureComponent` class +- The `shouldComponentUpdate` lifecycle method and `PureComponent` class (and their modern equivalent, `React.memo` for functional components). - The React rendering reconciliation process. ### Materials @@ -52,34 +52,39 @@ _Building (includes minification)_ You've already configured building for heroku deployment. Let's run a build on our local machine, check out what gets generated, and then use our Node.js backend to serve up a "production" build locally. 1. Run the `npm run build` command from somewhere in your repository. -2. Take a look at the contents of the newly created `build` directory in the repository root directory, specifically what's inside the `static` and `static/js` directories. You can read more about these files in the linked page in the [Materials](#materials) section. +2. Take a look at the contents of the newly created `dist` directory in the repository root directory, specifically what's inside the `assets` directory. You can read more about these files in the linked page in the [Materials](#materials) section. 3. You can modify your backend to serve up these built assets by adding these two lines: + ```javascript // Serve any static files - app.use(express.static(path.join(__dirname, "rel/path/to/build-dir"))); + app.use(express.static(path.join(__dirname, "rel/path/to/dist-dir"))); // Handle React routing, return all requests to React app app.get("*", (req, res) => { - res.sendFile(path.join(__dirname, "rel/path/to/build-dir", "index.html")); + res.sendFile(path.join(__dirname, "rel/path/to/dist-dir", "index.html")); }); } ``` -Keep in mind that you need to modify the `rel/path/to/build-dir` to the actual relative path to your build directory--something like: `../client/build`. +Keep in mind that you need to modify the `rel/path/to/dist-dir` to the actual relative path to your build directory--something like: `../dist` (if your server is in a `server` folder and `dist` is in the project root). 4. Once you've configured your express server to serve up your built assets, go ahead and open up your browser to `http://localhost:5000` (or whatever port your express app is using), and make sure that it works just like you expect. _Adding Eventful Search Caching to Your App_ -We're going to be adding a component to your app, along with a separate server-side search API app that allows us to search the Eventful API. The code for both can be found in this [gist](https://gist.github.com/mhess/2a9213d209c4ea464ab305f7bec56300). Click the "Download ZIP" button on the gist page, unzip the contents, and then place the **EventfulSearch.js** next to all your other component files, and the **eventful-api-server.js** in your API repository folder. You should be able to run it with the following command: +We're going to be adding a component to your app, along with a separate server-side search API app that allows us to search for events. The code for both has been provided: `EventfulSearch.tsx` (located in `src/components/`) and `eventful-api-server.js` (located in `server/`). The code can be found in this [folder](/electives/optimization/optimizing-your-react-node-project/). + +You should be able to run the API server with the following command: -```shell -$ EVENTFUL_API_KEY= node eventful-api-server.js +```plaintext +$ npm run server ``` -Import and place the `` somewhere in one of your app components that makes sense. You should now be able to see that component in your app. Don't worry about styling for now. You can add that later :) +(For development with auto-reloading, use `npm run server:dev`). + +The `EventfulSearch` component is already integrated into `src/App.tsx`. You can import and place the `` somewhere else in your app components if it makes more sense for your project structure. You should now be able to see that component in your app. Don't worry about styling for now. You can add that later :) -Reload your Eventonica React app with the browser dev tools open. Notice the delay when searching for events, and use the network tab to see how long the AJAX requests are actually taking. Would be nice if we could cache results so that repeat searches are fast... +Reload your app with the browser dev tools open. Notice the delay when searching for events, and use the network tab to see how long the AJAX requests are actually taking. Would be nice if we could cache results so that repeat searches are fast... Let's optimize this for two different use cases: @@ -91,7 +96,7 @@ _Optimizing React_ By default a React component will re-render whenever it receives new **props** or if **setState** method is called with any argument. This is usually what you want, but if your component always renders the same thing with the same **props** and **state** (and it probably should) then that means that whenever its parent component re-renders, then the child will re-render too, even if its **props** and **state** are the same. -There's a component lifecyle method called `shouldComponentUpdate(nextProps, nextState)` that decides if a component will re-render. Its default implementation always returns `true`. We could implement this method ourselves, and make it so that it checks whether the `nextProps` and `nextState` are the same as the originals, and returns `false` if that's the case; however, React provides a component class `PureComponent` that does this automatically for us. This means that we can just replace the `Component` part of our component class definitions with `PureComponent`, and we get all the benefits of **props** and **state** checking! +There's a component lifecyle method called `shouldComponentUpdate(nextProps, nextState)` that decides if a component will re-render. Its default implementation always returns `true`. We could implement this method ourselves, and make it so that it checks whether the `nextProps` and `nextState` are the same as the originals, and returns `false` if that's the case; however, for class components, React provides a component class `PureComponent` that does this automatically for us. For functional components, the equivalent and recommended approach is to wrap your component with `React.memo`. This means that we can just replace the `Component` part of our component class definitions with `PureComponent`, or use `React.memo` for functional components, and we get all the benefits of **props** and **state** checking! However, we must take into account that the `shouldComponent` update method of `PureComponent` does only _shallow_ object comparison. This means that we need to be sure that we never mutate objects that we're using in our **state** or **props** because otherwise the changed objects will be considered to be the same, and our component won't re-render when it's supposed to. Instead, we need to create new objects whenever we call **setState** or pass new **props**. The React documentation has a great explanation of this [here](https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data). @@ -114,4 +119,4 @@ Now let's put these React optimization techniques into practice! 3. Once the "React" tab is open, select the a "Profiler" tab within that. 4. Click the gray circular record button on the left, and this will begin profiling the render actions in your app. 5. Perform a single action like creating, editing or deleting one of your events, and then click the orange "stop" button in the dev tools. A flamegraph will appear with all the component renders and their associated time cost. -6. With a delete, create, or update of an event, only one `` element is really getting changed, the rest should stay the same. Compare that to how many (re-)renders are you seeing in your flame graph. Use the techniques we learned above to try and make it so that your app only renders the modified/deteled/created element. +6. With a delete, create, or update of an event, only one `` element is really getting changed, the rest should stay the same. Compare that to how many (re-)renders are you seeing in your flame graph. Use the techniques we learned above (e.g., `React.memo` for functional components or `PureComponent` for class components) to try and make it so that your app only renders the modified/deleted/created element. diff --git a/electives/optimization/optimizing-your-react-node-project/index.html b/electives/optimization/optimizing-your-react-node-project/index.html new file mode 100644 index 000000000..054e490eb --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/index.html @@ -0,0 +1,13 @@ + + + + + + + React Optimization Lesson + + +
+ + + diff --git a/electives/optimization/optimizing-your-react-node-project/package.json b/electives/optimization/optimizing-your-react-node-project/package.json new file mode 100644 index 000000000..b743759c4 --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/package.json @@ -0,0 +1,81 @@ +{ + "name": "react-optimization-lesson", + "version": "1.0.0", + "description": "React optimization lesson with caching and performance examples", + "main": "eventful-api-server.js", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "server": "node server/eventful-api-server.js", + "server:dev": "node --watch server/eventful-api-server.js", + "lint": "next lint" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-accordion": "1.2.2", + "@radix-ui/react-alert-dialog": "1.1.4", + "@radix-ui/react-aspect-ratio": "1.1.1", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-context-menu": "2.2.4", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-hover-card": "1.1.4", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-menubar": "1.1.4", + "@radix-ui/react-navigation-menu": "1.2.3", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "1.1.1", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-scroll-area": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slider": "1.2.2", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-toggle-group": "1.1.1", + "@radix-ui/react-tooltip": "1.1.6", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "4.1.0", + "embla-carousel-react": "8.5.1", + "express": "^4.18.2", + "cors": "^2.8.5", + "geist": "^1.3.1", + "input-otp": "1.4.1", + "lucide-react": "^0.454.0", + "next": "14.2.25", + "next-themes": "^0.4.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.54.1", + "react-resizable-panels": "^2.1.7", + "recharts": "2.15.0", + "sonner": "^1.7.1", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.6", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "postcss": "^8.5", + "tailwindcss": "^3.4.17", + "typescript": "^5.2.2", + "vite": "^5.0.8" + }, + "keywords": ["events", "api", "mock", "education"], + "author": "Techtonica", + "license": "MIT" +} diff --git a/electives/optimization/optimizing-your-react-node-project/server/eventful-api-server.js b/electives/optimization/optimizing-your-react-node-project/server/eventful-api-server.js new file mode 100644 index 000000000..92b05890f --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/server/eventful-api-server.js @@ -0,0 +1,146 @@ +import express from "express" +import cors from "cors" + +const app = express() +const PORT = process.env.PORT || 3001 + +// Enable CORS for all routes +app.use(cors()) +app.use(express.json()) + +// Mock event data +const mockEvents = [ + { + id: "1", + title: "Summer Music Festival", + description: "A fantastic outdoor music festival featuring local bands", + venue_name: "Central Park", + start_time: "2024-07-15T18:00:00", + city_name: "New York", + region_abbr: "NY", + }, + { + id: "2", + title: "Tech Conference 2024", + description: "Annual technology conference with industry leaders", + venue_name: "Convention Center", + start_time: "2024-08-20T09:00:00", + city_name: "San Francisco", + region_abbr: "CA", + }, + { + id: "3", + title: "Food Truck Rally", + description: "Delicious food from local food trucks", + venue_name: "Downtown Square", + start_time: "2024-07-22T12:00:00", + city_name: "Austin", + region_abbr: "TX", + }, + { + id: "4", + title: "Art Gallery Opening", + description: "New contemporary art exhibition opening", + venue_name: "Modern Art Museum", + start_time: "2024-07-30T19:00:00", + city_name: "Chicago", + region_abbr: "IL", + }, + { + id: "5", + title: "Comedy Night", + description: "Stand-up comedy show with local comedians", + venue_name: "Laugh Track Club", + start_time: "2024-08-05T20:00:00", + city_name: "Los Angeles", + region_abbr: "CA", + }, + { + id: "6", + title: "Farmers Market", + description: "Fresh produce and local goods", + venue_name: "Town Square", + start_time: "2024-07-25T08:00:00", + city_name: "Portland", + region_abbr: "OR", + }, +] + +// Simple in-memory cache for server-side caching example +const serverCache = new Map() +const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes + +// Helper function to simulate network delay +const simulateDelay = () => { + return new Promise((resolve) => { + // Random delay between 500ms and 2000ms to simulate real API latency + const delay = Math.random() * 1500 + 500 + setTimeout(resolve, delay) + }) +} + +// Search endpoint +app.get("/api/events/search", async (req, res) => { + const { keywords, location } = req.query + const cacheKey = `${keywords || ""}-${location || ""}` + + console.log(`Searching for events with keywords: "${keywords}", location: "${location}"`) + + // Check server-side cache first + const cachedResult = serverCache.get(cacheKey) + if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_DURATION) { + console.log("Returning cached result") + return res.json({ + events: cachedResult.data, + cached: true, + source: "server-cache", + }) + } + + // Simulate API delay + await simulateDelay() + + // Filter events based on search criteria + let filteredEvents = mockEvents + + if (keywords) { + const keywordLower = keywords.toLowerCase() + filteredEvents = filteredEvents.filter( + (event) => + event.title.toLowerCase().includes(keywordLower) || event.description.toLowerCase().includes(keywordLower), + ) + } + + if (location) { + const locationLower = location.toLowerCase() + filteredEvents = filteredEvents.filter( + (event) => + event.city_name.toLowerCase().includes(locationLower) || + event.region_abbr.toLowerCase().includes(locationLower), + ) + } + + // Cache the result + serverCache.set(cacheKey, { + data: filteredEvents, + timestamp: Date.now(), + }) + + console.log(`Found ${filteredEvents.length} events`) + + res.json({ + events: filteredEvents, + cached: false, + source: "api", + }) +}) + +// Health check endpoint +app.get("/api/health", (req, res) => { + res.json({ status: "OK", timestamp: new Date().toISOString() }) +}) + +app.listen(PORT, () => { + console.log(`Event API server running on port ${PORT}`) + console.log(`Try: http://localhost:${PORT}/api/events/search?keywords=music&location=new`) +}) diff --git a/electives/optimization/optimizing-your-react-node-project/src/App.tsx b/electives/optimization/optimizing-your-react-node-project/src/App.tsx new file mode 100644 index 000000000..f25949175 --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/src/App.tsx @@ -0,0 +1,17 @@ +import EventfulSearch from "./components/EventfulSearch" + +function App() { + return ( +
+
+

React Optimization Lesson

+

Learn about caching strategies and React performance optimization

+
+
+ +
+
+ ) +} + +export default App diff --git a/electives/optimization/optimizing-your-react-node-project/src/components/EventfulSearch.tsx b/electives/optimization/optimizing-your-react-node-project/src/components/EventfulSearch.tsx new file mode 100644 index 000000000..2d8813373 --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/src/components/EventfulSearch.tsx @@ -0,0 +1,314 @@ +"use client" + +import type React from "react" +import { useState, useCallback } from "react" + +interface Event { + id: string + title: string + description: string + venue_name: string + start_time: string + city_name: string + region_abbr: string +} + +interface SearchResponse { + events: Event[] + cached: boolean + source: string +} + +// Client-side cache implementation +class ClientCache { + private cache = new Map() + private readonly CACHE_DURATION = 2 * 60 * 1000 // 2 minutes + private readonly MAX_CACHE_SIZE = 50 // Limit memory usage + + get(key: string): SearchResponse | null { + const cached = this.cache.get(key) + if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { + return { ...cached.data, cached: true, source: "client-cache" } + } + if (cached) { + this.cache.delete(key) // Remove expired entry + } + return null + } + + set(key: string, data: SearchResponse): void { + // Implement LRU-like behavior by removing oldest entries when cache is full + if (this.cache.size >= this.MAX_CACHE_SIZE) { + const firstKey = this.cache.keys().next().value + this.cache.delete(firstKey) + } + + this.cache.set(key, { + data: { ...data, cached: false }, + timestamp: Date.now(), + }) + } + + clear(): void { + this.cache.clear() + } + + getStats(): { size: number; maxSize: number } { + return { + size: this.cache.size, + maxSize: this.MAX_CACHE_SIZE, + } + } +} + +const clientCache = new ClientCache() + +const EventfulSearch: React.FC = () => { + const [keywords, setKeywords] = useState("") + const [location, setLocation] = useState("") + const [events, setEvents] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [searchInfo, setSearchInfo] = useState<{ + cached: boolean + source: string + requestTime?: number + } | null>(null) + const [useClientCache, setUseClientCache] = useState(true) + + const searchEvents = useCallback( + async (searchKeywords: string, searchLocation: string) => { + const cacheKey = `${searchKeywords}-${searchLocation}` + + // Check client-side cache first (if enabled) + if (useClientCache) { + const cachedResult = clientCache.get(cacheKey) + if (cachedResult) { + setEvents(cachedResult.events) + setSearchInfo({ + cached: true, + source: cachedResult.source, + requestTime: 0, + }) + return + } + } + + setLoading(true) + setError(null) + const startTime = Date.now() + + try { + const params = new URLSearchParams() + if (searchKeywords) params.append("keywords", searchKeywords) + if (searchLocation) params.append("location", searchLocation) + + const response = await fetch(`http://localhost:3001/api/events/search?${params}`) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data: SearchResponse = await response.json() + const requestTime = Date.now() - startTime + + setEvents(data.events) + setSearchInfo({ + cached: data.cached, + source: data.source, + requestTime, + }) + + // Cache the result on client-side (if enabled and not already cached on server) + if (useClientCache && !data.cached) { + clientCache.set(cacheKey, data) + } + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred") + setEvents([]) + setSearchInfo(null) + } finally { + setLoading(false) + } + }, + [useClientCache], + ) + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault() + searchEvents(keywords, location) + } + + const clearCache = () => { + clientCache.clear() + setSearchInfo(null) + } + + const cacheStats = clientCache.getStats() + + return ( +
+

Event Search

+ +
+
+ setKeywords(e.target.value)} + style={{ + padding: "8px", + marginRight: "10px", + width: "200px", + border: "1px solid #ccc", + borderRadius: "4px", + }} + /> + setLocation(e.target.value)} + style={{ + padding: "8px", + marginRight: "10px", + width: "200px", + border: "1px solid #ccc", + borderRadius: "4px", + }} + /> + +
+ +
+ + +
+
+ + {/* Cache and Performance Info */} +
+ Cache Stats: {cacheStats.size}/{cacheStats.maxSize} entries + {searchInfo && ( +
+ Last Search: {searchInfo.cached ? "Cached" : "Fresh"}({searchInfo.source}) + {searchInfo.requestTime !== undefined && ` - ${searchInfo.requestTime}ms`} +
+ )} +
+ + {error && ( +
+ Error: {error} +
+ )} + + {loading && ( +
+
Searching for events...
+
+ (This may take 1-2 seconds to simulate real API latency) +
+
+ )} + +
+

Results ({events.length} events found)

+ {events.length === 0 && !loading && !error && ( +

+ No events found. Try searching for "music", "tech", or "food" +

+ )} + + {events.map((event) => ( + + ))} +
+
+ ) +} + +// Separate component for individual events - good for optimization exercises +const EventCard: React.FC<{ event: Event }> = ({ event }) => { + console.log(`Rendering EventCard for: ${event.title}`) // For debugging re-renders + + return ( +
+

{event.title}

+

{event.description}

+
+
+ Venue: {event.venue_name} +
+
+ Location: {event.city_name}, {event.region_abbr} +
+
+ Date: {new Date(event.start_time).toLocaleDateString()} +
+
+
+ ) +} + +export default EventfulSearch diff --git a/electives/optimization/optimizing-your-react-node-project/src/components/OptimizedEventCard.tsx b/electives/optimization/optimizing-your-react-node-project/src/components/OptimizedEventCard.tsx new file mode 100644 index 000000000..71eae8797 --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/src/components/OptimizedEventCard.tsx @@ -0,0 +1,93 @@ +"use client" + +import React from "react" + +interface Event { + id: string + title: string + description: string + venue_name: string + start_time: string + city_name: string + region_abbr: string +} + +interface EventCardProps { + event: Event + onDelete?: (id: string) => void + onEdit?: (event: Event) => void +} + +// Optimized version using React.memo for performance comparison +const OptimizedEventCard: React.FC = React.memo(({ event, onDelete, onEdit }) => { + console.log(`Rendering OptimizedEventCard for: ${event.title}`) // For debugging re-renders + + return ( +
+

{event.title}

+

{event.description}

+
+
+ Venue: {event.venue_name} +
+
+ Location: {event.city_name}, {event.region_abbr} +
+
+ Date: {new Date(event.start_time).toLocaleDateString()} +
+
+ + {(onDelete || onEdit) && ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ )} +
+ ) +}) + +OptimizedEventCard.displayName = "OptimizedEventCard" + +export default OptimizedEventCard diff --git a/electives/optimization/optimizing-your-react-node-project/src/main.tsx b/electives/optimization/optimizing-your-react-node-project/src/main.tsx new file mode 100644 index 000000000..50f340bcd --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react" +import ReactDOM from "react-dom/client" +import App from "./App" + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +) diff --git a/electives/optimization/optimizing-your-react-node-project/tsconfig.json b/electives/optimization/optimizing-your-react-node-project/tsconfig.json new file mode 100644 index 000000000..3934b8f6d --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/electives/optimization/optimizing-your-react-node-project/tsconfig.node.json b/electives/optimization/optimizing-your-react-node-project/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/electives/optimization/optimizing-your-react-node-project/vite.config.ts b/electives/optimization/optimizing-your-react-node-project/vite.config.ts new file mode 100644 index 000000000..e9b2c49ec --- /dev/null +++ b/electives/optimization/optimizing-your-react-node-project/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react" + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + }, + build: { + outDir: "dist", + sourcemap: true, + }, +})