vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.67k stars 33.66k forks source link

Feature Request: React like hook shouldComponentUpdate for Vue lifecycle #4255

Closed mosleyjr closed 7 years ago

mosleyjr commented 7 years ago

I think it would be useful to have a hook like React's shouldComponentUpdate in the Vue lifecycle. The idea behind this hook would be to dynamically prevent the DOM from refreshing the component with data change.

For my use case I am attempting to build a component that uses Contenteditable as a text editor interface which updates on keyup. The DOM refresh in the lifecycle results in the caret / cursor resetting with the refreshed component. Having the option to define when the component should update would allow use of Contenteditable while continuing to update the state (vuex) and keep any of the component {{references}} in the template reactive.

From my understanding of React, the ability to make re-render conditional would also be a big performance booster for components with lots of DOM refreshes like my example above. I'm a big fan of Vue and I think adding this lifecycle hook would open up a lot of doors.

yyx990803 commented 7 years ago
  1. I'm not sure if shouldComponentUpdate is the right solution for your problem, maybe you can provide a jsfiddle to better illustrate what you are trying to achieve.
  2. Vue doesn't have shouldComponentUpdate exactly because Vue doesn't need it. Vue's reactivity system ensures only the right components get re-rendered when state changes. Think of it as React with shouldComponentUpdate already implemented for you.
mosleyjr commented 7 years ago

@yyx990803 thanks for the reply,

  1. I have put together a JSFiddle. It contains two components. The first shows that the contenteditable updating with no caret jump. This is because there are no HTML changes to the state, and so Vue does not re-render the component. The second contenteditable component contains HTML, and when editing displays the behavior I described above. The DOM refreshes and the caret jumps to the beginning.
  2. You are right that Vue is correctly determining which components to re-render. It is re-rendering the contenteditable div when the state changes. However in my use case I need an option to stop that automated behavior. I would like the state to continue updating, but for the component to wait to re-render until I want it to refresh. This is where a shouldComponentUpdate hook would be useful.

I like that Vue automates the rendering of the correct components, but I believe it makes sense to have an option to circumvent that behavior if desired. I believe exposing such an option/hook would open Vue up to the use of contenteditable, provide potential for performance boosts, and solve my headache :)

HerringtonDarkholme commented 7 years ago

Hi, v-once might be helpful here. https://jsfiddle.net/he1ug0fo/

If you still need more fine grained control over rendition, one more computed getter might be helpful.(For example, getContent only updates when the cursor is not in the contenteditable).

For general cases, computed properties that gets selectively updated might be enough for implementing shouldComponentUpdate. The only case I can conceive needs shouldComponentUpdate is you have so many interlaced state changes that manually implementing computed is cumbersome. But I wonder if such cases are common.

mosleyjr commented 7 years ago

@HerringtonDarkholme thanks for the direction! you are correct, v-once does indeed resolve this issue for my use case.

The original JSFiddle you updated did not work correctly because it was using older versions of Vue and Vuex. I have created another JSFiddle with Vue and Vuex 2.0 which demonstrates the updated functionality.

Your suggestion of a computed property makes sense to me if I intended to display the content separately through the template, however that part of the JSFiddle was just for example purposes. I still believe a shouldComponentUpdate option would be necessary for a contenteditable component that also acts as the display. Basically for anything that acts as a "text editor".

Your v-once solution works as long as I am ok with not re-rendering the component, however I could see use cases where one might wish to re-render a component @blur or @keydown. I am currently working on adding a watcher to trigger component re-render @blur, but have not had success.

I would still be interested in seeing a shouldComponentUpdate hook, or on a more macro scale, an option to end the lifecycle at any point without causing errors.

yyx990803 commented 7 years ago

After looking at your use case, I still don't think shouldComponentUpdate is the correct answer to it. What you want is essentially a way to let you explicit make the view and the state out of sync, (which is imo not a correct approach in the first place) and shouldComponentUpdate is primarily offered for performance optimization.

The fully framework-compliant way to deal with it (and is also what I would suggest even in React) is to have a separate piece of state that represents the input content, which can then be out of sync with the model state and can be updated only when you want to.

CodinCat commented 7 years ago

I just found a case which shouldComponentUpdate may make sense. https://jsfiddle.net/hnc1u3bn/1/

You can see the dev console. Every time you click on a tab, all other tabs will re-render because they depend on selectedIndex (I guess). They think they need to re-render, but in fact they don't.

So in this case, even all the props are the same, the components will still update itself. If we want to optimize the performance, shouldComponentUpdate or a react-like PureComponent may be helpful.

yyx990803 commented 7 years ago

@CodinCat in this case the tabs are forced to update because they have slot children. Technically, this means if you implement a shouldComponentUpdate for your tabs that solely depends on the selected prop, it will be incorrect because the slot children may have changed and are not accounted for. It can lead to subtle bugs when the slot content of your tab component changes.

In React, if you use a pure component for this purpose, it will also always re-render because the children prop always gets new ReactElement references, so a shallow compare will consider them different.

Of course, you can try to diff the children nodes in a custom shouldComponentUpdate, but it's unlikely to be correct for all edge cases - plus at that point it's not much different from just re-rendering the containing component.

mijzcx commented 6 years ago

+1 shouldComponentUpdate might be added only to Render Functions & JSX section.

jeff-hykin commented 4 years ago

have a separate piece of state

@yyx990803 How would you recommend passing 60-changes-per-second data from a parent to a DOM heavy child that can't render at 60fps (and only needs to render at 1fps)? I'm currently wrapping the data-from-parent in a function and then having the parent change/refresh that function when the child should be updated, but it is less than ideal and circumvents the normal flow of Vue. I don't see how a separate piece of state would handle this situation.

Scenario

I have mousePosition and other event drive data in <parent> that is needed in many parts of the app, some need those changes in real time, and others, like a graph I have, cannot keep up. The graph uses SVG's to plot a smooth animated curve between many data points including the rapidly updating mousePosition. The issue is that the graph has to do some decently complex math to find and animate a smooth curve through all of the points. The graph doesn't need to be real time, however it does need to continually update using the data from the parent.

edit: improve clarity/wording

glassdimly commented 4 years ago

I sometimes come across threads like this in the open-source world, where a person requests a feature like computed re-render, and others jump in to tell them they're doing it wrong.

I always feel like this misses the point: As developers, we need power tools. We need the power to tell the system what to do.

jeff-hykin commented 4 years ago

While I do agree (in general) with you @glassdimly, and I love the ruby programming language for following the assume-the-dev-knows-best paradigm, Vue.js does have a responsibility/goal to be minimal.

I don't want a fix like goto just because it would solve my current problem. I'd rather the Vue.js team analyze the entire range of limitations and integrate a tool that solves all of them, or add a tool that eliminates the situation altogether, instead of the quick fix of tacking on a shouldComponentUpate.

For example, maybe it would be better for there to be a v-should-update attribute that replaces v-once which lets the parent control the updates, or maybe there should be a <component #data="data" /> where the # indicates that changes in the data attribute won't cause the component to re-render. I don't have all of the possible situations available so I can't say I have the ideal solution.

However, I still would like to know how @yyx990803 or anyone else would recommend passing rapidly-changing data to a component that needs to render less-rapidly.

sirlancelot commented 4 years ago

I think your second paragraph makes a very good point @jeff-hykin which no one should overlook. I learned Vue first before learning React, and when I discovered that React didn't actually react to my data changes automatically (à la Vue), I was flabbergasted. How could something so "revolutionary" require such a primitive hack.

shouldComponentUpdate is an anti-pattern in a true reactive framework.

As for solutions to the problems outlined in this nearly 4-year-old topic, you are basically looking at a throttle or debounce. This is not strictly a Vue problem/solution. It is, however, a problem which can be solved in a userland component:

<template>
  <div>Slow data: {{ throttledData }}</div>
</template>

<script>
import throttle from "mout/function/throttle"

export default {
  name: "ThrottledDataView",
  props: {
    data: { type: Object, required: true }
  },
  data: () => ({
    throttledData: null
  }),
  created() {
    const vm = this
    const throttledUpdate = throttle(
      (newData) => (vm.throttledData = newData),
      1000
    )

    vm.$watch(() => vm.data, throttledUpdate, { immediate: true })
    vm.$on("hook:beforeDestroy", throttledUpdate.cancel)
  }
}
</script>

Vue's $watch function on a prop works great for when you need to control updates like this. The ThrottledDataView component will always receive the most up-to-date value of data, but the watcher only updates the data model every 1000ms using throttle.

glassdimly commented 4 years ago

I agree with you @jeff-hykin. One doesn't want to implement a goto solution. But I think @sirlancelot's post is suggesting a workaround for a specific problem that shouldComponentUpdate solves.

In React, whenever state changes, it triggers an update--similar to data. And the same happens with a props update. So in React, the assumption is the same as in Vue (as I understand it from my novice perspective): that when state or props update, the component re-renders. The question is whether or not your component can bail out of an update. shouldComponentUpdate allows you to bail out based on the nextState or nextProps. Here're the docs.

Now, throttling component updates is great (mout/function/throttle), and so is rendering once and only once (v-once), but what if I want to to render twice, for example? These are the sorts of totally unforeseeable (yet daily) arbitrary needs that computed bailouts of re-render solve.

shouldComponentUpdate is a power tool that allows fine-grained control over component performance by allowing the component to bail out of re-render based on arbitrary, computed criterion. And while I certainly have a lot to learn about Vue and could be wrong, it seems a problem to me that it lacks this kind of power tool.

sirlancelot commented 4 years ago

Here's something interesting I pulled verbatim from that section of the docs (emphasis mine):

This method only exists as a performance optimization. Do not rely on it to “prevent” a rendering, as this can lead to bugs.

React shouldn't be your metric for what is "right". It is full of workarounds such as shouldComponentUpdate which obscure the developer's understanding of their own data model. When you have a clear understanding of your data model and when you want things to change, you will never need to reach for this anti-pattern. Vue doesn't have this "feature" because it doesn't need it.

As I've demonstrated in my previous comment, there is a clear, and elegant solution with a clear data model and lifecycle. I don't feel the core developers need to explain why they're not creating more footguns. Even though JavaScript the language provides many of them, I find it refreshing that Vue.js helps me enforce clarity in my work.

I'm out. Have a great week, and stay safe!

glassdimly commented 4 years ago

@sirlancelot: Respectfully, it's important to have experience with the frameworks you critique.

jeff-hykin commented 4 years ago

Thanks @sirlancelot , I've actually used debouncing before but did not think of it in this situation. I'll see if I can make a version of that solution that works, but my concern is that assigning to the this.throttledData won't help because the data is an array. The value doesn't ever change, it just gets mutated by adding more data. The simple workaround is to deep copy, however that is also an issue because it results in O(n) operation and the array is very large (hour-long video data). There would need to me a more complex solution such as diffing all the values that had been added in the time since the last throttled change, which gets complicated quickly. Wrapping the data inside function that can be called when needed is a, still non-ideal, but less complex solution.

jeff-hykin commented 4 years ago

In React, whenever state changes, it triggers an update--similar to data. And the same happens with a props update.

@glassdimly My first web framework was React, I spent 1.5 years using it in work and school before I found Vue. I'm all too familiar with setState and props. I actually had a paragraph about React in my earlier response but removed it before commenting to prevent head-butting.

Using React at work, we decided to use shouldComponentUpdate on a component near the beginning of a project since it was causing quite a slowdown even when the data appeared to not be changing. Later, input validation removed invalid text during render (it was in render because things other than user-events could change text and there was no way to $watch them), but changing state during render causes an infinite loop, so, to prevent the infinite loop we used shouldComponentUpdate. By the end of the project (1 year later) we had shouldComponetUpdate on 50% of all class based components. It was so difficult to debug why the component was/wasn't rendering that there were cases where after changing state we would immediately call a force render just to be sure. We also had places where we added randomly generated numbers to the state of some components so that the diffing being done in shouldComponentUpdate would detect a change and re-render. It was a colossal mess and is one of the worst codebases I have ever contributed to.

I agree with you @jeff-hykin. One doesn't want to implement a goto solution. But I think @sirlancelot's post is suggesting a workaround for a specific problem that shouldComponentUpdate solves.

I agree the debouncing seems very much like a large workaround. However, I actually like the option of goto, it is a solution to many specific problems. shouldComponetUpdate is very similar in nature to goto: it breaks what is otherwise a predictable flow of code to directly achieve a specific behavior. Once the general rules have been broken: the only means of predicting the output is to know every intricate detail of the system, which is costly and impractical at even medium scales.

I'm a Texan, I believe in the right to shoot myself in the foot if I so desire, and I have successfully used goto without shooting myself in the foot. But, if there is a frequent reason (as indicated by a GitHub issue with many upvotes) where people truly NEED to break the rules, then the rules themselves are likely broken, and that should be discussed/addressed before handing someone a gun to shoot something very near to their foot.

I want fine grain control as well, but I want to easily control render times AND do it without breaking any principles.

@sirlancelot: Respectfully, it's important to have experience with the frameworks you critique.

I do agree that first hand experience is needed before critique. (And I think you handled that point professionally @glassdimly.) Although I do agree with @sirlancelot that React is littered with workarounds (including shouldComponentUpdate) that point is largely irrelevant here as there are a number of things, such as functional components with JSX, that I believe Vue could learn from React.