solidjs / signals

MIT License
144 stars 4 forks source link

isReactive #8

Open titoBouzout opened 1 year ago

titoBouzout commented 1 year ago

Hello,

I made a reactive renderer using solid reactivity, see https://pota.quack.uy/ , so I have some feedback and suggestions.

There's a need to know if something is a signal/memo

I propose a isReactive which returns true when something is a signal or a memo. The terminology isReactive because is ambiguous enough, it is not like it matters if it is a signal or a memo specifically, and avoids the case for when there's a need to distinguish a signal from a memo, in case isSignal and isMemo becomes a thing for reasons

When isReactive not needed

Contrary to popular belief, an isReactive is not helpful for this case:

function SomeComponent(props) {
  createEffect(() => {
   document.title = isReactive(props.title) ? props.title() : props.title
  })
}

Because it doesn't change anything to know if prop.title is a signal or not. prop.title could be

prop.title = function(){
  return signalSiteName() + ' - ' + signalSiteCategory() + ' - ' + signalPageTitle()
}

Then, what one would do when expecting a maybe signal is the following:

function SomeComponent(props) {
  createEffect(() => {
   document.title = getValue(props.title)
  })
}

export const getValue = value =>
  typeof value === 'function' ? getValue(value()) : value

When is useful to know if thing is isReactive

When processing a tree, you find a component with some callbacks, and there's no way to know what should be tracked and what should be not tracked.

<Show when={true}>
{
  [
    (item) => {
     // this is untracked
    } ,
    signal // this should be tracked
  ]
}
</Show>

One could probably just wrap the signal/memo https://github.com/potaorg/pota/blob/master/src/lib/reactivity/primitives/solid.js#L37-L50, but brings these questions:

  1. is this the performant way to mark the signal/memo? how to do it?
  2. everyone will be doing the same thing if they have a use for knowing if something is a signal/memo?

Functions to effects

If a signal/memo has any meaning to you, then you would do something like the following, because you would want to use the reactive property of the function you are receiving

if (isReactive(unknown)) {
  const placeholder = givemePlaceholder();
  createEffect(() => {
    placeholder.clear();
    placeholder.append(unknown());
  });
} else {
  body.append(unknown());
}
lxsmnsyc commented 1 year ago

This is borderline impossible to implement.

fabiospampinato commented 1 year ago

I think the assumption is that this would be useful for use cases such as @titoBouzout's, where getters are not used so props.foo is just whatever was passed to it, which makes implementing this trivial.

ryansolid commented 8 months ago

I still don't get what it solves. Any function could need to be tracked. Just because a subset is a Signal or Memo doesn't change that.

titoBouzout commented 8 months ago

Say you have

function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count() + 1);

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

<Counter/>

vs

const [Counter, setCount] = createSignal(1);
<Counter/>

Say you have

function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count() + 1);

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}

function insert(thing, where){
    where.append(makeComponent(thing))
}
function makeComponent(thing){
    return untrack(thing)
}

insert(Counter, document.body)

vs

const [Counter, setCount] = createSignal(1);

function insert(thing, where){
    where.append(makeComponent(thing))
}
function makeComponent(thing){
    return untrack(thing)
}

insert(Counter, document.body)

function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count() + 1);

  return (
    <button type="button" onClick={increment}>
      {count()}
    </button>
  );
}
const [Count, setCount] = createSignal(1);

<Show when={{someValue:true}}>
{Counter}
{Count}
</Show>


const writable = createWritableMemo(1);

<Show when={{someValue:true}}>
{writable}
</Show>
// writable === {someValue:true}

I haven't use writable memos much yet, but I can already see that isReadable and isWritable would be also useful. You have a callback that receives a value, you do not want to give that callback the value when it's a writable because it will change its value (case for when same function acts as getter/setter). Sometimes you sort of cant avoid this. like a ref. It provides a nice DX by hiding in plain sight the fact that you can write and read to the thing at the same time.

import {ref} from 'pota'

const element = ref() // readable and writable signal 

effect(()=>{
   if(element()){ ... }
})

<input ref={element}/>

addendum

The following irrelevant(for the case) example always comes up:

  createEffect(() => {
   document.title = isReactive(props.title) ? props.title() : props.title
  })

Some items above I mention thats solved instead as

createEffect(() => {
   document.title = getValue(props.title)
  })

export const getValue = value =>
  typeof value === 'function' ? getValue(value()) : value

Since then, I have found a pattern that meets better my expectations

withValue(props.title, (value) =>  document.title = value)

const withValue = (value, fn) =>
    typeof value  === 'function'
        ? effect(() =>  fn(value()))
        : fn(value)

As you can see on this block of addendum text, knowing if something is a signal or not its completely irrelevant.


All of this only makes sense if you think of solidjs/signals as an API that a consumer could just take and consume, as they wish, with their own requirements and idiosyncrasies.