fix(guides): fix hard reload empty state and frame rendering bugs

- Split GuideForm into outer/inner components to gate rendering on
  guide context loading, fixing empty edit form on hard reload
- Add loading state to GuideContext
- Replace URL.createObjectURL in render with useMemo + useEffect cleanup
  to fix memory leak and empty src warning
- Fix frame reorder bug by keying Frame on frame.id instead of index
This commit is contained in:
johannes-hernehult 2026-06-17 20:21:46 +02:00
parent ea6adc236e
commit eb30a325db
4 changed files with 35 additions and 24 deletions

View file

@ -1,6 +1,7 @@
import "./Frame.css";
import type { FrameType, StepType } from "../../../lib/schemas";
import Step from "./Step";
import { useEffect, useMemo } from "react";
export default function Frame({
frame,
@ -21,6 +22,15 @@ export default function Frame({
updateTitle: (frameId: string, title: string) => void;
reOrder: (startIndex: number, endIndex: number) => void;
}) {
const objectUrl = useMemo(
() => URL.createObjectURL(frame.file),
[frame.file],
);
useEffect(() => {
return () => URL.revokeObjectURL(objectUrl);
}, [objectUrl]);
return (
<div className="frame">
<div className="buttons">
@ -38,7 +48,6 @@ export default function Frame({
>
Down
</button>
<button
type="button"
className="remove"
@ -47,7 +56,6 @@ export default function Frame({
Remove Frame
</button>
</div>
<label>
Title
<input
@ -56,7 +64,7 @@ export default function Frame({
onChange={(e) => updateTitle(frame.id, e.target.value)}
/>
</label>
<img src={URL.createObjectURL(frame.file)} alt={frame.title} />
<img src={objectUrl} alt={frame.title} />
<button
type="button"
className="add-step"
@ -64,7 +72,6 @@ export default function Frame({
>
Add Step
</button>
<div className="steps">
{frame.steps.map((step: StepType, index: number) => (
<Step

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import {
type CompiledGuideType,
type FrameType,

View file

@ -4,12 +4,9 @@ import useGuideForm from "../hooks/useGuideForm";
import Frame from "../components/Frame";
import { useParams } from "react-router";
import { useGuides } from "../../../lib/contexts/guide.context";
import type { CompiledGuideType } from "../../../lib/schemas";
export default function GuideForm() {
const { id } = useParams();
const { guides } = useGuides();
const g = guides.find((g) => g.id === id);
function GuideFormInner({ initial }: { initial?: CompiledGuideType }) {
const {
guide,
frames,
@ -23,8 +20,7 @@ export default function GuideForm() {
handleRemoveStep,
handleUpdateStep,
handleSaveGuide,
isEditing,
} = useGuideForm(g);
} = useGuideForm(initial);
return (
<div className="new-guide">
@ -46,13 +42,11 @@ export default function GuideForm() {
onChange={(e) => handleUpdateDescription(e.target.value)}
/>
</label>
<FileUpload onChange={handleAddFrames} />
{frames.length > 0 &&
frames.map((frame, index) => (
<Frame
key={index}
key={frame.id}
frameIndex={index}
frame={frame}
onRemove={handleRemoveFrame}
@ -63,13 +57,21 @@ export default function GuideForm() {
reOrder={handleReOrderFrames}
/>
))}
{frames.length === 0 && <p>No images added yet.</p>}
{frames.length > 0 && <FileUpload onChange={handleAddFrames} />}
<button type="submit">Save</button>
</form>
</div>
);
}
export default function GuideForm() {
const { id } = useParams();
const { guides, loading } = useGuides();
if (loading) return null;
const initial = id ? guides.find((g) => g.id === id) : undefined;
return <GuideFormInner initial={initial} />;
}

View file

@ -12,28 +12,30 @@ import { useIDB } from "./dbinit.context";
interface GuideContextValue {
guides: CompiledGuideType[];
setGuides: React.Dispatch<React.SetStateAction<CompiledGuideType[]>>;
loading: boolean;
}
const GuideContext = createContext<GuideContextValue | undefined>(undefined);
export function GuideProvider({ children }: { children: ReactNode }) {
const [guides, setGuides] = useState<CompiledGuideType[]>([]);
const [loading, setLoading] = useState(true);
const { db } = useIDB();
useEffect(() => {
if (!db) return;
db.getAll("guides").then((fetchedGuides) => {
const sortedGuides = [...fetchedGuides].sort((a, b) => {
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
});
const sortedGuides = [...fetchedGuides].sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
setGuides(sortedGuides);
setLoading(false);
});
}, [db]);
return (
<GuideContext.Provider value={{ guides, setGuides }}>
<GuideContext.Provider value={{ guides, setGuides, loading }}>
{children}
</GuideContext.Provider>
);