preactjs / signals

Manage state with style in every framework
https://preactjs.com/blog/introducing-signals/
MIT License
3.71k stars 91 forks source link

useSignalEffect not activated, effect triggers fine #369

Closed isolin closed 1 year ago

isolin commented 1 year ago

This is probably some issue on my side, but I would be very glad if anyone could help me fixing it.

To Reproduce

I have a global state

const state = {
  data: signal([0])
}

export default state;

When I reassign the data state.data.value = [0, 1, 2] the event is propagated only partially. If I have a functional component, effect works.

export default function SceneTable() {
    let l = 0;
    effect(() => console.log(appstate.data.value.length));
    return <p>{appstate.data.value.length}+</p>;
}

But when I use a class Component instead, then useSignalEffect never triggers. Why so? Did I forget about something?

//a props interface IProps

export default class SceneTable extends Component<IProps> {
  useSignalEffect(){
    console.log(appstate.data.value.length);
    //never hits here
  }

  //some other methods
}

Maybe an important info: the class component is created as tableRef = createRef<SceneTable>() in a parent class component, while the my test with the functional component was stand-alone.

rschristian commented 1 year ago

Are you using React or Preact?

You should be using the hook versions (useX) in function components, and the non-hooks in class components. React limits hooks to function components, you cannot use them in class components. Preact does allow you to use hooks in class components, but it's quite unidiomatic and I'm not sure if support is guaranteed long-term.

isolin commented 1 year ago

I am of course using preact. I just tried to follow the docs where useSignalEffect is mentioned. I haven't been able to make it trigger, so I have rebuilt my components to functional. Nevertheless, I consider class components more verbose and easier to understand as the naming like componentDidMount and componentWillUnmount is much better for understanding and maintenance than having an useEffect with the destructor being returned.

rschristian commented 1 year ago

I am of course using preact.

Both React and Preact are supported, so that bit was unclear.

Nevertheless, I consider class components more verbose and easier to understand as the naming like componentDidMount and componentWillUnmount is much better for understanding and maintenance than having an useEffect with the destructor being returned.

Use what you prefer, that's not an issue. Just you shouldn't really be using hooks in class components.

The issue is that you're misusing useSignalEffect, if your snippet isn't a typo. It's not a lifecycle method, but a function that takes a callback, just like useEffect and effect. Try this:

useSignalEffect(() => {
    console.log(appstate.data.value.length);
});
isolin commented 1 year ago

Ah that was not clear to me. I thought it is a lifecycle method! I wrongly interpreted the following docs line

When responding to signal changes within a component, use the hook variant: useSignalEffect(fn).

as being a lifecycle method in class components.

So just out of curiosity - is there any concept that I could use within a class component to handle updates in signal values?

rschristian commented 1 year ago

as being a lifecycle method in class components.

Hooks are in contrast to life cycle methods. Very different things, and APIs.

Again, Preact sorta blurs the lines by technically allowing hooks in class components, but generally should be avoided as it's quite unidiomatic.

So just out of curiosity - is there any concept that I could use within a class component to handle updates in signal values?

I'm not really sure what the idiomatic solution is here, but this works I suppose?

class CheckBox extends Component {
    #checked = signal(this.props.checked);
    #_void = effect(() => console.log(this.#checked.value));

    toggle = () => {
        this.#checked.value = !this.#checked.value;
    }

    render({ label }) {
        return (
            <label>
                <input type="checkbox" checked={this.#checked} onInput={this.toggle} />
                {label}
            </label>
        );
    }
}

Keep in mind, you only need to do this if you're creating the signal that needs to be watched inside the component. If your state can be external (like it doesn't need to be initialized by a prop), it can be a bit simpler:

const checked = signal(false);
effect(() => console.log(checked.value));

class CheckBox extends Component {
    toggle = () => {
        checked.value = !checked.value
    }

    render({ label }) {
        return (
            <label>
                <input type="checkbox" checked={checked} onInput={this.toggle} />
                {label}
            </label>
        );
    }
}
isolin commented 1 year ago

I rather thought of something like:

const data = signal([1,2,3]);

class CheckBox extends Component {
    process = effect(() => {
      //some heavy processing specific for this component
      this,state.data = this.process(data.value);
    });
}

I need to process data before rendering them and a computed signal makes not much sense as

  1. the data view is specific for the component instance
  2. it does not explicitly appear in the render() function but instead it is fed as scene data to a THREE.js scene

but that is my specific use case. The above concept with process = effect(() => { /*...-* }) seems to work in class components and I anyway switched to functional in the meanwhile :)