0
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
1400
1500
1600
1700
1800
1900
2000
2100
2200
2300
2400
2500
2600
2700
2800
2900
3000
3100
3200
3300
3400
3500
3600
3700
3800
3900
4000
4100
4200
4300
4400
4500
4600
4700
4800
4900
5000
5100
5200
5300
5400
5500
5600
5700
5800
5900
6000
6100
6200
6300
6400
6500
6600
6700
6800
6900
7000
7100
7200
7300
7400
7500
7600
7700
7800
7900
8000
8100
8200
8300
8400
8500
8600
8700
8800
8900
9000
9100
9200
9300
9400
9500
9600
9700
9800
9900
10000
10100
10200
10300
10400
10500
10600
10700
10800
10900
11000
11100
11200
11300
11400
11500
11600
11700
11800
11900
12000
12100
12200
12300
12400
12500
12600
12700
12800
12900
13000
13100
13200
13300
13400
13500
13600
13700
13800
13900
14000
14100
14200
14300
14400
14500
14600
14700
14800
14900
15000
15100
15200
15300
15400
15500
15600
15700
15800
15900
16000
16100
16200
16300
16400
16500
16600
16700
16800
16900
17000
17100
17200
17300
17400
17500
17600
17700
17800
17900
18000
18100
18200
18300
18400
18500
18600
18700
18800
18900
19000
19100
19200
19300
19400
19500
19600
19700
19800
19900

Stop using ChatGPT to write Framer code components.

Stop using ChatGPT to write Framer code components.

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.

Work with me → hello@framer-expert.com


New to Framer? Get 3 months free on the Pro plan when you sign up yearly with my referral link: framer.link/framerexpert or clickthe text here.

Stop using ChatGPT to write Framer code components.

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])

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])

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

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)
}, [])

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)
}, [])

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)

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 }
})

The component receives an object with a .src property, not a string.

Broken:

<img src={props.image} />

That's how it works in normal React. In Framer, you need:

Fixed:

<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.

Work with me → hello@framer-expert.com


New to Framer? Get 3 months free on the Pro plan when you sign up yearly with my referral link: framer.link/framerexpert or clickthe text here.

subscribe for more

Frequently Asked Questions

Most Framer projects move from discovery to launch within 2 to 4 weeks. This includes high-fidelity design, interactive prototyping, and final development. Timelines vary based on the complexity of custom animations and CMS requirements.

Yes. While Framer is a powerful no-code tool, I often implement custom React components and overrides to achieve specific functionality or complex data integrations that aren't available out of the box.

Once the project is finalized and approved, I transfer the Framer project directly to your account. I provide a recorded walkthrough of the CMS and site settings so your team can manage content with zero friction.

Absolutely. I offer monthly retainers for high-growth brands that need continuous updates, new landing pages, and ongoing performance optimisation. Priority availability, ongoing Framer support, and a dedicated expert in your corner.

Get in Touch

© 2026 Portfolio

Get in Touch

© 2026 Portfolio

0
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
1400
1500
1600
1700
1800
1900
2000
2100
2200
2300
2400
2500
2600
2700
2800
2900
3000
3100
3200
3300
3400
3500
3600
3700
3800
3900
4000
4100
4200
4300
4400
4500
4600
4700
4800
4900
5000
5100
5200
5300
5400
5500
5600
5700
5800
5900
6000
6100
6200
6300
6400
6500
6600
6700
6800
6900
7000
7100
7200
7300
7400
7500
7600
7700
7800
7900
8000
8100
8200
8300
8400
8500
8600
8700
8800
8900
9000
9100
9200
9300
9400
9500
9600
9700
9800
9900
10000
10100
10200
10300
10400
10500
10600
10700
10800
10900
11000
11100
11200
11300
11400
11500
11600
11700
11800
11900
12000
12100
12200
12300
12400
12500
12600
12700
12800
12900
13000
13100
13200
13300
13400
13500
13600
13700
13800
13900
14000
14100
14200
14300
14400
14500
14600
14700
14800
14900
15000
15100
15200
15300
15400
15500
15600
15700
15800
15900
16000
16100
16200
16300
16400
16500
16600
16700
16800
16900
17000
17100
17200
17300
17400
17500
17600
17700
17800
17900
18000
18100
18200
18300
18400
18500
18600
18700
18800
18900
19000
19100
19200
19300
19400
19500
19600
19700
19800
19900
0
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
1400
1500
1600
1700
1800
1900
2000
2100
2200
2300
2400
2500
2600
2700
2800
2900
3000
3100
3200
3300
3400
3500
3600
3700
3800
3900
4000
4100
4200
4300
4400
4500
4600
4700
4800
4900
5000
5100
5200
5300
5400
5500
5600
5700
5800
5900
6000
6100
6200
6300
6400
6500
6600
6700
6800
6900
7000
7100
7200
7300
7400
7500
7600
7700
7800
7900
8000
8100
8200
8300
8400
8500
8600
8700
8800
8900
9000
9100
9200
9300
9400
9500
9600
9700
9800
9900
10000
10100
10200
10300
10400
10500
10600
10700
10800
10900
11000
11100
11200
11300
11400
11500
11600
11700
11800
11900
12000
12100
12200
12300
12400
12500
12600
12700
12800
12900
13000
13100
13200
13300
13400
13500
13600
13700
13800
13900
14000
14100
14200
14300
14400
14500
14600
14700
14800
14900
15000
15100
15200
15300
15400
15500
15600
15700
15800
15900
16000
16100
16200
16300
16400
16500
16600
16700
16800
16900
17000
17100
17200
17300
17400
17500
17600
17700
17800
17900
18000
18100
18200
18300
18400
18500
18600
18700
18800
18900
19000
19100
19200
19300
19400
19500
19600
19700
19800
19900