Chapter 4: Global State Management#
Welcome back to the loaders.gl-showcases
tutorial! In the previous chapter, we looked at Layer Management and how the application controls which datasets and basemaps are visible on the map. We saw that clicking a checkbox or selecting a basemap needed to somehow tell the map visualization component (Chapter 2) what to display. But how does this information travel from the UI component to the map, and how is it kept consistent across potentially many different parts of the application?
Imagine you have multiple panels open, a button that saves the current view as a bookmark ([Chapter 5]), and the main map display. They all need to know things like:
- What is the current camera position (view state)?
- Which layers are currently visible?
- What debug options are turned on?
- Which basemap is selected?
If every component tried to manage this information itself or pass it directly to every other component that needed it, the code would become incredibly tangled and hard to understand. This is where Global State Management comes in.
What is Global State Management?#
Think of Global State Management as setting up a single, central information hub or a "control room" for your entire application. Instead of different parts of the application trying to keep track of important details on their own, they all agree to store and retrieve this information from one place.
As the concept description says:
Using Redux Toolkit, the application keeps track of all important information in one central place. This includes the current camera view, which layers are loaded and visible, what debug options are active, and more. Think of it as the control room for the entire application, where all the important switches and monitors are located to keep everything running smoothly and consistently.
This central place holds the "state" of your application β all the data that describes what the application is currently doing or showing.
The loaders.gl-showcases
application uses a popular library called Redux Toolkit to help manage this global state. Redux Toolkit makes it easier to set up and work with a central state store in a predictable way.
Key Concepts in Redux Toolkit#
Let's look at the basic ideas behind Redux Toolkit that are used in the application. You don't need to be a Redux expert to follow along, just understand the role of these pieces:
- Store: This is the main "control room" or container that holds the entire application's state. There's only one store in a Redux application.
- State: The actual data living inside the store. It's organized like a large JavaScript object. For example, it might have sections for
viewState
,flattenedSublayers
,debugOptions
, etc. - Actions: These are plain JavaScript objects that describe something that happened in the application (e.g.,
{ type: 'layer/visibilityChanged', payload: { id: 'buildings', visible: false } }
or{ type: 'viewState/setViewState', payload: { main: { longitude: ..., latitude: ... } } }
). They are the only way to signal that you want to change the state. - Reducers: These are pure functions that take the current state and an action as input, and return a new state. They specify how the state should change in response to an action. Reducers are the only things allowed to change the state in the store. They never modify the existing state directly; they always create a new state object.
- Slices: Redux Toolkit organizes related state and reducers into "slices". A slice typically manages a specific part of the overall state (like everything related to
viewState
ordebugOptions
). This makes the code more modular. - Selectors: These are functions that help you easily extract specific pieces of data from the large state object in the store.
Central Use Case: Synchronizing Camera View#
Let's revisit the camera view (or "view state") from Chapter 2: Map Visualization. The map wrapper component (DeckGlWrapper
or ArcgisWrapper
) needs to know where the camera is looking to draw the map correctly. When the user pans or zooms, the map wrapper calculates the new camera position. This new position needs to be available to other parts of the application, like a panel displaying the current coordinates, or a bookmark feature that saves this exact view.
Global State Management solves this: the camera position is stored in the central state. The map wrapper reads the current position from the state, and when the user interacts, it dispatches an action to update that position in the state. Other components that care about the camera position also read it from the same central state.
How to Use Global State (View State Example)#
Components that need to interact with the global state typically do two things:
- Read data from the state: Using a selector function.
- Dispatch actions to update the state: Using a function provided by Redux Toolkit.
Let's look at how a component might read and write the viewState
. The application provides custom hooks, useAppSelector
and useAppDispatch
(src/redux/hooks.ts
), which are the standard way to interact with the Redux store in React applications using Redux Toolkit.
1. Reading Data from State (Using a Selector)
Suppose a component needs to know the current main camera position:
// In a React component file (.tsx)
import { useAppSelector } from '../../redux/hooks'; // Import the selector hook
import { selectViewState } from '../../redux/slices/view-state-slice'; // Import the specific selector
function SomeComponentThatNeedsCameraPosition() {
// Use the hook to get the view state from the global store
const currentViewState = useAppSelector(selectViewState);
// currentViewState will be an object like { main: {...}, minimap: {...} }
return (
<div>
{/* Displaying latitude from the state */}
Current Latitude: {currentViewState.main?.latitude.toFixed(2)}
</div>
);
}
useAppSelector
is the hook that connects your component to the Redux store.- You pass a selector function (
selectViewState
in this case) touseAppSelector
. - The selector function knows how to find the specific piece of data you want (
viewState
) inside the overall big state object (RootState
). useAppSelector
runs your selector function on the current state in the store and returns the result (currentViewState
).- Crucially, if the
viewState
in the store changes,useAppSelector
will automatically cause your component to re-render with the new data!
2. Dispatching Actions to Update State (Using useAppDispatch
)
Suppose a component (like the map wrapper when the user pans) needs to update the main camera position:
// In a React component file (.tsx)
import { useAppDispatch } from '../../redux/hooks'; // Import the dispatch hook
import { setViewState } from '../../redux/slices/view-state-slice'; // Import the action creator
function MapWrapperComponent() {
// Get the function to dispatch actions
const dispatch = useAppDispatch();
// Imagine this function is called by the map library when the view changes
const handleMapViewStateChange = (newDeckGlViewState) => {
// Create the action payload
const actionPayload = { main: newDeckGlViewState };
// Dispatch the action to update the state in the store
dispatch(setViewState(actionPayload));
};
// ... rest of the component rendering the map ...
return (
<DeckGlWrapper onViewStateChange={handleMapViewStateChange} /* ... */ />
);
}
useAppDispatch
is the hook that gives you thedispatch
function.dispatch
is how you send an action to the Redux store.setViewState
is an action creator. It's a function (generated automatically by Redux Toolkit for each reducer you define) that creates the action object for you. When you callsetViewState({ main: newDeckGlViewState })
, it returns an action like{ type: 'viewState/setViewState', payload: { main: newDeckGlViewState } }
.dispatch(setViewState(actionPayload))
sends this action to the store. The store then finds the correct reducer (in theviewState
slice) based on the action'stype
and runs it to update the state.
This is the core cycle: components read state using selectors and write state by dispatching actions.
Under the Hood: How Redux Toolkit Manages State#
Let's peek behind the curtain at how the Redux store and slices are structured.
1. The Store: The single source of truth is configured in src/redux/store.ts
. This file brings together all the different "slices" of state using combineReducers
.
// Simplified snippet from src/redux/store.ts
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import flattenedSublayersSliceReducer from "./slices/flattened-sublayers-slice";
import debugOptionsSliceReducer from "./slices/debug-options-slice";
import viewStateSliceReducer from "./slices/view-state-slice";
// ... other slice reducers
// Combine all the individual slice reducers into one root reducer
const rootReducer = combineReducers({
flattenedSublayers: flattenedSublayersSliceReducer, // State managed by flattenedSublayers slice
debugOptions: debugOptionsSliceReducer, // State managed by debugOptions slice
viewState: viewStateSliceReducer, // State managed by viewState slice
// ... other state slices
});
// Configure the store with the root reducer
export const setupStore = (preloadedState?) => {
return configureStore({
reducer: rootReducer,
// ... middleware configuration ...
});
};
export type RootState = ReturnType<typeof rootReducer>; // Defines the shape of the total state
// ... other types
combineReducers
takes an object where keys are the names of the different state sections (likeviewState
,debugOptions
) and values are the reducers from the corresponding slices. This creates the overall state structure (e.g.,state.viewState
,state.debugOptions
).configureStore
sets up the actual Redux store instance, using the combined reducer.
2. A State Slice (View State Example): Each piece of related state is managed in its own slice file. Let's look at view-state-slice.ts
(src/redux/slices/view-state-slice.ts
).
// Simplified snippet from src/redux/slices/view-state-slice.ts
import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
import { type ViewState } from "@deck.gl/core";
import { type RootState } from "../store"; // Import RootState type
// Define the structure of the state this slice manages
export interface ViewStateState {
/** main viewport */
main?: ViewState;
/** minimap viewport */
minimap?: ViewState;
}
// Define the initial state when the application starts
const initialState: ViewStateState = {
main: { /* default camera settings */ longitude: -120, latitude: 34, zoom: 14.5, /* ... */ },
minimap: { /* default minimap camera settings */ },
};
// Create the slice
const viewStateSlice = createSlice({
name: "viewState", // Name of the slice (used in action types like 'viewState/setViewState')
initialState, // The initial state
reducers: { // Define the reducers (functions that handle actions)
setViewState: (
state: ViewStateState, // The *current* state managed by this slice
action: PayloadAction<ViewStateState> // The action object, with payload type
) => {
// Redux Toolkit uses Immer internally, allowing 'mutations'
// This looks like modifying state directly, but Immer creates a new state behind the scenes
return { ...state, ...action.payload }; // Create and return the NEW state
},
// ... other reducers if needed for this slice ...
},
});
// Export the action creator(s) generated by createSlice
export const { setViewState } = viewStateSlice.actions;
// Export selector(s) to easily get this slice's state
export const selectViewState = (state: RootState): ViewStateState =>
state.viewState; // Access the 'viewState' section of the overall RootState
// Export the reducer function itself (used in store.ts)
export default viewStateSlice.reducer;
createSlice
is the main function from Redux Toolkit. You give it aname
,initialState
, and define yourreducers
.- For each function you define in
reducers
(likesetViewState
), Redux Toolkit automatically creates an action type ('viewState/setViewState'
) and an action creator function (setViewState
). - The
setViewState
reducer function receives the currentstate
for this slice only and theaction
. It updates the state based onaction.payload
and returns the new state. Redux Toolkit uses a library called Immer which lets you write state updates as if you are directly modifying the state (state.someProperty = value
), but it actually handles creating an immutable new state object for you. - The
selectViewState
function is a simple arrow function that takes the entireRootState
and returns just theviewState
part of it (state.viewState
).
The Data Flow Cycle
Putting it together, here's a simplified flow when the user pans the map:
sequenceDiagram
Participant User
Participant MapWrapper
Participant ReduxDispatch
Participant ViewStateSliceReducer
Participant ReduxStore
Participant OtherComponents
User->>MapWrapper: Pans the map
MapWrapper->>MapWrapper: Calculates new camera position
MapWrapper->>ReduxDispatch: Calls dispatch(setViewState(newPosition))
ReduxDispatch->>ViewStateSliceReducer: Dispatches { type: 'viewState/setViewState', payload: newPosition }
ViewStateSliceReducer->>ReduxStore: Returns new state object
ReduxStore->>MapWrapper: State change notifies components using selectors
ReduxStore->>OtherComponents: State change notifies components using selectors
MapWrapper->>User: Map re-renders with new camera position
OtherComponents->>User: UI updates based on new view state (e.g., coordinate display)
- The User interacts with the Map Wrapper.
- The Map Wrapper component calculates the new camera position.
- Instead of updating its own internal state or telling the map library directly and telling other components, it uses
dispatch
to send thesetViewState
action with the new position as the payload. - The Redux store receives the action and finds the
setViewState
reducer in theviewState
slice. - The
setViewState
reducer updates theviewState
part of the global state, creating a new state object. - The Redux store updates its internal state to this new object.
- Because the state has changed, any components using
useAppSelector
withselectViewState
(like the Map Wrapper itself, orSomeComponentThatNeedsCameraPosition
) are notified and will re-render. - The Map Wrapper component re-renders, receives the new
viewState
from the updated state, and tells the underlying map library to render the scene from the new camera position. SomeComponentThatNeedsCameraPosition
also re-renders, reads the new state, and updates the coordinates displayed on screen.
This ensures that the camera position is always consistent across the entire application, and any component that needs this information simply reads it from the central store.
Other Pieces of State Managed Globally#
The viewState
is just one example. Many other important application settings are managed this way using different slices:
- Loaded and Visible Layers/Sublayers: The list of layers added by the user, their type, URL, and critically, their visibility toggled in the Layers Panel. This is managed primarily by the
flattened-sublayers-slice.ts
(src/redux/slices/flattened-sublayers-slice.ts
). - Debug Options: Settings like showing bounding volumes, wireframe mode, tile coloring modes, etc., controlled by the Debug Panel ([Chapter 8]). These are managed by
debug-options-slice.ts
(src/redux/slices/debug-options-slice.ts
). - Basemap Selection: Which background map (street, satellite, etc.) is currently active. Managed by
base-maps-slice.ts
(code not fully provided, but conceptually similar). - Symbolization Settings: How layers are colored or filtered based on their attributes (e.g., coloring buildings by height). Managed by
symbolization-slice.ts
(src/redux/slices/symbolization-slice.ts
). - Comparison Mode State: Which side (left/right) is active, what layers are on each side (handled by extending other slices like
flattenedSublayersState
), and the position of the comparison slider ([Chapter 6]).
Each of these areas has its own slice (createSlice
) in the src/redux/slices
directory, defining its initialState
, reducers
to handle updates, and selectors
to read the data. They all follow the same fundamental pattern demonstrated by the viewState
slice.
Benefits of Global State Management#
Using a system like Redux Toolkit for global state provides significant advantages:
- Single Source of Truth: All important application data is in one predictable place, preventing inconsistencies.
- Predictable State Updates: State can only be changed by dispatching actions, and reducers are pure functions. This makes it much easier to understand why and how the state changed.
- Easier Debugging: Tools are available that let you see every action that was dispatched and how the state changed as a result, making it simpler to track down bugs.
- Decoupling: Components don't need to have direct knowledge of other components. They just read from or write to the central state, which other components also read from. This makes components more reusable and the application structure cleaner.
- Avoids "Prop Drilling": Without global state, you might have to pass data down through many layers of components that don't actually use the data themselves, just to get it to a deeply nested component that does. Global state lets any component connected to the store access the data it needs directly via selectors.
Conclusion#
In this chapter, we explored Global State Management as the central "control room" for the loaders.gl-showcases
application, powered by Redux Toolkit. We learned that important application data like camera view, visible layers, and settings are stored in a single Store. We saw how components read this state using Selectors (via useAppSelector
) and update it by Dispatching Actions (via useAppDispatch
and Action Creators), which are processed by Reducers organized within Slices. This pattern ensures consistency and predictability throughout the application.
Understanding how the application manages its global state is key to seeing how different features interact. In the next chapter, we'll look at a feature that heavily relies on global state: Bookmarks, which save and restore the application's state, including the camera view and visible layers, directly from the global store.
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/hooks.ts), 2(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/slices/debug-options-slice.ts), 3(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/slices/flattened-sublayers-slice.ts), 4(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/slices/symbolization-slice.ts), 5(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/slices/view-state-slice.ts), 6(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/store.ts)