In the previous chapter, Core State Definition (The Store), we learned how to create a "Single Source of Truth" (our Whiteboard) using AppState. We learned how to write data to it and how to read the raw data back.
However, reading raw data is often messy. The raw database might be complex, but your UI usually wants a simple answer to a simple question.
This is where State Selectors come in.
Imagine your AppState store is a massive library containing thousands of books (data).
In technical terms, Selectors are pure functions that take the raw state as input and return derived data.
Let's look at a real problem in our application.
We have a chat input box. When the user types a message and hits Enter, where should that message go?
The "Bad" Way (Doing it manually everywhere): Every time we render the Input box, we would have to write complex logic like this:
// messy_component_logic.ts
const state = store.getState();
// Check if we are viewing a specific agent
if (state.viewingAgentTaskId) {
const task = state.tasks[state.viewingAgentTaskId];
// Check if that task actually exists and is the right type
if (task && task.type === 'in_process_teammate') {
console.log("Send to Teammate");
}
} else {
console.log("Send to Leader");
}
This is fragile. If we change how tasks are stored, we have to fix this code in 10 different places.
The "Selector" Way: We move that logic into a helper function (a Selector).
// component.ts
import { getActiveAgentForInput } from './selectors';
// Just ask the question!
const destination = getActiveAgentForInput(store.getState());
Derived Data is information that doesn't need to be saved in the database because it can be calculated from existing data.
birthYear: 1990age: 34 (Calculated as CurrentYear - birthYear)
We do not store age in AppState. We calculate it using a Selector. This ensures the data never gets out of sync.
A Selector is not a special framework feature. It is just a standard TypeScript function.
AppState as an argument.Here is the flow of data when a component uses a selector:
selectors.tsLet's build the solution for our "Who am I talking to?" use case. We will break it down into two small selectors.
First, we make a small selector just to find the "Viewed Teammate".
// selectors.ts (Part 1)
export function getViewedTeammateTask(appState: AppState) {
const { viewingAgentTaskId, tasks } = appState
// 1. Are we even looking at an agent?
if (!viewingAgentTaskId) return undefined
// 2. Does the task exist in our list?
const task = tasks[viewingAgentTaskId]
if (!task) return undefined
// 3. Is it the right type (a teammate)?
// (We use a helper check function here)
if (!isInProcessTeammateTask(task)) return undefined
return task
}
Explanation: This function safely digs through the data. If anything is missing, it returns undefined. The UI doesn't need to know how to find the task, it just gets the result.
Now we solve the main problem: Where does the input go? We use the result of the helper above.
// selectors.ts (Part 2)
export function getActiveAgentForInput(appState: AppState) {
// Reuse logic from the helper above!
const viewedTask = getViewedTeammateTask(appState)
// Case A: We are looking at a teammate
if (viewedTask) {
return { type: 'viewed', task: viewedTask }
}
// Case B: Default to the Leader
return { type: 'leader' }
}
Explanation: This function returns a simple object (a "Discriminated Union") that tells the UI exactly what to do.
viewingAgentTaskId to focusedId in the future, we only have to update code in one file (selectors.ts). The rest of the app doesn't care.if (task) in our components. The selector handles the safety checks.getActiveAgentForInput(state) is much easier to read than a 5-line if/else statement.In this chapter, we learned:
Now that we know how to read complex data, let's learn how to change it in a structured way. Instead of random updates, we will use Domain Actions.
Next Chapter: Teammate View Logic (Domain Actions)
Generated by Code IQ