React/Flux - Keeping Status Attributes Organized? - javascript

It seems pretty common in react/flux to respond to trigger actions that set specific status attributes on your state data for your components to react to by rendering this vs. that.
Simple example: User clicks the "complete" button on a to-do list item component, it fires an action that sets complete: true on that item in the store, the component re-renders and reacts to that "complete" status attribute by rendering a friendly check-mark.
Not-so-simple example: Application starts loading collection of data from your API, so you init your store with loading: true, and your component reacts by rendering a spinner. When the load is complete, an action sets loading: false so the component will go ahead and render the items...but if there was an error, the action will also trigger error: true prompting the component to render some red warning text. Now you've got two toggles to manage, un-set the error flag if you give it another go, etc.
Really-not-so-simple example: User clicks the "edit" button on a to-do list item, so you set editing: true on that item so the component renders the form instead of the display. Then, the user hits a submit button. Now you've got editing: true and submitting: true in case you want to prevent a double-submit while the update is in transit. Then, if there's an error, you need to shut submitting off but keep editing true and add an error flag with a message and now if the user just navigates away from the page you've got to clean all that up...
So...how do we keep all this stuff sane? Consistent? Readable? Are there patterns/conventions/utilities for this kind of status data? How do we keep all these status flags separate from our actual data - the stuff we loaded and may want to submit back to the API, without it having to swat away all these additional bits of information we tacked on?
Maybe it all just comes down to writing clean code, but it seems like the kind of domain that there might be more to thank I'm aware of...

Instead of using flags (booleans), I would recommend having a currentStatus field. You can then use constants to give that field a value. Something like...
var Status = {
LOADING: 1,
EDITING: 2
};
and so on. In the case that you might have more than one state at once (can't come up with a decent example right now), you can use JS' Bitwise ops.
Status = {
LOADING: 1,
EDITING: 2,
SUBMITTING: 4,
SOMETHING_ELSE: 8
};
// Let's say I'm loading and doing something_else
var currentStatus = Status.LOADING + Status.SOMETHING_ELSE;
// Am I editing?
console.log(Status.EDITING & currentStatus); // 0, so no
// Am I doing something_else?
console.log(Status.SOMETHING_ELSE & currentStatus); // non-zero? Then true

Related

How to store multiple copies of a certain state in redux

It should be noted that I am using Redux with React-Native.
For simplicity sake, lets say I have an email viewer. There's a homepage with a massive list of emails and when you tap on an email, you are navigated to another screen. Here, the contents of the email, title, subject, body, is all held in a piece of state managed by redux. This all works fine.
Lets say I back out of that page, and now navigate to another email. The previous email pops up for a split second before the new email is shown. It should be noted here that I am storing the data in AsyncStorage as a sort of "cache?". The issue here is that since I only re-update the state whenever I tap on an email, the state which is the body of the email viewing page gets updated a split second after the user is navigated to it. This, is annoying.
The heart of the question is this
How can I store the body of my data in another piece of state, functionally identical to the current-email-viewing-state without overwriting the currently active state?
or
is this even the best way to do this?
Thanks
You could use Redux's lifecycle methods to handle this. Let's say the state for your email detail component looks something like this:
export const initialState: StateShape = {
loading: false,
readOnlyEmailData: {
recipientEmails: null,
senderEmail: null,
body: null,
},
};
When the email detail component (let's call it EmailDetail.jsx) is loading, you can use ComponentDidMount() to get and set your values.
You'll probably have actions and actionCreators like getEmail, getEmailSuccess, and getEmailError. Set loading to true in getEmail, and then false again on success or error. You can add a conditionally rendered spinner component (easy to borrow from something like MaterialUI) to EmailDetail, which is visible only when loading is true, and render the rest of your content when loading is false.
When the user hits the back button or otherwise navigates away from the component, componentWillUnmount() can be given a list of things to do as the component prepares to unmount. In this case you could use a reset method to reset loading and readOnlyEmailData to initial state on unmount.
When the user clicks on a new email, it will load the new email data on mount and show the spinner until the new email data is available.
There are other ways to do this, this is not the most optimized, but it should work quite a bit better than what you've tried so far. I hope it helps :)

Using Redux-Form for search filters, how can I "reset" and re-submit the form?

Update: Here is an example Pen
https://codepen.io/anon/pen/vwzGYY?editors=0011
Preface
Based on my research, it seems like I need a completely different approach. Maybe you can suggest one?
Context
I'm using a Redux-Form (technically an older version, but the API's
in question seem really stable. We can burn that bridge when we get there.) to set some "filters" for a sort of search results list.
In particular, since I want the pages to be link-able, I'm also setting the form content in the URL query params, via React-Router, or initially setting it on page load via similar mechanism.
The only field so far is "organization_name", a text field, used to set the query param value, and trigger an API request for /endpoint?name={some_name}.
E.g.,
<Field
name="organization_name"
component="input"
type="text"
placeholder="Organization Name"
value={value}
/>
I've tried several things, but here's a recent shot:
I'm grabbing reset, change, and other things from default props. I'm passing in a handleSubmit as required.
handleSubmit works correctly, to do some state updating, set/push the URL query params with React Router, and then make a new API call/update display of new results! Woot!
What I want / expect
In the long run, I would like a "reset filters" button that sets all filter values back to defaults (e.g., set the "name" value to empty string), and re-submits the form (thus triggering handleSubmit).
What I first tried to implement was a button, as such:
<button
name="reset_filters_button"
type="button"
onClick={resetAndSubmit}
disabled={pristine || submitting}
>
Clear Search
</button>
Where resetAndSubmit is defined on the form container as such:
const resetAndSubmit = event => {
reset();
handleSubmit();
};
What actually happens... (submit takes precedence over dispatched events?)
Using the Chrome dev tools debugger, I can clearly see that the reset method is called, and returns it's dispatch(...)'d event. However, the form and state values are not updated before handleSubmit() runs and submits the form.
I think this might have to do with the submit event taking priority?
I have also tried something janky, like importing change (default prop for the container) and defining the reset button thus:
<button
name="reset_filters_button"
type="button"
onClick={() => {
change('organization_name', '');
methodThatDispatchesSubmitAction();
}}
disabled={pristine || submitting}
>
Clear Search
</button>
Which (if I remove methodThatDispatchesSubmitAction()) works correctly to set the field value back to blank, making the form technically "pristine" again as well.
methodThatDispatchesSubmitAction() (if it's not obvious) is bound on the parent via dispatchToProps, and passed in to the form container, where it uses the "remote submit" suggestion, e.g,
// organization_list_filter == name of the Redux-Form to submit.
dispatch(submit('organization_list_filter'));
TL;DR and final question:
How does one properly reset a form and submit its' default/empty values?
Every time I dispatch or directly call Redux Form 'submit', it ends up submitting the form before clearing values from state, or the UI. I have walked through this with a debugger and it's not skipping my call to reset or change. It's like an async/race issue, but I admit I am out of my league in this particular case for sure.
Am I just Straight Up Doing It Wrong?
It is most definitely a race condition issue (or since we aren't actually dealing with threads, an order of events issue).
The reason using a methodThatDispatchesSubmitAction works when your current example does not, is because a dispatched action has the benefit of reading data directly from the redux store. Your example is not reading from the redux store, it's reading from a property that is passed in. Yes, this property comes from the redux store, but the problem you are seeing is that it hasn't been updated in your component yet.
Bear with me as this next piece is not going to be entirely accurate but it should suffice to explain what you are seeing.
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is fired using values prop (old state data still)
Component is updated with new props from redux state
As you can see, the order of events don't allow for an updated state to be given to the property until our click code has finished running. If you've ever watched a video on the JS Event Loop (I highly recommend it), you'll know that our onClick handle will run in full before any other async operations (or sync operations that come after our click) have a chance to run.
There are good reasons why Components aren't given updated props right away but the primary one is performance. You can see that this order is in fact the problem by wrapping the handleSubmit in an async event that fires immediately (it doesn't actually fire immediately, all other sync/async operations queued before it will finish).
const resetAndSubmit = (event) => {
reset();
setImmediate(() => handleSubmit());
}
This changes the order of events as follows:
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is queued on the event loop (not run yet)
Component is updated with new props from redux state
Event loop reaches queued code and runs is
-> Handle submit is fired using values prop (new state data)
Hopefully, this helps you understand why the problem is occurring. As for solutions to fix it. Obviously, you can queue the handle submit as I've shown above. Another option would the one you've described as using a dispatch to perform the submit. A third option would be to use something a bit heavier like redux-thunk or redux-sagas that tie the resetAndSubmit action into a single dispatch. Although honestly, this is the same as option two, just reduced into a single dispatch. Option four, don't use redux for all your data. Obviously, this fourth option comes with trade-offs but my point being, just because you are using redux in a project doesn't mean every single piece of data needs to be in redux. Though it completely defeats the purpose of redux-forms.
I should also add, you are not alone in being confused by this. When you introduce redux, it messes with how you traditionally think about working with code. Normally you think, I do A then B. But with redux, you do A, wait for A's changes to make it through the system, and then you do B. That's where Sagas or Thunks can be nice. You move more logic to the store to act on the dispatch rather than wait for it to all make its way back down to a component via props.

How to fetch data in redux before first render and set it to default state

I have dropdown filter to show items by date, for example and show data for last 24 hours, show data for last 3 days.
I have defaultState in my reducer
const defaultState = {
dataArray: [],
a: true,
b: false
}
By default dataArray is empty.
And I have reducer and action. In componentDidMount method I fetch data from server by dispatching some actions.
If I refresh page, default page that list last items for 24 hours is empty because dataArry comes from defaultState in my reducer. But if I change page to list data for last 3 days then componentWillReceiveProps works and inside this method I fetch data and it reduce my state and returns new one with
dataArray = [{some data}]
How to fetch data and set it to state to render it after page was refreshed?
Add to defaultState a 'loading' variable and initialize it to true. When fetching of data is completed, set it to false.
In your component check this variable. If it is true display a spinner and/or a loading message, otherwise display the data.
In addition to that, every time you start fetching, before fetching, fire an action called 'FETCH_START' which will set the loading variable to true.
If there is an error in fetching, you can set another state variable to the error message. This variable will be initialized (every time you start fetching) to an empty string. If loading is completed you can check this error variable, and display the error message if there was an error, instead of displaying the data.
This process is useful for various cases, such as authentication, etc.
Yossi's answer is pretty spot on. I just want to take it a step further and mention that you can also enhance your users' experience by knowning when to fetch data. For instance, if you're on a landing page and know most of your users are going to be browsing your store, start fetching that data on the landing page while nothing is happening.
To go along with that, take a look at this: Google Dev's requestIdleCallback()

VueJS if / else: the else loads first, then the if loads

The problem:
VueJS is briefly showing the "else" condition before the "if" condition loads. This is causing an unintended experience for the user because they should never see the "else" when the "if" is truthy. They should only see the "if" if the "if" is true.
The detail:
I have a custom online store and many different markets. I get back an item count from my server and if that number is zero then I say there are no items for that country, in other words the country is not open yet for this store. If the number is not zero then don't show the message and actually show the items.
Here is a slimmed down version of my code:
HTML
<div v-if="count !== 0">Items are in the store, here they are!</div>
<div v-else>Sorry no items in the store.</div>
JS
mounted() {
// Checks the location and gets items from server.
if (this.location) {
this.getItemsForHomeView();
}
}
I found this: Understanding Vue.js Lifecycle Hooks but it didn't help because when I tried moving the function out of the mounted() and into any of the earlier ones, like beforeCreate() or created() or beforeMount() I get the same result.
In addition to handling an empty item count once the data has resolved, you need to handle the case of there being no data yet to count. You didn't mention using Vue Router, but this discussion of handling async data still applies: you can either have the async fetch prevent the component from rendering at all, using a beforeMount or beforeRouteEnter hook, or wrap all the item list markup in another conditional, which is only rendered after your data has been fetched, and something like isLoaded (local state) is set to true.

Dom node insertion/removal without mutation observers

I'm building a Tour component in React whose purpose is to introduce the user to the web app's interface. Parts of the "Tour" involve validating the user's actions, (e.g. if the current step involves opening a modal, once the user does so, the "Tour" should progress otherwise it should show an error if the user tries to progress by clicking 'Next').
For this I need to detect changes in the DOM, (e.g. a modal being opened or a div with a specific class appearing). I've had some ideas about wiring up an 'onNext' function that progresses the tutorial once the user interacts with certain target elements (e.g. 'Open Modal' button), but this seems like a hack, I want to govern the progression of the tour only by the actual elements present in the DOM not by listening for clicks that will result in the necessary elements showing up eventually.
One of the big constraints is avoiding MutationObservers in addition to usage of jQuery. With that said, I'm interested in hunches about how to validate the dom, how would one use pure javascript and the dom to determine the addition and removal of elements?
I think you're best served by implementing a Flux architecture to handle this. Redux is a good fit.
Create a Redux Reducer for your tour progression. The state of this reducer should be a key that corresponds to the current step of the tour that the user is within.
All components used in the tour should have access to this tour state as a prop. Use this prop to determine functionality. I.e. for your example of a dialog that must be opened, the code might look like this, within a relevant component;
openModal(){
if(this.props.tourStep == 'prompt_modal_open'){
ActionCreator.progressTourStep();
}
// code for actually opening the modal goes here
},
someOtherAction(){
if(this.props.tourStep == 'prompt_modal_open'){
//Display error message here
} else {
//normal action result here
}
}
When the user is not taking the tour, simply set tourStep in the reducer to undefined, and any tour related functionality will be turned off.
Alternately, if you want to keep your components clean and "dumb", you can put this logic directly into the action creator with the help of Redux-Thunk;
ActionCreator.openModal = function(){
return function(dispatch, getState){
var state = getState();
if(state.tourStep == 'prompt_modal_open'){
dispatch({type: 'progress_tour_step'});
}
dispatch({type: 'open_modal'});
}
}
ActionCreator.someOtherAction = function(){
return function(dispatch, getState){
var state = getState();
if(state.tourStep != undefined){
dispatch({type: 'show_error'});
} else {
dispatch({type: 'some_other_action_type'});
}
}
}

Categories