To Vuex Or Not To Vuex

December 10, 2019

This article discusses the design decisions behind storing data in Vuex. It's a good practice to put app-wide, shared data in the Vuex Store or a Vue.observable alternative. Vuex provides governance around mutations that reduces side effects. However, not everything needs to go in Vuex, particularly data that is not shared. That sounds intuitive, but as an app grows, the Vuex Actions may become a clearinghouse for API service calls. That function is best left to another type of object.

Two versions of the same program will be shown. Both versions satisfy the same functional requirements which are

Display a list of products,

Allow the list to be filtered, and

Select a product for a detailed view.

All three requirements will be implemented in a single view.

This CodePen presents the first version of the program. Type in "R" in the search bar and press "Search". Verify that the products are filtered. Select one of the products. Verify that the right panel displays product details.

See the Pen When Not to Vuex #1 by Carl Walker (@walkerca) on CodePen.

The following screenshot breaks the structure of the view down. There is a Search input text and button, a products table, and a product details table.

Product Search, Table, and Details

This UML class diagram provides additional insight into the internal structure of the program. This is the first version.

Structure of #1

The program is centered on the Products component. Products is composed of a ProductsSearch, a ProductDetails, and a BaseLayout component. The Vue App instance contains only the single component instance (Products).

The line with a a solid diamond is a UML composition relationship. This can be read as "has-as" as in "Products has-a ProductsSearch".

All of the data -- except a internal "nf" variable in ProductsSearch -- resides in the Vuex Store. These are state fields like "products" and "nameFilter". The fields are shared among the three components.

There are two types of fields stored in Vue in this program. There are app-wide, system fields like "loadingFlag" and "globalMessage". There are fields specific to the products functional area: "products", "nameFilter". Since BaseLayout is likely to be involved in other non-product components, having the ability to set the app-wide fields will transcend the product area. This makes them perfect candidates for shared Vuex Store state.

The other fields -- products specific -- may not be as universally needed. In fact, their use may be limited to just the products functional area. Suppose there are other functional areas. Following this design pattern, we'd expect the shared global state captured in the Vuex Store to grow. That may or may not be needed. If it's not needed, then the program begins to suffer from a lack of encapsulation; details are bleeding out of the functional areas.

In a UML class diagram, there are two compartments drawn into the rectangle class shape: attributes and operations. I'm using attributes for data() function fields and operations for methods . The slash in front of "filteredProducts" is a UML derived field notation that I think is ideal for a computed or a getter .

Although Vuex controls access to the shared data, storing unneeded items can introduce side effects if the full ramification of who's using what data aren't know as the program is being maintained. That is -- through the structured access -- a developer can still introduce a side effect. There's also additional code clutter (though this can be fended off with a Vuex Module-based design).

Data in Functional Area

This UML diagram presents an alternative. The functional area specific data is moved from the Vuex Store to the functional area (the Products component and offshoots). Rather than brokering all inter-component communication through Vuex, this structure has a more encapsulated feel that can reduce side effects. There's less of a chance that the products data can be mismanaged.

Structure of #2

The Vuex Store is greatly thinned out but still serves an important role in maintaining truly app-wide data. The products data has gravitated to the products-specific component "Products". The filteredProducts getter has been converted to a computed. Products now manages the data for the child components "Search" and "ProductDetails".

In fact, the ProductsSearch component has been fully generalized to a Search component. This is because Search no longer needs to refer to the specific field $store.state.nameFilter . Instead, it houses an input text and button and communicates back to Products. By breaking away from the Vuex Store, we're taking steps to forming shared and general components.

While ProductDetails remains very products-centric, it too is based on interactions with Products. This means that if the source of the product selection changes -- say to redact some fields -- the ProductDetails component will be insulated from that. Products can provide whatever it wants in the product prop.

This is the CodePen for version #2. It behaves identically to #1 but its components are less tied to the shared state.

See the Pen When Not to Vuex #2 by Carl Walker (@walkerca) on CodePen.

An additional design improvement could be a service class. If you would like to group like API calls in a single object for better maintenance, you can register that object with the Vue.prototype .

This article explained how a Vuex Store can break encapsulation by accruing unshared data from different functional areas. I noticed this on one of my teams. Developers saw a clean pattern in wrapping business logic up in Vuex Actions and began to do this unconditionally. If this happens, move unshared data out of Vuex and build function-oriented components (props in/events out).