Skip to content

[Issue 2146] Replace Missing Content in the Optimizing Your React Node Project #2502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions electives/optimization/optimizing-your-react-node-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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=<YOUR_KEY_HERE> node eventful-api-server.js
```plaintext
$ npm run server
```

Import and place the `<EventfulSearch />` 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 `<EventfulSearch />` 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:

Expand All @@ -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).

Expand All @@ -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 `<Event />` 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 `<Event />` 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Optimization Lesson</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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`)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import EventfulSearch from "./components/EventfulSearch"

function App() {
return (
<div className="App">
<header style={{ padding: "20px", textAlign: "center", backgroundColor: "#f5f5f5" }}>
<h1>React Optimization Lesson</h1>
<p>Learn about caching strategies and React performance optimization</p>
</header>
<main>
<EventfulSearch />
</main>
</div>
)
}

export default App
Loading