Reference
Gotchas
Things that cost real time to work out.
Things that cost real time to figure out.
Theme structure
Section filenames are global. Flattening means two files with the same name in different folders collide. The build warns rather than silently overwriting.
Section groups only accept header, footer or aside as type. Anything
else fails to upload with no useful error, and the section silently renders
nothing.
Removing a preset removes its default blocks. If a section's blocks are supposed to exist by default, they belong in the group or template JSON.
Schemas
{% schema %} is pure JSON. No Liquid tags inside, and unknown keys are
rejected — even a "comment" field.
Range settings need at least 3 selectable values. (max - min) / step + 1
must be ≥ 3.
1{ "type": "range", "id": "span", "min": 1, "max": 2, "step": 1 } // fails to upload2{ "type": "range", "id": "span", "min": 1, "max": 3, "step": 1 } // finevisible_if doesn't work on resource pickers — collection, product,
page, blog, article, metaobject and their _list variants. Every other
type is fine, and referencing a picker from another setting's visible_if
works.
Shopify has no file-upload setting type. image_picker and video filter by
media type. For fonts or anything else, a text field is your only option.
React
The theme editor replaces section markup on every edit. React remounts and
loses state. Persist anything that should survive with
useSectionState.
useStore selectors must be Object.is-stable or you get an infinite
render loop.
1useStore(cartStore, (s) => s.cart.item_count) // fine — a primitive2useStore(cartStore, (s) => s.cart) // fine — the same reference3useStore(cartStore, (s) => s.cart.items ?? []) // loops — a new [] every callDepend on the whole useSelectedBlock() object, not .id. Re-selecting the
same block produces the same id, and only seq tells you it happened again.
1const selection = useSelectedBlock()2useEffect(() => { … }, [selection.id]) // dead after the first click3useEffect(() => { … }, [selection]) // fires every time45useRevealOnSelect(blocks, (i) => setActive(i)) // better — handles it for youImport Lucide icons by name, never the namespace.
1import { ShoppingBag } from 'lucide-react' // yes2import * as Icons from 'lucide-react' // no — pulls in every iconThe API
Deploying a Shopify app is not installing it. The App Proxy only exists on stores where the app is installed.
prefix and subpath are fixed at install time. Changing them later needs
an uninstall and reinstall, not just a redeploy.
Adding an operation doesn't reach production until you rebuild. Operations are compiled into the deploy folder.
npm run api:buildcd deploy/netlify && npx netlify deploy --prodMoney maths goes on the integer, never the formatted string. Re-formatting cents in JavaScript breaks currencies with non-decimal subunits.
1const total = lines.reduce((sum, line) => sum + line.final_line_price, 0) // integer2<span>{cart.total_price_formatted}</span> // render the string