Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useEffect #3

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

useEffect #3

wants to merge 2 commits into from

Conversation

Atrue
Copy link

@Atrue Atrue commented Feb 15, 2022

The main reason I want to have it there is deps function to control the effect explicitly and say when this effect should be called. Another reason is a full typescript coverage, as there are no decorators and no implicit type changes.

Usage:

useEffect can be autotracked or controlled

  1. To have a controlled effect you'll need to pass the deps function (() => [trackedProp1, trackedProp2]). If any of these properties is updated it'll trigger the new effect.
    Note, the property can be updated, but the value can be the same (this.prop = this.prop), it'll trigger the effect anyway. To prevent this you can use the dedupeTracked decorator from https://github.com/tracked-tools/tracked-toolbox instead.

    To have the effect on mount only, pass the function with empty array (() => []).

    Passing non-tracked properties to this array has no effect on updates.

    The deps array is also passed to the effect function as arguments. (useEffect(this, (prop1, prop2) => {...}, () => [prop1, prop2])

  2. If you don't pass the deps argument, the effect will be autotracked, so it will trigger the function whenever any tracked property in the effect is updated.

    Be careful with this effect if you are using more than 1 tracked property inside the effect as it can be complicated to investigate what property triggers the update. Use controlled effect there instead.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { useEffect } from 'ember-tracked-effects-placeholder';
interface Args {
  id: string;
}
export default class MyComponent extends Component<Args> {
  @tracked input = '';
  constructor(owner, args) {
    super(owner, args);
    
    // constructor style
    useEffect(this, () => {
      // some code
    });
  }
  
  /* property assignment style */
  autotrackedEffect = useEffect(this, () => {
    console.log('this is called whenever @id or this.input is updated');
    console.log(this.args.id, this.input); // consume this.args.id and this.input
  }); // no consumer
  mountEffect = useEffect(this, () => {
    console.log('this is called only on mount and is neved updated');
    console.log(this.args.id, this.input); // it doesn't matter what you call here
  }, () => []); // empty array consumer
  idEffect = useEffect(this, () => {
    console.log('this is called whenever @id is updated');
    console.log('it is safe to use any other tracked properties', this.args.id, this.input);
  }, () => [this.args.id]); // consume @id only
}

@Atrue Atrue changed the title Use effect useEffect Feb 15, 2022
@BryanCrotaz
Copy link
Owner

What's the use case for mountEffect? Why not just call it in the constructor?

@Atrue
Copy link
Author

Atrue commented Feb 22, 2022

It's especially will be useful after #4

  1. Working with dom elements. As the effects will be called after the render you can work with dom there. The only thing you'll need there is the ref modifier. Useful for video, canvas elements, and some 3rd party libraries that require some parent dom element.
  2. Making some complex calculations. So the component will have some intermediate state before locking the event loop.
  3. Updating the model/service. If the component updates the model in the constructor that is consumed in some other places it throws an error (here is the example of it call all effects on the actions queue #4).

So it will be an ideal replacement {{did-insert}} modifier on the code level.

@BryanCrotaz
Copy link
Owner

Case number 1 is covered better by modifiers as they directly affect elements.

@Atrue
Copy link
Author

Atrue commented Feb 22, 2022

Right now - yes. You have to use {{did-insert}} and {{did-update}} to make the changes after the render and use them as an analog of effects.
Imagine some component drawing the content from the args on the canvas

draw(text) {
   this.canvas.fillText(text, 10, 50);
}

the effects fully cover the situation:

useEffect(this, () => {
   this.draw(this.args.text || 'Hello world');
},
// () => [] // use this to render the text once
// () => [this.args.text] // use this to render it and update whenever the argument is changed
)

Without the effects, you have to use both modifiers and write all consumers in the template which tears the logic

@BryanCrotaz
Copy link
Owner

BryanCrotaz commented Feb 22, 2022

Actually no. Did-insert and did-update are crutches that you should only use for very very simple cases.

For Dom mods you should use a custom modifer

'Ember g modifier draw-graph'

Put an effect in that modifier to call play on the video for example, then attach the modifier to the video element

@Atrue
Copy link
Author

Atrue commented Feb 22, 2022

A custom modifier doesn't help if you need some control via actions, buttons from this component or any child components, or even from some models if the logic is complex
Anyway, the deps can give you control over everything. So an empty array is just a possible case there

@BryanCrotaz
Copy link
Owner

One thing Ember is very good at is providing one way to do a particular task.

If you need a button to trigger something, do that in the button action. If you need to call a method on an element, do that in a modifier that contains the logic and is passed the tracked data it needs to make the decision.

The one place that you can't call a method from a data change is a service, where there is no render context.

Modifiers and effects do the same thing - they turn a change event into a method call. Modifiers are linked to an element, effects are not.

We shouldn't pollute that by having effects that are linked to elements as you seek to be suggesting.

@Atrue
Copy link
Author

Atrue commented Feb 22, 2022

Modifiers are good for something very simple. But when it becomes a little bit harder, you are stuck, because there is no good way to do it.

If you need to trigger something on the element from the button or the input, of course, you can create a batch of tracked properties and put them into the modifier. But if you need to have them bi-directional (for example currentTime from video element) it's better to set it directly to the element. The line between using components and modifiers is blurred.

My vision is to have the effects to use them both for something very simple and something complex. And my suggestion is not to link them to elements, but give them that lifecycle that doesn't limit you. So the effect is called would mean that one of the dependencies is updated so its related computed properties and its related components are updated, so it would be safe to update new properties/read the dom elements properties/call the element's methods.

I agree that an empty array in dependencies is a rare case, but the deps also cover it without any problems

@BryanCrotaz
Copy link
Owner

I think you're describing what I've done.

The only big difference is that I forgot to put the effect callback into the actions queue. I've just added that.

My original design took a list of observed properties, like yours, but the discussion here was to get rid of that because they were automatically entangled in the function.

What's a use case for using a value in the function but not wanting to entangle it? Not just not needing to, but specifically not wanting to have it rerun the function.

@Atrue
Copy link
Author

Atrue commented Feb 24, 2022

Ok, there are some examples where you can stuck without clarifying the deps.

  1. Using the property for re-assignment in the same event loop triggers an error:
@tracked history;
useEffect(this, () => {
  const newItem = //someCalculations;
  this.history = [this.history, ...newItem];
})

It can be a real problem if you have some this or some tracked counter (this.counter++) inside some service/model/library

  1. Assignment of some already used property in the next event loop triggers infinity re-rendering.
useEffect(this, () => this.service.getData(args))
...
@tracked token;
async getData(...args) {
   const result = await fetch(...args, { token: this.token }); // consuming the token
   this.token = result.headers['token']; // updating the token in the next loop
}
  1. You really want to specify how to trigger the effects
// type and args are tracked
makeSomeInstance(type, args) {
  return type === 'a' ? new SomeA(args) : new SomeB(args);
}

// you want to run this only on type change. This method calls hard computations
useEffect(this, () => {
  this.instance = makeSomeInstance(type, args);
});
// call this if any of the args is changed. This method is light
useEffect(this, () => {
  this.instance.update(args);
});

Also, another benefit is typescript coverage, as this code

@effect
something = () => {}

can be surprise as something is not a function, but an object

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.

None yet

2 participants