Skip to content

Conversation

@interim17
Copy link
Contributor

@interim17 interim17 commented Nov 11, 2025

Problem

Changes discussed with @meganrm for state management and some optimizations to prefetching of input options and recipe data.

Basically part 2 of #150 and should address the TODOs I added there.

Solution

Instead of RecipeManifest or RecipeManifest/RecipeData ===> we use:RecipeMetadata and RecipeData

RecipeMetadata is whatever we can get on our first trip to server when we call getAllDocsFromCollection.
That means no actual recipe and no unpacked editableFields.

Regardless how we would like to shape this data up front, it makes sense to differentiate these data since they load at very different rates.

Splitting the firebase utils into a batch meta data loader, and a per-ID utility that gets the details (it's slower), makes loading the page smoother.

We can also get rid of isLoading state for now and derive it from the presence of either the input options, or a given recipe object.

Default result URLs are in the metadata, but I think we should prevent loading them beacuse it looks weird to have the viewer populate before the recipe panel does, so I throttled them to go together.

@github-actions
Copy link

github-actions bot commented Nov 11, 2025

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 20.23% 367 / 1814
🔵 Statements 20.23% 367 / 1814
🔵 Functions 40% 24 / 60
🔵 Branches 71.05% 81 / 114
File Coverage
File Stmts % Branch % Funcs % Lines Uncovered Lines
Changed Files
src/components/PackingInput/index.tsx 0% 0% 0% 0% 1-2, 12-16, 26-30, 32-35, 37-40, 43-45, 47-49, 51, 54-56, 58-68, 70-71, 73-80, 82, 84, 86
src/state/store.ts 0% 0% 0% 0% 1, 51, 53-59, 61-63, 65, 67-70, 72-86, 88-89, 91-92, 94-96, 99-101, 104, 107-114, 116-118, 120-122, 124-127, 129-137, 139-150, 152-163, 165-167, 169, 171-176, 178-187, 189-192, 195-204, 207-213, 215-216, 218-226, 228-245, 247-263, 266-273, 275-280, 282-287, 289-293, 295-299, 301-302, 306-308, 310-313, 315-318, 320-323, 325-328, 330-341, 344-359
src/types/index.ts 100% 100% 100% 100%
src/utils/firebase.ts 25.98% 62.5% 25% 25.98% 35-36, 41-42, 76-78, 81-91, 94-98, 101-109, 113-126, 129-134, 137-139, 142-166, 174-184, 186-197, 204-216, 230-231, 234-244, 246-255, 257, 259-263, 265-270
Generated in workflow #169

@github-actions
Copy link

github-actions bot commented Nov 11, 2025

PR Preview Action v1.6.2
Preview removed because the pull request was closed.
2025-11-13 20:05 UTC

Comment on lines +70 to +81
{!recipeObj ? (
loadingText
) : (
<Tabs defaultActiveKey="1" className="recipe-content">
<Tabs.TabPane tab="Edit" key="1">
<RecipeForm onStartPacking={handleStartPacking} />
</Tabs.TabPane>
<Tabs.TabPane tab="Full Recipe" key="2">
<JSONViewer title="Recipe" content={recipeObj} />
</Tabs.TabPane>
</Tabs>
)}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't a good reason to prevent this panel from loading once the first recipe has been fetched, but I figured showing the loading marker was in line with our overall design pattern, and it should only be visible very briefly if the user selects a recipe right after the dropdown populates.

Comment on lines 80 to 102
const { inputOptions, loadRecipe } = get();

const optionList = Object.values(inputOptions || {});
if (optionList.length === 0) return;

const recipeIds = optionList
.map(o => o?.recipeId)
.filter(id => id && !get().recipes[id]);

// Make sure our default initial is in the options we queried
const initialIdToLoad =
recipeIds.includes(INITIAL_RECIPE_ID) ? INITIAL_RECIPE_ID : recipeIds[0];

// Ensure the bootstrap recipe is loaded & selected
if (!get().recipes[initialIdToLoad]) {
await loadRecipe(initialIdToLoad);
}
get().selectRecipe(recipeToLoad);

// Load remaining recipes in the background (don’t block)
const remainingRecipesToLoad = recipeIds.filter(
id => id !== initialIdToLoad && !get().recipes[id]
);
await Promise.all(remainingRecipesToLoad.map((id) => loadRecipe(id)));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for churn here, but I do think this is better.

Hopefully reads well and it loads the initial recipe before moving on to the rest.

@interim17 interim17 mentioned this pull request Nov 11, 2025
@interim17 interim17 requested a review from Copilot November 11, 2025 03:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the recipe data loading architecture by splitting RecipeManifest into two distinct types: RecipeMetadata (fast-loading fields available immediately from the collection query) and RecipeData (slower-loading fields like recipe content and editable fields). The refactoring optimizes page load performance by fetching metadata in bulk while deferring individual recipe details until needed.

Key changes:

  • Split data types to differentiate between quickly-available metadata and slower recipe details
  • Removed isLoading state in favor of deriving loading status from data presence
  • Implemented throttling to prevent result viewer from loading before recipe panel
  • Updated bootstrap logic to load initial recipe first, then remaining recipes in background

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/utils/firebase.ts Refactored data fetching functions to separate metadata batch loading from individual recipe data loading
src/types/index.ts Split RecipeManifest into RecipeMetadata and reorganized RecipeData interface
src/state/store.ts Removed isLoading state, updated selectors and loading logic to work with new data types
src/components/PackingInput/index.tsx Replaced isLoading check with derived loading state based on data presence
src/components/Dropdown/index.tsx Updated type reference from RecipeManifest to RecipeMetadata

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@interim17 interim17 changed the title split manifest into RecipeMetadata and RecipeData and refactor prefet… RecipeMetadata and prefetching optimizations Nov 11, 2025
interim17 and others added 4 commits November 10, 2025 19:55
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@interim17 interim17 marked this pull request as ready for review November 11, 2025 04:01
Copy link
Contributor

@ascibisz ascibisz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the distinction you came up with of RecipeMetadata and RecipeData. Everything looks good and works well for me!

placeholder: string;
defaultValue?: string;
options: Dictionary<RecipeManifest>;
options: Dictionary<RecipeMetadata>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally I still like the term manifest but I'm not super attached to it. it's what we use in other apps like CFE for this same type of data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sure I like manifest too, for some reason I thought you said at our 1:1 that you wanted it called metadata.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I think I was just using that as a category term, sorry!

recipeId: string;
configId: string;
displayName: string;
defaultRecipeData: ViewableRecipe;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you're calling this whole object RecipeData, having defaultRecipeData inside it is a little unclear. I think I would just call this defaultRecipe. or originalRecipe (and then the edits are spicy 😅🍗)

Base automatically changed from feature/ssot-refactor to main November 13, 2025 19:37
@meganrm meganrm requested a review from Copilot November 13, 2025 19:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

*/
const getRecipeDataFromFirebase = async (recipeId: string, editableFieldIds: string[]): Promise<RecipeData> => {
const defaultRecipe = await getFirebaseRecipe(recipeId);
const editableFields = await getEditableFieldsList(editableFieldIds) || [];
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || [] fallback is unnecessary because getEditableFieldsList already returns undefined when the input array is empty, and the function signature is Promise<EditableField[] | undefined>. The || [] will not catch the undefined case. Instead, handle the undefined explicitly: const editableFields = await getEditableFieldsList(editableFieldIds); if (!editableFields) return ...; or change the return type expectation.

Suggested change
const editableFields = await getEditableFieldsList(editableFieldIds) || [];
const editableFields = await getEditableFieldsList(editableFieldIds);
if (!editableFields) {
return {
recipeId,
defaultRecipe,
editableFields: [],
edits: {}
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 182 to 188
const editedValue = lodashGet(rec.edits, path);
if (editedValue !== undefined) {
if (typeof editedValue === "string" || typeof editedValue === "number") {
return editedValue;
}
return undefined;
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using lodashGet on rec.edits is incorrect because edits is a flat Record<string, string | number> where keys are paths, not a nested object. The path should be used directly as a key: const editedValue = rec.edits[path];

Copilot uses AI. Check for mistakes.
@meganrm meganrm requested a review from Copilot November 13, 2025 19:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

meganrm and others added 3 commits November 13, 2025 12:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@meganrm meganrm merged commit 61ae362 into main Nov 13, 2025
2 checks passed
@meganrm meganrm deleted the fix/prefetch branch November 13, 2025 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants