Imagine this simple case. You have a Vue JS application in which users can create lists of tasks and sort them. These lists should be stored in a database by the server. Let's assume we have a ListComponent which does the bulk of the UX.
My question is, which pattern should I use to handle front-end and back-end data synchronisation?
A) Go through vuex: Store the active lists (those being shown, edited or created) in the vuex store. The ListComponent will change the store and then, the changes made to a list will be sent to the backend through an API.
B) Go directly to the server: Read and write directly from the ListComponent to the server every time a list is shown, edited or created.
If following A, what architecture should the store have? How and when should I kick a synchronisation? How can I keep track of what has changed and what has not?
Both can be correct depending on your use case. The application I'm currently building uses both.
When to use B: Go Directly to the server
Use B if the data is very important to save. In this case you may want to go directly to the server and serve up the response to verify it was committed to the DB. We use this process for Admin type changes. Note that you can also update Vuex after the server response and still use Vuex to serve up your data.
When to use A: Go Through Vuex
Use A if you require faster experience, since there's no waiting for the server. You must be okay with optimistically displaying changes before actually saving. The other benefit is you can sync Vuex to localStorage. We use this for user preferences that are used to customize views. Its a poor experience to slow down the page just to wait on fetching those.
How to use A: Go through Vuex
There's a couple ways to do this. Here's one pattern:
Dispatch Vuex Action 1 from component
Commit Vuex Mutation from Action that updates state - this is an optimistic update as you're assuming it'll go through
Dispatch another Vuex Action 2 from Action 1 - This assumes you'll reuse this Action in multiple Actions, otherwise it can all go in Action 1
Action 2 sends data to server
Upon promise return, Action 2 commits mutation to update Vuex state
Mutation needs to handle any discrepancies (or errors)
Count the cost of Vuex
Like your comment shows, its good to count the cost if you need to use Vuex at all because it does add a lot of overhead and complexity. The ideal is to write your components in such a way as to contain all interactions with one type of data (such as 'Lists') so you do not have to share state through Vuex.
Related
My question is, what's the point of setting up Vuex for the server when the state will be overwritten when the client side hydration takes place?
I have some data (Helm env variables) that I want to store in the vuex store for later use.
These variables is only available to me on the server, so I started trying to add them to the store in my createApp script when running on the server.
The store state however is reset when the client side hydration kicks in, so no env variables left.
Google told me I should use like window.INITIAL_DATA to set the state again on the client:
store.replaceState(window.INITIAL_DATA)
But if have to use the window object to pass store data to the client, what's the point of using Vuex on the server at all?
Isn't it better to skip Vuex overhead on the server and just use Vuex on the client and populate it with INITIAL_DATA?
I'm probably missing something..
https://ssr.vuejs.org/guide/data.html#data-store
During SSR, we are essentially rendering a "snapshot" of our app. The asynchronous data from our components needs to be available before we mount the client side app - otherwise the client app would render using different state and the hydration would fail.
To address this, the fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, we can pre-fetch and fill data into the store while rendering. In addition, we will serialize and inline the state in the HTML after the app has finished rendering. The client-side store can directly pick up the inlined state before we mount the app.
Also to mention:
The data you access while SSR in any Component needs to come from somewhere if you want to share information across Components, this is what Vuex is there for.
I have turned several axios requests made by application from 3 sibling components into one. So this query now brings back all the information in one simple request. Trouble is I'm not sure architecturally, where it makes sense to perform the request, store the data and propogate the response to the three sibling components.
Should I store the response in an application-Level shared state ? Also then how do I propogate the response, to these 3 sibling comnponents using Vuex?
if it is critical data then you might want to request it in Vuex nuxtServerInit action and save it in vuex state using commit. Read more
That way your app will receive the info once when it renders on server & can be used from any page or component using vuex getters.
Is it necessary that the store functions will be executed only client side or is there any case where the store functions can be invoked by Nuxt server too?
Store functions can absolutely be executed server side. For example, serverInit. They can also potentially be called in lifecycle hooks that happen serverside, like created and beforeCreate.
However, it's exceedingly rare that you should be worried about this. Generally, store actions or mutations are committed once on page load, and from then on as a result of user interaction which always happens client side.
So rather than using process.server checks everywhere, simply code your data fetching in your mounted hooks.
Let's suppose I have a collection of books retrieved by a Vuex action called fetchBooks (this action commit the SETBOOKS mutation to change state).
When I dispatch the action remove to remove a book, I have two options:
1) Send request to the API to delete the resource and then dispatch fetchBooks to reload the books listing.
2) Send request to the API to delete the resource and then commit the REMOVE mutation to remove the book from state, without any additional HTTP request.
The first seems more easy, I can use the same technique for add/update/delete, with the price of doing an additional request to reload the listing.
The second is more cheap (no additional request), but require more logic to handle state for each add/update/delete case.
What is the right way to do it?
I would go with option 2.
In the general practice "getBooks" could be a huge array, and it is better not to requery the data from the DB when you already have it locally.
Just make sure you are deleting the book inside a try catch block, so if the delete doesn't go in the backend, you should alert the user.
In my react native app that tracks instrument practice I have three stores:
SessionStore
GoalStore
InstrumentStore
The stores each manage one model (Session, Goal, Instrument) and getting/updating the server via a REST api.
The SessionStore listens to actions regarding Sessions (obviously): session.add, session.update. But it also listens to changes to the other stores, to be able to update the Sessions if a Goal or Instrument changes name.
Correspondingly the InstrumentStore listens to Instrument actions, but also to Session actions to update statistics on how many sessions uses a particular instrument.
To be able to not have race conditions, the InstrumentStore will act on the action session.add but wait for the SessionStore to handle the action first (to ensure the Session has been updated in the API). To do this I use dispatcher.waitFor with the SessionStore dispatchToken as a semaphore.
The problem: since all stores use each others dispatchTokens they all have to import each other. This is a circular dependency on modules and leads to strange race conditions. Sometimes one of the stores haven't been constructed when it's included by one of the other stores.
Here are my stores: https://github.com/osirisguitar/GuitarJournalApp/tree/feature/flat-ui/js/stores
Am I using the flux pattern the wrong way?
Addition
This is what I want to happen (in sequence):
Session is updated:
Send updated session to API
Refresh SessionStore
Refresh GoalStore
Refresh InstrumentStore
2, 3 and 4 need to wait for 1 to complete, that's why GoalStore and InstrumentStore need the SessionStore dispatch token.
Goal is update:
Send updated goal to API
Refresh GoalStore
Refresh SessionStore
2 and 3 need to wait for 1, this is why SessionStore needs the GoalStore dispatchToken which introduces the circular dependency.
You have some duplication going on.
All stores will hear all dispatches. That's the beauty of having a single dispatcher. So when you dispatch a sessions.add or sessions.update action, you're hitting three different Stores, and two of them are doing the exact same thing. That's a no-no.
As a rule, each Store's dispatch token should only be responsible for updating that store. So your Goal and Instrument stores should not be updating the SessionsStore. The .refresh and .emit should be happening within the SessionsStore dispatch token only.
EDIT to answer your edited question.
I think your confusion is because you're not recognizing that the dispatcher.register takes in a function as it's argument, and not an object.
Functions, in JS, do not evaluate their contents on declaration. They are evaluated when executed only.
Simple example;
func = function(){ console.log(testVar) } // No error, even though testVar is undefined
func() // ERROR: testVar is undefined
var testVar = 'hey';
func() // log: 'hey';
dispatcher.register takes a function as it's input, and returns an key (in the format ID_#). That key is generated by the dispatcher itself without running the input function. The input function is simply stored for later and run each time a payload is dispatched.
That means that you don't need the internal variables to be defined until your first dispatch. And because you also don't want to dispatch anything until you've created your stores, this becomes a non-issue.
But it also means that the dispatcher, by default, has a sort-of circular dependency against itself (relying on the return values of it's own functions, as stored in external variables). But that's the design of the dispatcher. Unless you're going to write a new dispatcher, that's just part of the deal.
It's also worth pointing out that if you create a true circular dependency by calling multiple waitFors that deadlock against one another, the dispatcher will correctly throw an error saying as much;
Dispatcher.waitFor(...): Circular dependency detected while waiting for ID_#