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

View file

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

View file

@ -4,12 +4,9 @@ import useGuideForm from "../hooks/useGuideForm";
import Frame from "../components/Frame"; import Frame from "../components/Frame";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { useGuides } from "../../../lib/contexts/guide.context"; import { useGuides } from "../../../lib/contexts/guide.context";
import type { CompiledGuideType } from "../../../lib/schemas";
export default function GuideForm() { function GuideFormInner({ initial }: { initial?: CompiledGuideType }) {
const { id } = useParams();
const { guides } = useGuides();
const g = guides.find((g) => g.id === id);
const { const {
guide, guide,
frames, frames,
@ -23,8 +20,7 @@ export default function GuideForm() {
handleRemoveStep, handleRemoveStep,
handleUpdateStep, handleUpdateStep,
handleSaveGuide, handleSaveGuide,
isEditing, } = useGuideForm(initial);
} = useGuideForm(g);
return ( return (
<div className="new-guide"> <div className="new-guide">
@ -46,13 +42,11 @@ export default function GuideForm() {
onChange={(e) => handleUpdateDescription(e.target.value)} onChange={(e) => handleUpdateDescription(e.target.value)}
/> />
</label> </label>
<FileUpload onChange={handleAddFrames} /> <FileUpload onChange={handleAddFrames} />
{frames.length > 0 && {frames.length > 0 &&
frames.map((frame, index) => ( frames.map((frame, index) => (
<Frame <Frame
key={index} key={frame.id}
frameIndex={index} frameIndex={index}
frame={frame} frame={frame}
onRemove={handleRemoveFrame} onRemove={handleRemoveFrame}
@ -63,13 +57,21 @@ export default function GuideForm() {
reOrder={handleReOrderFrames} reOrder={handleReOrderFrames}
/> />
))} ))}
{frames.length === 0 && <p>No images added yet.</p>} {frames.length === 0 && <p>No images added yet.</p>}
{frames.length > 0 && <FileUpload onChange={handleAddFrames} />} {frames.length > 0 && <FileUpload onChange={handleAddFrames} />}
<button type="submit">Save</button> <button type="submit">Save</button>
</form> </form>
</div> </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 { interface GuideContextValue {
guides: CompiledGuideType[]; guides: CompiledGuideType[];
setGuides: React.Dispatch<React.SetStateAction<CompiledGuideType[]>>; setGuides: React.Dispatch<React.SetStateAction<CompiledGuideType[]>>;
loading: boolean;
} }
const GuideContext = createContext<GuideContextValue | undefined>(undefined); const GuideContext = createContext<GuideContextValue | undefined>(undefined);
export function GuideProvider({ children }: { children: ReactNode }) { export function GuideProvider({ children }: { children: ReactNode }) {
const [guides, setGuides] = useState<CompiledGuideType[]>([]); const [guides, setGuides] = useState<CompiledGuideType[]>([]);
const [loading, setLoading] = useState(true);
const { db } = useIDB(); const { db } = useIDB();
useEffect(() => { useEffect(() => {
if (!db) return; if (!db) return;
db.getAll("guides").then((fetchedGuides) => { db.getAll("guides").then((fetchedGuides) => {
const sortedGuides = [...fetchedGuides].sort((a, b) => { const sortedGuides = [...fetchedGuides].sort(
return ( (a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
); );
});
setGuides(sortedGuides); setGuides(sortedGuides);
setLoading(false);
}); });
}, [db]); }, [db]);
return ( return (
<GuideContext.Provider value={{ guides, setGuides }}> <GuideContext.Provider value={{ guides, setGuides, loading }}>
{children} {children}
</GuideContext.Provider> </GuideContext.Provider>
); );