{"record":{"id":"5a56ca0f020584c7","repo":"CherryHQ/cherry-studio","slug":"useimagepreviewtransform-requires-zoomstep-0","errorCode":null,"errorMessage":"useImagePreviewTransform requires zoomStep > 0","messagePattern":"useImagePreviewTransform requires zoomStep > 0","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/ui/src/components/composites/image-preview/use-image-preview-transform.ts","lineNumber":78,"sourceCode":"  left.flipX === right.flipX &&\n  left.flipY === right.flipY &&\n  left.offsetX === right.offsetX &&\n  left.offsetY === right.offsetY &&\n  left.rotation === right.rotation &&\n  left.zoom === right.zoom\n\nexport function useImagePreviewTransform({\n  initialTransform,\n  maxZoom = 5,\n  minZoom = 1,\n  zoomStep = 0.25\n}: ImagePreviewTransformOptions = {}): ImagePreviewTransformControls {\n  if (minZoom > maxZoom) {\n    throw new Error('useImagePreviewTransform requires minZoom <= maxZoom')\n  }\n\n  if (zoomStep <= 0 || !Number.isFinite(zoomStep)) {\n    throw new Error('useImagePreviewTransform requires zoomStep > 0')\n  }\n\n  const initialValue = React.useMemo(\n    () => normalizeTransform(initialTransform, minZoom, maxZoom),\n    [initialTransform, maxZoom, minZoom]\n  )\n  const [transform, setTransform] = React.useState<ImagePreviewTransform>(initialValue)\n\n  const update = React.useCallback(\n    (nextUpdate: ImagePreviewTransformUpdate) => {\n      setTransform((current) => {\n        const patch = typeof nextUpdate === 'function' ? nextUpdate(current) : nextUpdate\n        const next = normalizeTransform({ ...current, ...patch }, minZoom, maxZoom)\n        return transformsEqual(current, next) ? current : next\n      })\n    },\n    [maxZoom, minZoom]\n  )","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/packages/ui/src/components/composites/image-preview/use-image-preview-transform.ts#L60-L96","documentation":"Thrown by the useImagePreviewTransform hook (in @cherrystudio/ui) during render when the zoomStep option is zero, negative, NaN, or Infinity. zoomStep is the delta added/subtracted on each zoomIn/zoomOut call, so a non-positive or non-finite value would make zoom controls non-functional or produce NaN zoom levels. The guard runs before any state is created, failing fast on invalid configuration.","triggerScenarios":"Calling useImagePreviewTransform({ zoomStep: 0 }), with a negative value (zoomStep: -0.5), or passing a computed value that evaluated to NaN/Infinity (e.g. zoomStep: someRatio where someRatio is NaN). Also triggered if zoomStep is derived from user input or a config file that was mis-parsed into a string then cast with Number().","commonSituations":"Passing zoomStep: 0 thinking it disables zoom; deriving zoomStep from a division that can yield NaN (e.g. 1/count when count is 0); reading a numeric setting from preferences that defaults to 0 before the user configures it; serializing/deserializing config where the number became a string and Number() coerced an empty string to 0.","solutions":["Pass a positive finite zoomStep (the default 0.25 is sane): useImagePreviewTransform({ zoomStep: 0.25 }) or omit the option entirely.","If zoomStep is dynamic, sanitize it before the call: const safeZoomStep = zoomStep > 0 && Number.isFinite(zoomStep) ? zoomStep : 0.25.","Check the call site feeding zoomStep — if it comes from user settings/preferences, ensure the schema clamps the minimum above 0.","Add a unit test asserting the hook accepts its documented range and rejects 0/negative/NaN."],"exampleFix":"// before\nuseImagePreviewTransform({ zoomStep: 0 }) // throws\n\n// after\nuseImagePreviewTransform({ zoomStep: 0.25 })","handlingStrategy":"validation","validationCode":"// Validate before calling the hook (hook-level guards cannot be conditional)\nconst safeZoomStep =\n  typeof zoomStep === 'number' && zoomStep > 0 && Number.isFinite(zoomStep)\n    ? zoomStep\n    : 0.25 // sane default\nuseImagePreviewTransform({ zoomStep: safeZoomStep, minZoom, maxZoom })","typeGuard":"const isValidZoomStep = (v: unknown): v is number =>\n  typeof v === 'number' && v > 0 && Number.isFinite(v)","tryCatchPattern":null,"preventionTips":["Never derive zoomStep from an unchecked division (1/count) without guarding count > 0.","Keep zoomStep as a constant or a preferences value clamped to > 0 by its schema.","Add a unit test asserting the hook rejects 0, negative, NaN, and Infinity."],"tags":["react","validation","image-preview","zoom","typescript"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}