Skip to content

Conversation

aleclarson
Copy link

@aleclarson aleclarson commented Jul 11, 2025

Background

I briefly discussed this idea with @rschristian, who said I should try to implement it.

Link to discussion: https://preact.slack.com/archives/C3NMBSJKH/p1752172452437339

Goal

Avoid needing to declare "computed expressions" with useComputed, enabling you to write "inline computeds" where they're needed. This is intended for computeds that are only used by one JSX attribute or JSX child.

How It Works

It's as easy as wrapping a "computed callback" with jsxBind(…), a function exported by the @preact/signals package.

Key Features

  • Host elements only: Only host elements support this feature
  • No rerender updates: Just like useComputed, the callback does not update on rerender
  • Dual support: This implementation supports use in JSX attributes and JSX children

Current Limitations

Currently, there is no warning when trying to use jsxBind with a composite element, as this would require iterating the props object. Maybe someone has an idea about how to efficiently check for such misuse.

Naming

Originally, I named it bind(), but decided jsxBind() makes its purpose a bit clearer. Ultimately, the name is up to you guys. :)

Copy link

netlify bot commented Jul 11, 2025

Deploy Preview for preact-signals-demo ready!

Name Link
🔨 Latest commit 00ad633
🔍 Latest deploy log https://app.netlify.com/projects/preact-signals-demo/deploys/688337adc9eabc0008b01dd1
😎 Deploy Preview https://deploy-preview-724--preact-signals-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link

changeset-bot bot commented Jul 12, 2025

🦋 Changeset detected

Latest commit: 00ad633

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@preact/signals Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@JoviDeCroock
Copy link
Member

This is missing a changeset btw

if (
value &&
typeof value === "object" &&
Object.getPrototypeOf(value) === jsxBind
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just using .prototype will be lower bytes and more performant I think, that being said not too fuzzed can look at this later. Will do one final pass over the weekend and get this merged

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that wouldn't be equivalent. it's either getPrototypeOf or .__proto__

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using constructor would be cheaper and smaller here.

if (typeof value === 'object' && value.constructor === jsxBind) {
  // ...
}

// ...

export function jsxBind<T>(cb: () => T): T {
	return { value: cb, constructor: jsxBind } as any;
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@developit The prototype needs to inherit from Signal (see line below jsxBind declaration) for the SignalValue component to be used (jsxBind(…) expression as JSX child).

personally, I don't see an issue with using __proto__ as I did originally, but I digress

@developit
Copy link
Member

I'd like to think about this for a few more days before we commit to landing it. I don't think the name is quite right - this isn't binding, it's deriving or computing.

Also, I keep thinking it would be better if we just built this in so it didn't require an import, simply by checking for a special character at the end of the prop name in our existing loop:

<div class$={() => `foo ${sig.value}`} />

Alternatively, what if we just checked the type of the prop value:

<div class={() => `foo ${sig.value}`} />

The only prop values that currently commonly accept functions in Preact are click handlers. We could have the logic be this:

	let props = vnode.props;
	for (let i in props) {
		if (i === "children") continue;

		let value = props[i];
		if (typeof value === 'function' && i[0] !== 'o' && i[1] !== 'n') {
			value = oldSignalProps?.[i] || computed(value);
		}

@developit
Copy link
Member

Also I believe there is a slight issue with the implementation here (and my suggestions above). In the following example, re-rendering <Demo /> with a different class prop will not re-trigger the computed. Even if the computed is re-triggered (by setting active.value), the callback function is never updated after the first render, so it is closed over the initial props value rather than the current.

let counter = 0;
let someSignal = signal('hello');

function Demo(props) {
  const active = useSignal(false);

  return <div class={jsxBind(() => `${props.class}${active.value ? ' active' : ''}`)} />
}

@aleclarson
Copy link
Author

@developit As I said in the linked Slack thread, I would also prefer not needing the jsxBind(…) wrapper call, so happy to see you say that :)

Also I believe there is a slight issue with the implementation here

That's intentional. It mimics the behavior of useComputed

@developit
Copy link
Member

heh yeah I was just going to point out that this is the same as computed/useComputed.

BTW - does this come up because you're trying to write components that never re-render? If so, DM me. I ... have been working on a thing. haha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants