I am adapting a ReactNative mobile app to ReactNative Web. The app is done with react-navigation.
Currently, every time I set the params of a route (either through navigate or setParams), those params show in the URL. I end up with bad looking URLs like so:
http://localhost:19006/me/undefined?user=%5Bobject%20Object%5D
Either that or the URL contains irrelevant data for the user and generally looks messy.
Is there a way to not show the route params inside the URL?
You should re-consider if params is an appropriate place to have this data if you don't want in the URL. That you think that the URL contains irrelevant data is a sign that the data doesn't belong in the navigation state.
If you visit a screen directly from a URL, it should render the same as when you navigated to it porgrammatically. If you're passing something in params, it means that that information is needed for the screen to render correctly, and if you remove it from the URL, then that information is lost. Consider that it's possible to open the page directly from a URL, either on Web or with a deep link. If the required data isn't available, then it's not going to work.
In your case, seems like you're passing a full user object (maybe not, hard to say without code). Ideally, this object should be in your global store instead of params, and you should only pass the user id in params. Then you should gran the full object from your global store using that id, or trigger a data fetch if the objet isn't fetched yet (if needed).
You didn't specify the version of React Navigation in your question. In v5, you can customize how your params are stringified to URLs, and how they are parsed back using the stringify and parse options:
const linking = {
screens: {
Profile: {
path: 'user/:id',
parse: {
id: (id) => id.replace(/^#/, '')
},
stringify: {
id: (id) => `#{id}`,
},
},
},
};
This should help you with making URLs look prettier, and handle cases where params are not simple strings. However you can't omit params from the URL altogether since they are necessary data for the screen to render.
More details: https://reactnavigation.org/docs/configuring-links#passing-params
satya164's answer is definitely the proper path to follow. Just wanted to also post this solution as it is a direct answer to the question, even if not the most advisable.
import { getPathFromState} from "#react-navigation/native";
const linking = {
screens: {
...
},
getPathFromState(state, config){
let path = getPathFromState(state, config);
const index = path.indexOf("?")
if(index>=0){
path = path.substr(0, index);
}
return path;
}
};
NOTE: To use this solution you need to make sure that every object and function parameters are optional, otherwise if you reload the page you will get an error.
I removed every object and function from the url adding this custom getPathFromState to linkingOptions:
const linking = {
config: {
screens: {
...
}
},
getPathFromState: (state, options) => {
const cleanState = {
...state,
routes: state.routes.map(route => {
if(!route.params) {
return route
}
const cleanParams = {}
for(const param in route.params) {
const value = route.params[param]
if(typeof value !== "object" && typeof value !== "function") {
cleanParams[param] = value
}
}
return {
...route,
params: cleanParams,
}
}),
}
return getPathFromState(cleanState, options) //imported from #react-navigation/native
},
}
You can pass the parameters the component needs as a prop
like this
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
then in Details Component
const { itemId , otherParam} = route.params;
Alternatively, if you use Redux/Mobx or any other global state management
You can pull the data from there and not pass it through the URL
then get data with the help connect or with useSelector hooks
Related
My script I'm using axios and vuex but it was necessary to make a change from formData to Json in the script and with that it's returning from the POST/loginB2B 200 api, but it doesn't insert in the localstorage so it doesn't direct to the dashboard page.
**Auth.js**
import axios from "axios";
const state = {
user: null,
};
const getters = {
isAuthenticated: (state) => !!state.user,
StateUser: (state) => state.user,
};
async LogIn({commit}, user) {
await axios.post("loginB2B", user);
await commit("setUser", user.get("email"));
},
async LogOut({ commit }) {
let user = null;
commit("logout", user);
},
};
**Login.vue**
methods: {
...mapActions(["LogIn"]),
async submit() {
/*const User = new FormData();
User.append("email", this.form.username)
User.append("password", this.form.password)*/
try {
await this.LogIn({
"email": this.form.username,
"password": this.form.password
})
this.$router.push("/dashboard")
this.showError = false
} catch (error) {
this.showError = true
}
},
},
app.vue
name: "App",
created() {
const currentPath = this.$router.history.current.path;
if (window.localStorage.getItem("authenticated") === "false") {
this.$router.push("/login");
}
if (currentPath === "/") {
this.$router.push("/dashboard");
}
},
};
The api /loginB2B returns 200 but it doesn't create the storage to redirect to the dashboard.
I use this example, but I need to pass json instead of formData:
https://www.smashingmagazine.com/2020/10/authentication-in-vue-js/
There are a couple of problems here:
You do a window.localStorage.getItem call, but you never do a window.localStorage.setItem call anywhere that we can see, so that item is probably always empty. There also does not seem to be a good reason to use localStorage here, because you can just access your vuex store. I noticed in the link you provided that they use the vuex-persistedstate package. This does store stuff in localStorage by default under the vuex key, but you should not manually query that.
You are using the created lifecycle hook in App.vue, which usually is the main component that is mounted when you start the application. This also means that the code in this lifecycle hook is executed before you log in, or really do anything in the application. Instead use Route Navigation Guards from vue-router (https://router.vuejs.org/guide/advanced/navigation-guards.html).
Unrelated, but you are not checking the response from your axios post call, which means you are relying on this call always returning a status code that is not between 200 and 299, and that nothing and no-one will ever change the range of status codes that result in an error and which codes result in a response. It's not uncommon to widen the range of "successful" status codes and perform their own global code based on that. It's also not uncommon for these kind of endpoints to return a 200 OK status code with a response body that indicates that no login took place, to make it easier on the frontend to display something useful to the user. That may result in people logging in with invalid credentials.
Unrelated, but vuex mutations are always synchronous. You never should await them.
There's no easy way to solve your problem, so I would suggest making it robust from the get-go.
To properly solve your issue I would suggest using a global navigation guard in router.js, mark with the meta key which routes require authentication and which do not, and let the global navigation guard decide if it lets you load a new route or not. It looks like the article you linked goes a similar route. For completeness sake I will post it here as well for anyone visiting.
First of all, modify your router file under router/index.js to contain meta information about the routes you include. Load the store by importing it from the file where you define your store. We will then use the Global Navigation Guard beforeEach to check if the user may continue to that route.
We define the requiresAuth meta key for each route to check if we need to redirect someone if they are not logged in.
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '../store';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false
}
}
];
// Create a router with the routes we just defined
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// This navigation guard is called everytime you go to a new route,
// including the first route you try to load
router.beforeEach((to, from, next) => {
// to is the route object that we want to go to
const requiresAuthentication = to.meta.requiresAuth;
// Figure out if we are logged in
const userIsLoggedIn = store.getters['isAuthenticated']; // (maybe auth/isAuthenticated if you are using modules)
if (
(!requiresAuthentication) ||
(requiresAuthentication && userIsLoggedIn)
) {
// We meet the requirements to go to our intended destination, so we call
// the function next without any arguments to go where we intended to go
next();
// Then we return so we do not run any other code
return;
}
// Oh dear, we did try to access a route while we did not have the required
// permissions. Let's redirect the user to the login page by calling next
// with an object like you would do with `this.$router.push(..)`.
next({ name: 'Login' });
});
export default router;
Now you can remove the created hook from App.vue. Now when you manually change the url in the address bar, or use this.$router.push(..) or this.$router.replace(..) it will check this function, and redirect you to the login page if you are not allowed to access it.
In my NuxtJS(v. 2.10.2) application, I have a URL like below where pid is a post's id.
/post?pid=5e4844e34202d6075e593062
This URL works fine and loads the post as per the value passed to the pid query parameter. However, user can add new post by clicking Add Post button on the application bar that opens a dialog. Once the user clicks add, a request to back-end server is made to save the request. And once successful, user is redirected to the new post using vue router push like below
.then(data => {
if (data) {
this.$router.push({ path: `/post?pid=${data.id}` });
}
})
The problem is, user is not redirected to the new post, only the query parameter pid is updated. I suspect VueJS does not acknowledge this as a different URL and hence does nothing.
How to fix this?
Update: As an alternative tried the syntax below but getting the same behavior.
this.$router.push({ path: "post", query: { pid: data.id } });
Say you have a component post.vue which is mapped with /post URL.
Now if you redirect the user to /post?pid=13, the post.vue component won't mount again if it's already mounted ie. when you are already at /post or /post?pid=12.
[1] In this case, you can put a watch on the route to know if the route has been changed.
watch: {
'$route.path': {
handler (oldUrl, newUrl) {
let PID = this.$route.query.pid
// fetch data for this PID from the server.
// ...
}
}
}
OR
[2] If the component post.vue is mapped with some route say /post.
You can also use the lifecycle -> beforeRouteUpdate provided by vue-router
beforeRouteUpdate (to, from, next) {
let PID = to.query.pid
// fetch data for this PID from the server.
// ...
next()
}
By changing the approach component data can be updated as per the new query string value. Here is how it can be done.
Rather than trying to push to the same page again with different query string. The query string pid itself can be watched for change and on update new data can be fetched and the component data can be updated. In NuxtJS(v. 2.10.2) apps, this can be achieved with watchQuery. watchQuery is a NuxtJS property which watches changes to a query strings. And once it detects the change, all component methods(asyncData, fetch, validate..) are called. You can read more https://nuxtjs.org/api/pages-watchquery/
As for the solution, pushing to the same page with new query string remains the same.
.then(data => {
if (data) {
this.$router.push({ name: 'post', query: { pid: data.id } });
}
})
However, on the page.vue, where the data is fetched from the server. We need to add watchQuery property.
watchQuery: ["pid"],
async asyncData(context) {
let response = await context.$axios.$get(
`http://localhost:8080/find/id/${context.route.query.pid}`
);
return { postData: response };
},
data: () => ({
postData: null
})
Now, everytime the query string pid will change asyncData will be called. And that is it. An easy fix to updating component data when the query string value change.
try this solution
.then(data => {
if (data) {
this.$router.push({ name: 'post', query: { pid: data.id } });
}
})
hints:
// with query, resulting in /register?plan=private
router.push({ path: 'register', query: { plan: 'private' } })
Use watchQuery property (https://nuxtjs.org/docs/2.x/components-glossary/pages-watchquery)
export default {
watchQuery: true,
data: () => ...
}
In case anybody was looking for this:
Query parameters specified as a string do not work when passed to a path parameter:
router.push({path: 'route?query=params'})
When you want you use them as a string, just pass the whole string as an argument, like so: router.push('route?query=params')
It'll then be automagically picked by router and navigation will happen.
try this :
.then(data => {
if (data) {
this.$router.push('/post?pid=' + data.id);
}
})
hope it works!!!
My data doesn't appear on screen after reload page, but I have it on my store. I noticed after reload page mounted hook doesn't work. What it can be? I'm using nuxt
This can be because of many reasons. How does the data end up in the store? Is it static (already hardcoded into the store data) or is it fetched from an other source like an API?
Without your code, there is not much I can do to help out though - please share it and i'll take a look :)
In general, get familliar with the concepts below so you'll understand how Vue and Nuxt work together and handle data:
https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram (this is the Vue lifecycle)
https://nuxtjs.org/guide#schema (this is how Nuxt works on top of that)
https://dev.to/lilianaziolek/understanding-nuxt-vue-hooks-and-lifecycle-part-1-48lc (an article explaining a lot about these concepts)
What you can do is create a plugin that calls an action in your store which can load the data from another source such as ajax or localStorage and load the data into your state. You will also need something to save your state at some point.
An example module; if you are not using a module move this to Vuex.Store({...}):
const store1 = {
state: { value1: '' },
mutations: {
setValue(state, value) {
state.value1 = value
}
},
actions: {
async load({ commit }) {
let data = await fetch('/getMyValue')
let json = await data.json()
commit('setValue', json.value)
},
// Call `store.dispatch('store1/save')` to save
async save({ state }) {
await fetch('/setMyValue', {
method: 'post',
data: JSON.stringify(state) // Data should contain: { value: 'my value' }
})
}
}
}
You would then create the root of store like the following which uses the above module. This has a basic plugin. Once the store has finished loading each function in the plugins array is called. In our case we will use it to load/init the data in the store or modules.
export const store = new Vuex.Store({
// Remove if you are not using modules
modules: {
store1, store2
},
plugins: [
store => {
// Use `store.dispatch('load')` if you are not using modules
store.dispatch('store1/load')
store.dispatch('store2/load')
}
]
})
Background: I'm working on building a mobile app with react-native, and am setting up AWS's AppSync for synchronizing the app with cloud data sources.
The challenge: I have a view which shows all items in a list. The list's ID is passed in as a prop to the component. I need to use that list ID to query for the items of that list. I have the query working fine if I hard-code the list ID, but I'm having a hard time figuring out how to dynamically set the list ID for the query when props update.
Here's what I have working (with a hard-coded ID of testList01) in my ListPage component:
const getListItems = id => gql`
query getListItems {
getListItems(listID: ${id}) {
reference_id,
quantity,
}
}
`;
export default graphql(getListItems('testList01'), {
options: {
fetchPolicy: 'cache-and-network',
},
props: props => ({
listItems: props.data ? props.data.getListItems : [],
data: props.data,
}),
})(withNavigationFocus(ListPage));
I would like to be able to dynamically set which list to look up the items for based on a list ID, which is being passed in from props. Specifically, I'm using react-navigation to enter the ListPage, a view where a user can see the items on a List. So here's the code that gets executed when a user clicks on a list name and gets routed to the ListPage component:
handleListSelection(list: Object) {
const { navigation, userLists } = this.props;
navigation.navigate('ListPage', {
listID: list.record_id,
listName: list.list_name,
userLists,
});
}
From my previous (pre-AppSync/GraphQL) implementation, I know that I can access the list ID in ListPage via this.props.navigation.state.params.listID. I would like to be able to use that in my AppSync query, but because the query is created outside the component, I'm unable to access the props, and so am struggling to get the ID.
Got this working using a package called react-apollo-dynamic-query which I found here. The author of that package also links directly to a simple function for doing what I'm trying to do here.
Essentially it just wraps the regular graphql call in a simple way that exposes the props so they can be passed down to the query.
My code now looks likes this (which I have below my definition of the ListPage component, in the same file):
const getListItems = props => {
const listID = props.navigation.state.params.listID;
return gql`
query getListItems {
getListItems(listID: "${listID}") { // Note the variable being wrapped in double quotes
reference_id,
quantity,
}
}
`;
};
const config = {
options: {
fetchPolicy: 'cache-and-network',
},
props: props => ({
listItems: props.data ? props.data.getListItems : [],
}),
};
const MyApolloComponent = graphqlDynamic(getListItems, config)(ListPage);
export default MyApolloComponent;
It should work like this:
const getListItems = (id) => {
return gql`
query getListItems {
getListItems(listID: ${id}) {
reference_id,
quantity,
}
}
`;
}
Call this getListItems like the below
export default graphql(getListItems(id), { //from where ever you want to send the id
options: {
fetchPolicy: '
......
I have not tested this code. Please update if this works. Although I am quite sure that it works.
I have read using async data or fetch is a better approach in pages in nuxtjs instead of using the created hook.
I am struggling to get my code to work though
I had the following (Which does work fine)
created () {
this.$store.dispatch('cases/getCase', this.$route.params.caseId );
},
But how would I change that to work with the async method instead please, and be able to return more than one state when I need to.
I tried the following
async asyncData ({ params }) {
const thisCase = await this.$store.dispatch('cases/getCase', this.$route.params.caseId );
// constant thisUser
return { thisCase }
// return { thisCase, thisUser}
},
but this generated an error
undefined is not an object (evaluating 'this.$store')
Can anyone tell me what I am doing wrong please
Thanks
this not available in asyncData/fetch. It is even stated in docs in special orange warning.
You do NOT have access of the component instance through this inside
asyncData because it is called before initiating the component.
And again as said in docs
method receives the context object as the first argument, you can use
it to fetch some data and return the component data.
Context is where from you should be getting your store. Here docs for context.
So your code would be
async asyncData ({ params, store }) {
const thisCase = await store.dispatch('cases/getCase', params.caseId )
return { thisCase }
},