trying to learn redux and having issues passing state around to keep a user up to date, main example - create a user on page 1, navigate to page 2, set something on the user and navigate back to page 1.
having to create an action for page2 navigated passing the user and also creating an action for page 1 navigated passing the user back and forth.
what is the correct usage of redux?
user must be passed back and forth to keep app sync
{
page1: user: {...data}} set user details,
page2: user: {...data}} passed user details but able to edit user
details and able to go back to page1
}
user can be accessed globally at any time and updated with action calls
{
userDetails: {user: {...data}},
page1: only page specific state, accesses this.props.userDetails.user
page2: only page specific state, accesses this.props.userDetails.user
}
So normally you'd just subscribe whatever component needed user information to the Redux store, typically with react-redux. In page2 you'd dispatch an action to modify the user in the store, then when the reducer processes those modifications, page1 can update with the new value.
An action for mutating the user might look like this:
{
type: 'SET_USER_EMAIL',
email: 'foo#wombat.com'
}
You'd dispatch that action from your page2 component. It'd be received in the reducer for the user object in the store:
switch (action.type) {
case 'SET_USER_EMAIL':
return {
...state,
email: action.email
}
}
If both pages are subscribed to the Redux store, there's not really a "passing back and forth" of the user value so much as both pages are notified when the store changes, and can update themselves based on those changes. I guess you'd say that each component listens to the store and can take action when it updates.
Navigation is a somewhat separate issue, though it certainly can use Redux. I feel like possibly you might benefit from revisiting some of the core concepts to make things clearer.
Related
I've got an interesting problem in my vue.js application and I don't know how to solve it.
We've got a "my listings" page that shows a grid of listings that the user created. When they click on one, it takes them to the listing details page. It opens this page in a new browser tab.
What we want to do is add a new component to the top of the page that shows the user the stats on their listing. But we want this component to show up ONLY when they come to the listing details page from the My Listings page. There are other ways of getting to the Listing Details page and we don't want the stats component to show up when they come from these other ways.
I would think this could be handled in the router. I tried seeing if I could detect that the user was coming from the My Listings page from the "from" parameter in the beforeEach(...) method of the router. I did this:
router.beforeEach(async (to, from, next) => {
console.log('from=', from);
console.log('to=', to);
});
When it prints the from parameter, I get this:
to= {
fullPath: "/"
hash: ""
matched: []
meta: {}
name: null
params: {}
path: "/"
query: {}
}
It contains no information about where it came from. I'm guessing this is because it opens the Listing Details page in a new tab. So I can't use the router to tell where the user came from.
Instead, I resorted to using localStorage:
On the My Listings page:
<v-btn :href="`/listings/${listing.listingId}`" target="_blank" #click="saveFromMyListings();">View Listing</v-btn>
...
saveFromMyListings() {
localStorage.setItem('from-my-listings', true);
},
On the Listing Details page:
async created () {
this.fromMyListings = localStorage.getItem('from-my-listings') === 'true';
localStorage.setItem('from-my-listings', false);
},
So long as I set the 'from-my-listings' item in localStorage to false immediately after I use it to determine that the user came from the My Listings page, it works. That way, it is ONLY set if the user comes from the My Listings page, and never set if the user comes from anywhere else.
The problem with this method is that if the user refreshes the page, the stats disappear. Obviously, this is because created() reruns and this time 'from-my-listings' is removed from localStorage. I can fix this by not setting it to false in created() once it's used, but then where do I remove it in such a way that it's guaranteed to be removed no matter how the user leaves the page (entering a new url directly in the browser, closing the browser, computer loses power, etc.)?
Is there some other hook in vue.js besides created() that runs only once (when the user first visits the page) but not on subsequent loads (like refresh)? Is there a way to pass props to a component in the router based on the state of localStorage that won't have to be passed again on refresh? What other solutions might there be to this problem?
You could use query parameters. You'd have to change the links to something like this:
yourapp.com/listing-detail/333?from=list
then in the created function you can check window.location.search for the from value
I'm building a vue.js application. We'd like to have a popup come up when the user attempts to leave a specific page. The popup should say "Are you sure you want to leave the page?" I know I can implement something in the beforeRouteLeave hook of the component, but I'm wondering if there's a way to implement this in the beforeEach event of the router (i.e. not the component). The reason I'd like to use the router is because beforeEach in the router seems to respond to the user entering a different path in the browser url bar, whereas the beforeRouteLeave hook on the component does not. However, I don't have access to the popup in the router whereas I do in the component (the popup would just be part of the template).
So the question is: how can I bring up a popup in the router before the user actually leaves the page?
Thanks.
First you can assign a name for each of your routes objects in routes array inside your router or another field like requiredConfirmation or something like that, imagine that we have a routes like this :
routes : [
{
path : '/needconfirm',
component : NeedConfirmToLeaveCom,
name : 'needconfirm-route1'
},
{
path : '/neednotconfirm',
component : NeedNotConfirmToLeaveCom,
name : 'normal-route1'
},
]
then you can use router.beforeEach to set some conditions or some confirmations based on your Origin route and Destination route.
something like this :
router.beforeEach((to,from,next) => {
if(from.name.startsWith("needconfirm-")) {
if(window.confirm("Are you sure you want to leave the page?")) {
next();
}
}else next();
});
UPDATE * :
if you want to use some custom components for your popup, you can use vuex to store your component's logic and toggler and import that component in your App.Vue or other root/child components you wish. because you have access to your store management using $store right?
UPDATE ** :
and one other thing i want to mention, if you want to save some progress or state and because of that you want to get confirmation from user (progress will lost if they switch route), you should consider using Vuex to store your progress or state of your application and if you want more persisted solution you can use VuexPersisted store management which uses LocalStorage.
Vue router navigation guards document
Vuex Doc
You should use beforeunload event listener on the main component in that view.
MDN Reference
Depending on the browser, it will show the popup with default values populated.
This is how I use it in the created hook of the main component
window.addEventListener("beforeunload", (e) => {
e.preventDefault()
// chrome requires returnValue to be set
const message = "You have unsaved changes. Are you sure you wish to leave?"
e.returnValue = message
return message
})
I am creating an app in React Native (create react native) and I am attempting to fetch data from an API every time a user transitions to a screen.
For navigation, I am using the React Navigation library with a combination of Drawer and Stack Navigators.
Typically, I have seen fetch handled via the ComponentDidMount() lifecycle. However, when navigating to a screen in a stack navigator, the ComponentDidMount() lifecycle doesn't appear to trigger and the fetch doesn't run.
Ex. user is on post index, then navigates to add post screen, submits and is redirected to view screen (for that post), then clicks back to return to index. Returning to index does not trigger ComponentDidMount() and fetch isn't run again, so results are not updated.
Additionally, sometimes I need to access navigation params to alter a fetch request when navigation between screens.
I originally was attempting to determine screen transition (when navigation params were passed) via componentWillReceiveProps() method, however, this seemed a bit unreliable.
I did more reading and it sounds like I should be subscribing to listeners (via react navigation). I am having a hard time finding recent examples.
Current process (example)
On the desired screen I would subscribe to listener(s) in the componentDidMount() method:
async componentDidMount() {
this.subs = [this.props.navigation.addListener('willFocus', payload => this.setup(payload))];
}
Based of an example, it sounds like its good practice to remove all subs when unmounting the screen, so I also add:
componentWillUnmount() {
this.subs.forEach((sub) => {
sub.remove();
});
}
then I add a setup() callback method that calls whatever fetch methods I require:
setup = (payload) => {
this.getExampleDataFromApi();
};
Additionally, sometimes I need to access Navigation params that will be used in API queries.
I am setting these params via methods in other screens like so:
goToProfile = (id) => {
this.props.navigation.navigate('Profile', {
exampleParam: 'some value',
});
};
It seems like I cannot access the navigation prop via the provided getParam() method when within callback method from a navigation listener.
For example, this returns undefined.
setup = (payload) => {
console.log(navigation.getParam('exampleParam', []));
};
Instead I am having to do
componentDidFocus = (payload) => {
console.log(payload.action.params.exampleParam);
};
Question
I wanted to ask if my approach for handling fetch when navigating seems appropriate, and if not, what is a better way to tackle handling API requests when navigating between screens?
Thanks, I really appreciate the help!
I have a ClientManagePage where I display client information and allow for the removal of the displayed client.
The vue-router route configuration for that page looks like this:
{
path: '/client/:id/manage',
name: 'client',
component: ClientManagePage,
props: ({ params }) => ({ id: params.id }),
}
The client entities are stored in a vuex store. ClientManagePage gets its client entity from the store using the id prop and displays various properties of the client and a "remove" button.
The remove button listener is (inside a mapActions):
async removeClientClicked(dispatch) {
// Wait for the action to complete before navigating to the client list
// because otherwise the ClientListPage might fetch the client list before
// this client is actually deleted on the backend and display it again.
await dispatch('removeClientAction', this.id);
this.$router.push({ name: 'clientList' });
},
The vuex action that removes a client is:
async function removeClientAction({ commit }, id) {
// Remove the client from the store first (optimistic removal)
commit('removeClient', id);
// Actually remove the client on the backend
await api.remove('clients', id);
// Moving "commit('removeClient', id);" here still produces the warning mentioned below
}
My problem is how to handle navigating to the other route when removing a client. The current code produces warnings in development mode such as:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
found in
---> <ClientManagePage> at src/pages/ClientManagePage.vue
<Root>
This is of course caused by the reactivity system kicking in and trying to update the content of the page with the now-deleted vuex client entity. This happens before the removeClientAction is completed therefore the navigation to the ClientList page.
I've come up with some possible solutions to this, but they are not very appealing:
Have a v-if="client" at the top of the ClientManagePage that hides everything while the client does not exist in the store.
Use the computed property client in ClientManagePage to return a default "dummy" client that contains the required properties for the page. The page will still flash with "fake" content while the action is underway though.
Navigate to "clientList" right after (or even before) dispatching removeClientAction. This causes the clientList to display the removed client briefly while the action completes which is not good.
Are there other solutions to this seemingly common problem of navigating away when deleting the underlying vuex entity displayed on the current page?
I ended up doing a big v-if at the top of the ClientManagePage that hides everything while the client does not exist in the store. It's not pretty, but it works. An improvement could be to display a "please wait, operation in progress" in v-else.
One option is to externalize the deletion of the record. There are a number of ways to do that, but the simplest for me was to create a new route, /records/delete/:id, and place a route guard on that route that triggers the removal. Then redirect to the records list where you wanted to go in the first place. Something along the lines of:
import store from "wherever/your/store/is";
const routes = [{
path: "/records/delete/:id",
name: "deleteRecord",
props: true,
beforeEnter: (to, from, next) => {
store.dispatch("DELETE_RECORD", to.params.id).then(() => console.log("record deleted!"));
next({name: "some/path/you/wanted/to/go/to"});
}
}, ...];
We are using React-Redux in your application. The problem is that we want to do undo and redo Redux state based on user navigation from browser buttons. Assume user is in page A and user browses couple of other pages and then he navigates to page A, for instance. Now If user presses back button in the browser, he'll go back to page A but here we want to have the previous instance of state which application had when user the page A.
Is there a centralized approach to solve this problem that doesn't need to handle the state manipulation manually.
What you are trying to achieve is a default behavior of React-Redux. If you are not trying to dispatch some actions, which manipulates specific component's state, when a route changes, it should persist its old state, without any additional functionality.
So my guess is that you are dispatching some actions when new route loads the component. How it could be dealt with this (e.g not to fetch resources from rest API once it existed, which finally caused to manipulate component) is here: https://github.com/reactjs/redux/blob/master/examples/async/src/actions/index.js#L35
const shouldFetchPosts = (state, reddit) => {
const posts = state.postsByReddit[reddit]
if (!posts) {
return true
}
if (posts.isFetching) {
return false
}
return posts.didInvalidate
}
export const fetchPostsIfNeeded = reddit => (dispatch, getState) => {
if (shouldFetchPosts(getState(), reddit)) {
return dispatch(fetchPosts(reddit))
}
}
So what this is doing is that it won't pass a new data into component once route changes if it already exists, so the old data/state stays there. You can abstract this functions more to make it easily reusable for all the other components.