unabridged / motion

Reactive frontend UI components for Rails in pure Ruby
https://github.com/unabridged/motion
MIT License
697 stars 19 forks source link

[Question] What's the purpose of data-motion-key and -state? #33

Closed devdicated closed 4 years ago

devdicated commented 4 years ago

I love the simplicity of this Gem, but I was surprised by the amount of data that is generated. What's the exact purpose of the data-motion-key and data-motion-state attributes? I didn't find anything in the readme nor was I able to find a clear answer in the source code.

RolandStuder commented 4 years ago

I was also surprised by this. I had a fairly big component, that has state of five models, I am sure this could be refactored, but using motion my pageload went from 29kb to 290kb.

I think what motion is doing, it is dumping the whole component state into that data-motion-state attribute. Motion has to keep the state somewhere, and the current solution seems to be that. See here https://github.com/unabridged/motion/blob/master/lib/motion/serializer.rb

alecdotninja commented 4 years ago

data-motion-state

data-motion-state contains a Marshal'd copy of the component that has been signed and encrypted (for security reasons) with (by default) a key derived from the application secret. It is used by the server when mounting the component (it is continuously updated by the server as the component's state changes in case the connection is lost, but only actually sent/used when the connection is first open).

Since Marshal is fast and can serialize almost any Ruby object out of the box, I think it is the right choice for something like Motion. One downside though is that since it is a binary format, it can be hard to get visibility into what exactly is being serialized. I don't yet have a good answer for this.

A general strategy for reducing the size of your state is to look through all of your instance variables (I like to throw a binding.pry or byebug into a motion and run instance_variables) and make sure they are all things that you want to be included, then repeat this process for the typical values of each instance variable until you hit "primitive" values like strings, numbers, or an object from a library that you want to treat as a black box. At each stage, you can look at the Marshal.dump(value).size of a value to guide your search.

For completeness, I want to mention that you can also customize how your component is Marshal'd, but I suspect this is inappropriate for most application-level code (it does make sense in some cases though).


data-motion-key

data-motion-key contains a hash that is also derived from the Marshal'd representation of the component. Functionally, it is a stable identifier for the state of the component. It is used by the client during reconciliation to preserve the "inner state" of a nested component when an outer component re-renders.

In my mental model, data-motion-key is how the parent component sees a child. When a child component's state changes in response to a motion or broadcast (from the "inside" in my mental model), data-motion-key does not change. It remains the key for the child as the parent rendered it. When the parent renders, if it does not attempt to influence the state of the child (from the "outside" in my mental model), the key will match, and the child will not be replaced. On the other hand, if the parent does attempt to influence the state of the child, the keys will not match, and the child will be replaced.

If there is interest, it is technically possible to support something isomorphic to React's old componentWillReceiveProps in this scheme, but this maps a bit oddly onto the current object model since we don't have a formal concept of "props" (it would probably end up being a class method that takes the instance of the component currently mounted in the DOM, the instance that the parent component is attempting to replace it with, and returns the instance the should actually be used). I'm also skeptical that using this API is ever a good idea anyway. Function components have become a best practice in React for a reason, and I think the current behaviour mirrors their semantics.

devdicated commented 4 years ago

Ah, I get it, the state is then used to recreate the Component in the ActionCable channel at https://github.com/unabridged/motion/blob/c528897f18ee489711335fdb1a93e7f68989660d/lib/motion/channel.rb#L33

There was an ActiveRecord collection in my component so that explains the size of the attribute (although there were only 3 records). After removing the @collection variable and turning it into @collection_parent_id = parent.id and @state = collection.to_a.hash the state was much smaller.