Chapter 5: Main App Logic#
Welcome back to the tutorial! In the previous chapters, we've built up individual pieces of our application: * Chapter 1: Application Views showed how we divide our app into different screens like Login, Photo Upload, and Map views. * Chapter 2: User Input Handling (Files & Interaction) explained how we capture user actions, especially file uploads. * Chapter 3: 3D Geographic Visualization introduced how we use Deck.gl and Mapbox to display geographic data in 3D. * Chapter 4: Building Information Display focused on showing detailed information and data loading controls in a sidebar panel.
Now, the big question is: How do all these parts work together? Who decides when to switch views? Who takes the uploaded photo and tells the visualization component to show the corresponding building? Who manages the data that flows between these different pieces?
This is the job of the Main App Logic.
What is Main App Logic?#
Think of your application as an orchestra. You have different sections (the views, the visualization, the info panel, the input handlers). Each section is good at its specific task, but they need someone to coordinate them, tell them when to play, and ensure they're using the same sheet music (the application data).
The Main App Logic acts as the conductor of this orchestra. It's the central control unit that:
- Manages the State: Keeps track of important information that the whole application might need, such as:
- Which view is currently active (Chapter 1).
- The photo file the user selected (Chapter 2).
- The processed geographic data (like building GeoJSON) related to the photo.
- Any extracted metadata (like EXIF tags).
- Loading status (is the app currently busy processing?).
- Any error messages to show.
- Which LAZ file is selected and whether it should be drawn.
- The user's login token.
- Reacts to Input: Listens for signals from user input components (like "a file was selected" or "this button was clicked") and decides what actions to take.
- Orchestrates Data Flow: Gets data from input handlers or API calls and passes it down to the components responsible for display (Chapter 3, Chapter 4).
- Coordinates Actions: Triggers data processing (Chapter 8), API fetching (Chapter 7), and view switches based on the current state and user input.
In essence, the Main App Logic connects all the pieces we've discussed so far, making the application dynamic and responsive.
Where Does the Main App Logic Live?#
In our React application, the main app logic is primarily located within a single, high-level component: MainView
(src/views/main-view.tsx
).
Why put it here? Because MainView
is the component that renders the different application views (LoginView
, PhotoView
, MapResultView
, MapShowcaseView
) using conditional rendering based on the activeLayout
state (Chapter 1). Since it's the parent of these views, it's in a perfect position to:
- Hold state that needs to be shared or influence multiple views.
- Define functions that handle cross-cutting concerns (like what happens after any file is uploaded).
- Pass data and these handler functions down as props to its child view components.
Let's look at the structure of MainView
and how it handles its conductor role.
Core Elements of MainView
as the Conductor#
MainView
uses standard React hooks like useState
and useEffect
to manage the application's state and react to changes.
Managing Application State (useState
)#
MainView
holds the central pieces of data using useState
.
// Inside src/views/main-view.tsx (simplified)
import React, { useState } from "react";
import { LAYOUT } from "../types/layout"; // Import view types
export const MainView = () => {
// Which screen is currently visible? (Starts at Login)
const [activeLayout, setActiveLayout] = useState(LAYOUT.LOGIN);
// The image file the user selected
const [selectedImg, setSelectedImg] = useState<File | null | undefined>(undefined);
// Extracted metadata (EXIF tags) from the image
const [tags, setTags] = useState<any>();
// Processed geographic data (GeoJSON, camera info, metrics)
const [geo, setGeo] = useState<any>();
// Is the app currently loading/processing?
const [loading, setLoading] = useState<boolean>(false);
// Any error messages to display
const [errMsg, setErrMsg] = useState<any>();
// The currently selected LAZ file from the list
const [lazFile, setLazFile] = useState<null | NginxFile>(null); // Assuming NginxFile type
// Should the selected LAZ file be drawn?
const [drawLaz, setDrawLaz] = useState<boolean>(false);
// User's authentication token
const [bearerToken, setBearerToken] = useState<string>('');
// ... other state variables ...
};
These useState
calls define the key pieces of information MainView
needs to keep track of to manage the application. When any of these state variables change (e.g., setSelectedImg
is called with a new file, or setGeo
is called with processed data), MainView
re-renders, and this updated state is passed down to the appropriate child components.
Reacting to State Changes (useEffect
)#
Some actions in the application don't happen directly because of a user click, but rather because a piece of state has changed. For example, when a user selects a file, we don't immediately process it; we update the selectedImg
state. Reacting to this state update is done using the useEffect
hook.
MainView
uses useEffect
for things like:
- Processing the
selectedImg
when it changes. - Creating a temporary URL (
previewImg
) to display the selected image. - Fetching data or updating other state based on changes.
// Inside src/views/main-view.tsx (simplified)
// ... other useState definitions ...
export const MainView = () => {
// ... state definitions ...
// Effect 1: When selectedImg state changes, handle the image processing
useEffect(() => {
if (!selectedImg) { // Do nothing if no image is selected (e.g., cleared)
return;
}
// Call the function to process the image (extract EXIF, fetch building data)
handleImage(selectedImg);
// This effect depends on `selectedImg`. If selectedImg changes, re-run this effect.
}, [selectedImg]);
// Effect 2: When selectedImg state changes, create an object URL for preview
useEffect(() => {
if (!selectedImg) { // If no image, clear the preview URL
setPreviewImg(null);
return;
}
// Create a temporary URL for the browser to display the image
const objectUrl = URL.createObjectURL(selectedImg as any);
setPreviewImg(objectUrl);
// Cleanup function: When the component unmounts or selectedImg changes again,
// revoke the URL to free up memory.
return () => URL.revokeObjectURL(objectUrl);
// This effect depends on `selectedImg`.
}, [selectedImg]);
// ... other effects, handler functions, and render logic ...
};
These useEffect
hooks are listening for changes in their "dependency arrays" ([selectedImg]
). When selectedImg
changes (because the user selected a file and setSelectedImg
was called), both of these effects run, triggering subsequent actions like processing the image and creating a preview.
Handling User Input and Orchestrating Actions#
As we saw in Chapter 2: User Input Handling (Files & Interaction), input events are often handled by functions defined in the child components (like onImageChangeHandler
in PhotoView
). However, these handlers typically just extract the raw input (like the File
object) and then call a prop function provided by MainView
. This allows MainView
to centralize the logic that reacts to the input.
Let's trace the photo upload flow from the user selecting a file to seeing the result on the map:
- User selects a file: In
PhotoView
, theonChange
event on the hidden file input triggersonImageChangeHandler
. PhotoView
callsMainView
's handler:onImageChangeHandler
gets the selectedFile
object and calls theonImageChange
prop function, which is(result) => setSelectedImg(result)
inMainView
.MainView
updates state:setSelectedImg(selectedFile)
is called. This updatesMainView
'sselectedImg
state.useEffect
reacts to state change: TheuseEffect
that depends onselectedImg
runs, callinghandleImage(selectedFile)
.handleImage
starts processing: This function (defined inMainView
) setsloading
totrue
, usesExifReader
to gettags
(Chapter 8), updates thetags
state, and opens the metadata drawer.handleImage
triggers API call: If GPS tags are found,handleImage
callsgetPolygon(lat, lon, altitude, direction)
, another function defined inMainView
.getPolygon
fetches data:getPolygon
setsloading
totrue
again, callsfetchBuilding
(Chapter 7) with the extracted GPS data.getPolygon
updates state and switches view: WhenfetchBuilding
returnsdata
(containing GeoJSON, metrics, etc.),getPolygon
callssetGeo(data)
and importantly,setActiveLayout(LAYOUT.RESULT)
. It then setsloading
back tofalse
.MainView
re-renders: BecauseactiveLayout
andgeo
state have changed,MainView
re-renders.- Conditional rendering switches view: The conditional rendering logic now sees
activeLayout === LAYOUT.RESULT
is true and rendersMapResultView
. - Data flows down:
MainView
passes the updatedgeo
(containing GeoJSON, metrics),tags
,previewImg
,lazFile
,drawLaz
, and relevant handler functions (onLazChangeHandler
,drawLazHandler
, etc.) down as props toMapResultView
. MapResultView
displays results:MapResultView
uses these props. For instance, it passes thegeo
data to a helper function (createBuilding
) to create Deck.gl layers (Chapter 3), passes the layers and view state toDeckglWrapper
for visualization, and passes metrics, tags, and file handlers toBuildingAttributes
for the info display (Chapter 4).
Here's a simplified sequence diagram for this flow:
sequenceDiagram
participant User
participant PhotoView
participant MainView
participant Processing/API (handleImage, getPolygon, fetchBuilding)
participant MapResultView
User->PhotoView: Selects file
PhotoView->MainView: Calls onImageChange prop (setSelectedImg)
MainView->MainView: Updates selectedImg state
MainView->MainView: useEffect triggered by selectedImg change
MainView->Processing/API: Calls handleImage(selectedFile)
Processing/API->MainView: Updates tags state (setTags)
Processing/API->Processing/API: Extracts GPS, Calls getPolygon
Processing/API->Processing/API: Calls fetchBuilding(GPS data)
Processing/API-->Processing/API: Receives GeoJSON/metrics data
Processing/API->MainView: Calls setGeo(data) and setActiveLayout(LAYOUT.RESULT)
MainView->MainView: State updates trigger re-render
MainView->MapResultView: Renders MapResultView with props (geo, tags, etc.)
MapResultView-->>User: Displays map, 3D building, info panel
This diagram illustrates how MainView
sits in the middle, receiving input, triggering background processes/API calls, updating its central state, and then re-rendering the appropriate child views with the necessary data.
Passing Data Down and Handlers Up#
The core pattern enabling this flow is passing data down the component tree via props and passing functions down via props for child components to call when they need to communicate up or trigger an action in the parent.
Let's see how MainView
passes props to MapResultView
:
// Inside src/views/main-view.tsx (simplified)
// ... state and handler function definitions ...
return (
<Box sx={{ display: "flex", height: "100vh" }} ref={ref}>
{/* ... other global UI like CssBaseline ... */}
{/* Conditional Rendering */}
{activeLayout === LAYOUT.RESULT && (
<MapResultView
geo={geo} {/* Pass geo data down */}
view={view} {/* Pass current camera view type */}
imageUrl={previewImg} {/* Pass image preview URL */}
drawLaz={drawLaz} {/* Pass state for drawing LAZ */}
lazFile={lazFile} {/* Pass selected LAZ file */}
tags={tags} {/* Pass extracted metadata */}
previewImg={previewImg} {/* Pass image preview URL (redundant prop in snippet?) */}
extractedDrawerOpen={extractedDrawerOpen} {/* Pass state for metadata drawer */}
// Pass handler functions down as props for MapResultView or its children to call
onLazChange={onLazChangeHandler} {/* Handler for LAZ file selection */}
drawLaz_={drawLazHandler} {/* Handler to trigger LAZ drawing */}
onImageChange={(result) => setSelectedImg(result)} {/* Handler for image upload from inside MapResultView */}
onShowcaseClick={() => setActiveLayout(LAYOUT.SHOWCASE)} {/* Handler to switch to Showcase view */}
setExtractedDrawerOpen={setExtractedDrawerOpen} {/* Setter for metadata drawer state */}
/>
)}
{/* ... other views and global components ... */}
</Box>
);
This snippet from MainView
's return
block shows it rendering MapResultView
only when activeLayout
is LAYOUT.RESULT
. It then explicitly passes each piece of relevant state (geo
, view
, tags
, etc.) and each relevant function (onLazChangeHandler
, drawLazHandler
, setSelectedImg
, setActiveLayout
, setExtractedDrawerOpen
) as props.
MapResultView
(and components it renders like BuildingAttributes
or DeckglWrapper
) receives these props and uses them:
geo
is used to create Deck.gl layers (Chapter 3) and metrics (Chapter 4).tags
are displayed in the metadata drawer (Chapter 4).onLazChangeHandler
is passed down to theBuildingAttributes
component's LAZ dropdown (Chapter 4). When the user selects a file there,BuildingAttributes
calls this prop function, which then updates thelazFile
state inMainView
.
This constant flow of data down and action calls up is the hallmark of how components communicate in React and how MainView
maintains control as the central logic unit.
Coordinating Global UI#
MainView
also manages elements that appear across multiple views or are global to the application, such as:
- The
BottomNav
component, which allows switching between views.MainView
passes theactiveLayout
andonLayoutChange
handler to it. - The
Snackbar
for displaying messages (errMsg
). - The
Backdrop
andCircularProgress
for showing a loading spinner (loading
state). - The
InfoButton
andInfoModal
.
By managing the state (activeLayout
, errMsg
, loading
, infoOpen
) for these components in MainView
, the conductor can ensure they are displayed correctly based on the overall application status, regardless of which main content view (PhotoView
, MapResultView
, etc.) is currently active.
Conclusion#
The Main App Logic, centered in the MainView
component, is the heart of our application. It acts as the conductor, managing the overall state, receiving inputs from user interaction components (Chapter 2), triggering background processes and API calls (Chapter 7, Chapter 8), deciding which view to display (Chapter 1), and passing data and necessary functions down to the visualization (Chapter 3) and information display (Chapter 4) components.
By centralizing this coordination in MainView
using useState
, useEffect
, and the pattern of passing props down and calling prop functions up, we keep the application organized and make the flow of data and control clear.
Now that we understand how the application logic orchestrates the different parts, the next step is to look at the fundamental data structures, or Data Models, that represent the information flowing through the system.
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/App/App.tsx), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/views/main-view.tsx)