7.4 KiB
layout | title | date | author | categories | tags | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
post | A Tour of myPrayerJournal: State in the Browser | 2018-08-26 12:15:00 | Daniel |
|
|
NOTES:
- This is post 3 in a series; see [the introduction][intro] for all of them, and the requirements for which this software was built.
- Links that start with the text "mpj:" are links to the 1.0.0 tag (1.0 release) of myPrayerJournal, unless otherwise noted.
Flux (a pattern that originated at Facebook) defines state, as well as actions that can mutate that state. Redux is the most popular implementation of that pattern, and naturally works very well with React. However, other JavaScript framewoks use this pattern, as it ensures that state is managed sanely. (Well, the state is sane, but so is the developer!)
As part of Vue, the Vuex component is a flux implementation for Vue that provides a standard way of managing state. They explain it in much more detail, so if the concept is a new one, you may want to read their "What is Vuex?" page before you continue. Once you are ready, let's continue and take a look at how it's used in myPrayerJournal.
Defining the State
The store (mpj:store/index.js) exports a single new Vuex.Store
instance, with its state
property defining the items it will track, along with initial values for those items. This represents the initial state of the app, and is run whenever the browser is refreshed.
In looking at our store, there are 4 items that are tracked; two items are related to authentication, and two are related to the journal. As part of authentication (which will get a further exploration in its own post), we store the user's profile and identity token in local storage; the initial values for those items attempt to access those values. The two journal related items are simply initialized to an empty state.
Mutating the State
There are a few guiding principles for mutations in Vuex. First, they must be defined as part of the mutations
property in the store; outside code cannot simply change one state value to another without going through a mutation. Second, they must be synchronous; mutations must be a fast operation, and must be accomplished in sequence, to prevent race conditions and other inconsistencies. Third, mutations cannot be called directly; mutations are "committed" against the current store. Mutations receive the current state as their first parameter, and can receive as many other parameters as necessary.
(Side note: although these functions are called "mutations," Vuex is actually replacing the state on every call. This enables some really cool time-traveling debugging, as tools can replay states and their transformations.)
So, what do you do when you need to run an asynchronous process - like, say, calling an API to get the requests for the journal? These processes are called actions, and are defined on the actions
property of the store. Actions receive an object that has the state, but it also has a commit
property that can be used to commit mutations.
If you look at line 87 of store/index.js, you'll see the above concepts put into action1 as a user's journal is loaded. This one action can commit up to 4 mutations of state. The first clears out whatever was in the journal before, commits the LOADED_JOURNAL
mutation with an empty object. The second sets the isLoadingJournal
property to true
via the LOADING_JOURNAL
mutation. The third, called if the API call resolves successfully, commits the LOADED_JOURNAL
mutation with the results. The fourth, called whether it works or not, commits LOADING_JOURNAL
again, this time with false
as the parameter.
A note about the names of our mutations and actions - the Vuex team recommends defining constants for mutations and actions, to ensure that they are defined the same way in both the store, and in the code that's calling it. This code follows their recommendations, and those are defined in action-types.js
and mutation-types.js
in the store
directory.
Using the Store
So, we have this nice data store with a finite number of ways it can be mutated, but we have yet to use it. Since we looked at loading the journal, let's use it as our example (mpj:Journal.vue). Beginning on line 49, we wrap up the computed properties with ...mapState
, which exposes data items from the store as properties on the component. Just below that, the created
function calls into the store, exposed as $store
on the component instance, to execute the LOAD_JOURNAL
action.
The template uses the mapped state properties to control the display. On lines 4 and 5, we display one thing if the isLoadingJournal
property is true, and another (which is really the rest of the template) if it is not. Line 12 uses the journal
property to display a RequestCard
(mpj:RequestCard.vue) for each request in the journal.
I mentioned developer sanity above, and in the last post, I said that the logic that has RequestCard
making the decision on whether it should show, instead of Journal
deciding which ones it should show, would make sense. This is where we put those pieces together. The Vuex store is reactive; when data from it is rendered into the app, Vue will update the rendering if the store is changed. So, Journal
simply displays a "hang on" note when the journal is loading, and "all the requests" once it's loaded. RequestCard
only displays if the request should be displayed. And, the entire "brains" behind this is the thing that starts the entire process, the call to the LOAD_JOURNAL
action. We aren't moving things around, we're simply displaying the state of things as they are!
RequestListItem
(mpj:RequestListItem.vue) is another component that bases its display off state. It will always provide a button to view the full request, but based on whether the request is answered, snoozed, or in a recurrence period, it will display the appropriate buttons and informational text. It is used by the ActiveRequests
, SnoozedRequests
, and AnsweredRequests
list components, and those components are very simple; ActiveRequests
and SnoozedRequests
simply operate on the already-loaded journal, while AnsweredRequests
pulls its data when the page is loaded.
We've now toured our stateful front end; next time, we'll take a look at the API we use to get data into it.
1 Pun not originally intended, but it is now!