A founder DMed me last week. Their pricing page had been broken for three days. The hero animation just... wouldn't render. Dev tools showed nothing. They'd built the component with Claude. It worked locally. It worked in the Framer preview. It crashed the moment they hit Publish.
I fixed it in eleven minutes. Two lines.
This isn't a one-off. Three out of four code component bugs I see come from founders pasting AI-generated React into Framer and assuming it'll work the same way it would in a standard React app. It won't. Framer's runtime has rules ChatGPT doesn't know about, and the failures are predictable enough that I can guess the bug before opening the project.
Here are the four that come up most.
1. useEffect returning a value instead of nothing
This is the most common one, by a wide margin. ChatGPT writes single-line arrow functions because they're concise:
Broken:
useEffect(() => counterRef.current++, [someState])
useEffect(() => counterRef.current++, [someState])
That returns a number. React's useEffect expects to receive nothing back, or a cleanup function. A number breaks it. Framer crashes the component and hides it from the canvas, which is why the dev tools show nothing. There's nothing to log because nothing rendered.
Fixed:
useEffect(() => { counterRef.current++ }, [someState])useEffect(() => { counterRef.current++ }, [someState])Curly braces. Now the function returns undefined and React is happy. The component renders.
2. Accessing window, document, or navigator outside useEffect
Framer server-renders your pages. That's part of why it's fast, and part of why it ranks well in search. The downside is that browser-only APIs don't exist on the server. If your component reads window.innerWidth at the top level, the server tries to render it, finds no window, and the component disappears.
ChatGPT doesn't know your code is being SSR'd. It happily writes:
Broken:
const width = window.innerWidth
const width = window.innerWidth
Right at the top of the component, like you would in a Vite or Create React App project. In Framer, that has to live inside useEffect:
Fixed:
const [width, setWidth] = useState(0)
useEffect(() => {
setWidth(window.innerWidth)
}, [])const [width, setWidth] = useState(0)
useEffect(() => {
setWidth(window.innerWidth)
}, [])Same pattern for document.querySelector, navigator.userAgent, anything that touches the DOM or the browser environment.
3. Stale closures inside event handlers
This one breaks subtly. The component renders, the user interacts, and the state doesn't update the way you'd expect. Classic case: a counter that increments on click but only ever shows 1.
Broken:
const [count, setCount] = useState(0)
useEffect(() => {
const handler = () => setCount(count + 1)
window.addEventListener('click', handler)
}, [])const [count, setCount] = useState(0)
useEffect(() => {
const handler = () => setCount(count + 1)
window.addEventListener('click', handler)
}, [])The handler captures count as 0 because that's what it was when useEffect ran. Every click sets it to 0+1, not the current value plus one.
ChatGPT writes this pattern constantly when generating Framer interaction logic. The fix is the functional update form of setState:
Fixed:
const handler = () => setCount(c => c + 1)
const handler = () => setCount(c => c + 1)
Now you're not reading the stale value. You're telling React to use whatever the current value is.
4. ControlType.Image expecting a file, not a string
This one's Framer-specific and ChatGPT gets it wrong every time. When you expose an image prop on a component using addPropertyControls, the control type matters:
addPropertyControls(Component, {
image: { type: ControlType.Image }
})addPropertyControls(Component, {
image: { type: ControlType.Image }
})The component receives an object with a .src property, not a string.
Broken:
<img src={props.image} /><img src={props.image} />That's how it works in normal React. In Framer, you need:
Fixed:
<img src={props.image.src} /><img src={props.image.src} />The component will render blank otherwise, which looks identical to a broken image path.
If you want a string URL instead, use ControlType.String and let the user paste a URL. But for actual asset uploads, it's always .src.
Why this matters for what I do
Most of my retainer work isn't designing components from scratch. It's untangling code components that worked once, broke after a Framer update, or were written by a founder using AI and never tested at SSR. The bugs are repeatable. The fixes take minutes if you've seen them before, hours if you haven't.
If your site has a component that's flickering, blanking on first load, working in preview but not in production, or showing "We detected a problem in one of your code components" with no useful error, it's probably one of the four above.
I can fix it. Or I can write you a component that doesn't have these problems to begin with.