-- react,react-redux,react-router-dom --
if i use window.location.href = /contact/edit/${id}; works and redirect but problem is page reload and clear state data .
but it's not work when i use this.props.history.push(/contact/edit/${id});
there is any solution i want to redirect without page reload and also keep state data
class Contact extends Component {
editContact = id => {
const payload = {id:id,type:"setContact"}
this.props.singleAction(payload)
this.props.history.push(`/contact/edit/${id}`);
}
render() {
const { id, name, email, phone } = this.props.contact;
return (
<div className="card card-body mb-3">
<h4>
{name}
<i
className="fas fa-pencil-alt"
onClick={this.editContact.bind(this,id)}
style={{
cursor: 'pointer',
float: 'right',
color: 'black',
marginRight: '1rem'
}}
/>
</h4>
<ul className="list-group">
<li className="list-group-item">Email: {email}</li>
<li className="list-group-item">Phone: {phone}</li>
</ul>
</div>
);
}
}
export default connect(null,{singleAction})(Contact);
You probably don't have history in your props.
Try to inject it with withRouter HOC:
import { withRouter } from "react-router";
// ...
export default connect(null,{singleAction})(withRouter(Contact));
When I want to do a search multiple times it shows me the NavigationDuplicated error. My search is in the navbar and the way I have configured the search is to take the value using a model and then pass the value as a parameter to the ContentSearched component, and then receive the value of the search in that component.
I know the right way is to use an emitter, but I still don't know how to learn to use it. To access the emit is context.emit('', someValue)
NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/search") is not allowed", stack: "Error↵ at new NavigationDuplicated (webpack-int…node_modules/vue/dist/vue.runtime.esm.js:1853:26)"}
NavBar.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-nav" v-bind:class="{'navbarOpen': show }">
<div class="container">
<router-link to="/" class="navbar-brand">
<img src="../assets/logo.png" alt="Horizon Anime" id="logo">
</router-link>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" v-on:click.prevent="toggleNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent" v-bind:class="{'show': show }">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<router-link class="nav-link" to="/" ><i class="fas fa-compass"></i> Series</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'EpisodesSection'}" ><i class="fas fa-compact-disc"></i> Episodios</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'MovieSection'}" ><i class="fas fa-film"></i> Peliculas</router-link>
</li>
</ul>
<div class="search-bar">
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" v-model="query" type="search" placeholder="Buscar películas, series ..." aria-label="Search">
<button class="btn btn-main my-2 my-sm-0" #click.prevent="goto()" type="submit"><i class="fas fa-search"></i></button>
</form>
</div>
</div>
</div>
</nav>
</template>
<script>
import {value} from 'vue-function-api';
import {useRouter} from '#u3u/vue-hooks';
export default {
name: "NavBar",
setup(context){
const {router} = useRouter();
const query = value("");
let show = value(true);
const toggleNavbar = () => show.value = !show.value;
const goto = () =>{
let to = {name: 'ContentSearched' , params:{query: query}}
router.push(to);
};
return{
show,
toggleNavbar,
goto,
query
}
}
}
</script>
ContentSearched.vue
<template>
<div class="container">
<BoxLink/>
<main class="Main">
<div class="alert alert-primary" role="alert">
Resultados para "{{query}}"
</div>
<div v-if="isLoading">
<!-- <img class="loading" src="../assets/loading.gif" alt="loading"> -->
</div>
<div v-else>
<ul class="ListEpisodios AX Rows A06 C04 D02">
<li v-for="(content, index) in contentSearched" :key="index">
<div v-if="content.type === 'serie'">
<Series :series="content"/>
</div>
<div v-if="content.type === 'pelicula'">
<Movies :movies="content"/>
</div>
</li>
</ul>
</div>
</main>
</div>
</template>
<script>
import {onCreated} from "vue-function-api"
import {useState , useRouter , useStore} from '#u3u/vue-hooks';
import BoxLink from "../components/BoxLink";
import Movies from "../components/Movies";
import Series from "../components/Series";
export default{
name: 'ContentSearched',
components:{
BoxLink,
Movies,
Series
},
setup(context){
const store = useStore();
const {route} = useRouter();
const state = {
...useState(['contentSearched' , 'isLoading'])
};
const query = route.value.params.query;
onCreated(() =>{
store.value.dispatch('GET_CONTENT_SEARCH' , query.value);
});
return{
...state,
query,
}
}
};
</script>
This happened to me when I had a router-link pointing to the same route. e.g. /products/1.
The user is able to click on the products, but if a product was already clicked (and the component view was already loaded) and the user attempts to click it again, the error/warning shows in the console.
You can learn more on the github issue..
Posva, one of the main contributors of vue-router suggests:
router.push('your-path').catch(err => {})
However, if you don't want to have a catch block which does nothing, in order to solve the issue you can compare the router navigation with the current route and only navigate if they differ:
const path = `/products/${id}`
if (this.$route.path !== path) this.$router.push(path)
Note: $route is an object provided by vue-router to every component. See The Route Object for more info.
I think the best solution to this problem can be implemented at the root level if we are not going to further use Router.push as asynchronous call.
import Router from 'vue-router';
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
};
Vue.use(Router);
If you are not feeling comfortable catching all kind of errors, I think this implementation is more considerate:
this.$router.push("path").catch(error => {
if (error.name != "NavigationDuplicated") {
throw error;
}
});
As of 2021, global configuration :
I only wanted to silence NavigationDuplicated error, an empty catch can be dangerous. So I did this :
const router = new VueRouter({/* ... */})
const originalPush = router.push
router.push = function push(location, onResolve, onReject)
{
if (onResolve || onReject) {
return originalPush.call(this, location, onResolve, onReject)
}
return originalPush.call(this, location).catch((err) => {
if (VueRouter.isNavigationFailure(err)) {
return err
}
return Promise.reject(err)
})
}
Insert this once when you initialize vue-router.
Thanks to #Oleg Abrazhaev for the update.
I encountered the same problem while searching. My solution is to add timestamp to the this.$route.query parameters of the search page.
this.$router.push({
path: "/search",
query: {
q: this.searchQuery,
t: new Date().getTime(),
}
});
Hope it helps you.
if you are using router.push in your code and you don't care about the navigation failing, you should catch it by using catch:
router.push('/location').catch(err => {})
in manual:
router.push(location, onComplete?, onAbort?)
You can use more simply
router.push("/", () => {});
You mixed multiple concepts here from router-links to programmatic navigation, to query params to a state store. That makes it a bit difficult to help you and tell you what the "correct" solution here is.
Nonetheless, I think the best approach for you would be to:
1) define your route as
{
path: "/search/:searchString",
component: MySearchComponent,
props: true
}
2) use a responsive <router-link> instead of your router.push
<input type="text" v-model="searchString">
<router-link :to="'/search/'+searchString" tag="button">search</router-link>
3) access the searchString in your search component via props: ['searchString'] and this.searchString
props: ['searchString'],
...
computed: {
msg() {
return `Searching for, ${this.searchString}!`;
}
}
Full example: https://codesandbox.io/s/vue-routing-example-9zc6g
Note, I just forked the first codesandbox with a router I could find, adjust accordingly.
For TypeScript it worked like this
const superPush = VueRouter.prototype.push
VueRouter.prototype.push = async function push(loc:RawLocation):Promise<Route> {
try {
return await superPush.bind(this)(loc)
} catch (e) {
if (e?.name === 'NavigationDuplicated') {
console.warn(e)
return e
} else {
throw e
}
}
}
I'm very late to the party, but I thought I'd add my solution to this issue as it isn't listed: I simply placed an intermediate searching page as a pass-through to the search results view. I am now using this page for doing some preprocessing of the search terms.
The page template simply is:
<template>
<div>searching ...</div>
</template>
The NavigationDuplicated error is now gone and as an added benefit because I perform the fetch in this intermediate page, the responsibility for error-handling is isolated from both the search bar and the results view.
I post here the solution I found, because I was not able to find it well documented somewhere, and I went through it by trial and error.
It could be useful to someone, or someone may fix my misunderstood interpretation of vue-router guards.
It make use of vue-router V4.x and a global beforeEach guard.
The use cases are:
user asks for https://app.com/ without being already authorized;
user asks for https://app.com/ being already authorized;
user asks for any available routing, which requires auth or not.
Routes:
const routes = [
/**
* Routes not requiring auth
*/
{
path: '/',
component: () => import('layouts/NotAuthorizedLayout.vue'),
children: [
{
path: 'login',
name: 'LOGIN',
component: () => import('pages/Login.vue')
},
{
path: 'emailsignup',
component: () => import('pages/EmailSignup.vue')
},
{
path: 'forgottenpassword',
component: () => import('pages/ForgottenPassword.vue')
}
]
},
/**
* Routes requiring auth
*/
{
path: '/',
component: () => import('layouts/AuthorizedLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'authors',
name: 'AUTHORS',
component: () => import('pages/Authors.vue')
},
{ path: 'profile', component: () => import('pages/userProfile/index.vue') }
]
}
];
beforeEach global guard:
const redirectToLogin = route => {
const LOGIN = 'LOGIN';
if (route.name != LOGIN) {
return { name: LOGIN, replace: true, query: { redirectFrom: route.fullPath } };
}
};
const redirectToHome = route => {
const DEFAULT = 'AUTHORS';
return { name: DEFAULT, replace: true };
};
Router.beforeEach((to, from) => {
const userIsAuthenticated = store.getters['authentication/userIsAuthenticated'];
const requiresAuth = to.matched.some((route) => route.meta && route.meta.requiresAuth);
if (!userIsAuthenticated && to.fullPath === '/') {
return redirectToLogin(to);
}
if (!userIsAuthenticated && requiresAuth) {
return redirectToLogin(to);
}
if (to.fullPath === '/') {
return redirectToHome(to);
}
return true;
});
Your question is rather old.
Your error is the "#click.prevent". This statement does not work, because your button is a submit button (so your event is called twice).
Use "#submit.prevent" should work (or change the type of your button).
My solution is a mix of extending prototype with check Navigation Duplicated Error. Other errors and warning should be visible. After a week on production - no NavigationDuplicated and everything is working.
import { equals } from 'ramda'
export function register(Vue) {
const routerPush = Router.prototype.push
const routerReplace = Router.prototype.push
const isNavigationDuplicated = (currentRoute, nextRoute) => {
const { name: nextName, params: nextParams = {}, query: nextQuery = {} } = nextRoute
const { name, params, query } = currentRoute
return equals(nextQuery, query) && equals(nextParams, params) && equals(nextName, name)
}
Router.prototype.push = function push(location) {
if (!isNavigationDuplicated(this.currentRoute, location)) {
return routerPush.call(this, location)
}
}
Router.prototype.replace = function replace(location) {
if (!isNavigationDuplicated(this.currentRoute, location)) {
return routerReplace.call(this, location)
}
}
Vue.use(Router)
}
I noticed that error comes up when I tried to replace url query parameter with same value.
I have select filters and url query string params are in sync with their values. It works well as long as you change to a new value. If value remains the same (for example coming back from history) and thus replacing query string parameter with same value, error pops out.
Solution was to check if value is changed, and then replace query param in router:
let newValue = 'foo'; // new query value for parameter
let qcopy = { ...this.$route.query }; // clone current query
// prevent NavigationDuplicated: Avoided redundant navigation to current location
if (qcopy['your_param'] != newValue){
qcopy['your_param'] = newValue;
this.$router.replace({query: qcopy});
}
Stop the click propagation from hitting the router action
I have a high-level answer here that may be useful to others. It may not directly answer the OP, but this thinking applies.
My take on this is to NOT fiddle around with global configuration or attempt to catch router logic. It doesn't make sense to pollute your app with exceptions all over the place.
In the case of a results view with filters. If the filters happen to be presented as router links (for various reasons), but we don't want actual router logic to occur when they're clicked.
So, capture the click before it gets to the router action!
Then you get the best of both worlds:
Search filters (as a link) that...
Do logic within a view
Still provide the benefit of being presented as a link
(benefits to bot scans, user convenience and accessibility, etc)
Technique:
Use #click.prevent on a child inside the router-link, to capture and stop the click from hitting the router.
Example:
Before:
Router logic occurs, even though we're in the route already
<router-link class="nav-link" :to="{name: 'MovieSection'}" >
<i class="fas fa-film"></i>Peliculas
</router-link>
After:
Router logic inhibited, we run other logic (applyFilter)
<router-link class="nav-link" :to="{name: 'MovieSection'}" >
<div #click.prevent="myFunc(something)">
<i class="fas fa-film"></i>Peliculas
</div>
</router-link>
With this approach you may avoid messy high-level exceptions in your app.
Best practice would be:
import { isNavigationFailure, NavigationFailureType } from 'vue-router/src/util/errors';
this.$router.push(...).catch((error) => {
if (!isNavigationFailure(error, NavigationFailureType.duplicated))
throw error
);
}
See the Vue Router docs
This worked for me in the router/index.js init. You can catch can handle the exception types or message you want in the call() method to ignore certain conditions or throw certain ones.
See
//HANDLE ROUTE ERRORS HERE
comment
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
Vue.use(VueRouter);
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default function(/* { store, ssrContext } */) {
const router = new VueRouter({
scrollBehavior: () => ({ x: 0, y: 0 }),
routes,
// Leave these as they are and change in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
mode: "history"
});
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
var token = window.localStorage.getItem("token");
if (token != null) {
var decodedtoken = JSON.parse(atob(token.split(".")[1]));
var role =
decodedtoken[
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
];
if (role != "Admin") {
next("/accessdenied");
} else {
next();
}
} else {
next("/login");
}
} else {
next();
}
if (to.meta.requiresLogin) {
var token = window.localStorage.getItem("token");
if (token != null) {
next();
} else {
next("/login");
}
}
});
//HANDLE ROUTE ERRORS HERE
const originalPush = router.push;
router.push = function push(location) {
return originalPush.call(this, location).catch(err => {
console.warn("router>index.js>push()", err);
});
};
return router;
}
Based on #Abhishek Shastri's answer
Here's is a simple and efficient solution:
if(from.fullPath === to.fullPath){
return
}
Edited --
Let's say I have an array of JSON objects:
"social":[
{
"name":"facebook",
"className":"fa fa-facebook"
},
{
"name":"linkedin",
"className":"fa fa-linkedin"
},
{
"name":"instagram",
"className":"fa fa-instagram"
},
{
"name":"github",
"className":"fa fa-github"
}
]
How do I create an snippet for each of the objects such that they return
<p>{social.name}<p>
And I don't want to use map.
This is generalized for a more complicated example, but this seems to be the problem I am facing (i.e. I have the data in the format below and I need to get the property from each of the elements to display and I only have one function)
Assuming that social is a part of the state, you can implement a method that maps each item in the social array to a p tag:
renderSocialNames = () => {
return this.state.social.map(
socialItem => <p key={socialItem.className}>{socialItem.name}</p>
);
}
Here's a Working Sample StackBlitz for your ref.
cleaner code :) , this might solve your issue
import React, { Component } from "react";
class Projects extends Component {
constructor(props) {
//initialize this component with props
super(props);
}
render() {
const { data } = this.props;
if (data) {
const projects = data.map(project => {
return (
<a className="cell" data-remodal-target={project.id}>
<img
className="grid-image"
src={project.cover}
data-aload={projects.cover}
alt={project.name}
/>
</a>
);
});
const modals = data.map(project => {
return (
<div className="remodal" data-remodal-id={project.id}>
<button
data-remodal-action="close"
className="remodal-close"
></button>
<h1>Remodal</h1>
<p>
Responsive, lightweight, fast, synchronized with CSS animations,
fully customizable modal window plugin with declarative
configuration and hash tracking.
</p>
<br />
<button data-remodal-action="cancel" className="remodal-cancel">
Cancel
</button>
<button data-remodal-action="confirm" className="remodal-confirm">
OK
</button>
</div>
);
});
}
return (
<section id="projects">
<div className="grid-container remodal-bg">
{projects}
{modals}
</div>
</section>
);
}
}
I'm trying all day to figure out how to pass data from one component to another. I have read a lot of relevant tutorials and instructions, unfortunately with out luck.
I have fetched some data from an api and i present them in the home.vue
and i want to pass the data into a new file to generate a page that will show a random product from the list.
Maybe the approach is totally wrong, but it is the first time that i use vue components, i have experience just with the instance
I'm trying to implement it using props to return the data to the new page.
Here is the randomize.vue file where I would like to pass my data
<template>
<div class="hello">
<p> {{ this.propsdata[0].e }} </p>
<h1>dla;dl;djal;d</h1>
</div>
</template>
<script>
export default {
name: "randomize",
props: ["propsdata"],
data() {
return {
obj: this.propsdata
};
},
mounted(){
console.log(this.props);
},
};
</script>
This is the home.vue file that i fetch the data
<template>
<div>
<div class=" main-conte" >
<randomize :propsData=toBeShown />
<transition-group name="fade" tag="div" id="container" class=" row " >
<figure v-for="(value,index) in toBeShownOrdered" id="page-wrap" :key="index" class="beer-container col-xs-6 col-sm-6 col-lg-4 col-xl-2" >
<a >
<img #click="goTodetail(value.id)" class="logo lazy img-responsive loaded" v-bind:src="getMissingImg(index)"/>
<figcaption>
<div class="beer-title">{{value.name}}</div>
<div class="beer-availability"> {{value.tagline}}</div>
<div class="learn-more">
<h4 class="beer-info-title">Format</h4>
<span class="serving-icons"></span>
<div class="serving">
<i v-if="value.abv >= 0 && value.abv <=6 " class="fas fa-wine-glass-alt"></i>
<i v-if="value.abv >= 6 && value.abv <=7" class="fas fa-glass-cheers"></i>
<i v-if="value.abv >= 7 && value.abv <=100" class="fas fa-wine-bottle"></i>
<span class="measure">{{value.abv}}</span>%</div>
</div>
</figcaption>
</a>
</figure>
</transition-group>
<div class=prev-next>
<button #click="orderByName = !orderByName">Click Me!</button>
<button class="prev" #click="prevPage" :disabled="currentPage==1">
<i class="fas fa-angle-double-left"></i></button>
<button class="next" #click="nextPage" :disabled="currentPage == totalPages">
<i class="fas fa-angle-double-right"></i> </button>
</div>
</div>
<div>
</div>
</div>
</template>
<script>
import { mdbView, mdbMask } from "mdbvue";
import FadeTransition from "./fade-transition.vue";
import randomize from "#/components/randomize";
export default {
name: "home",
components: {
mdbView,
mdbMask,
FadeTransition,
randomize
},
data() {
return {
items: [],
message: '',
currentPage: 1,
orderByName: false,
};
},
computed: {
//show more less products
toBeShown() {
return this.items.slice(0, this.currentPage * 5);
},
totalPages() {
return Math.ceil( this.items.length / 4);
},
toBeShownOrdered() {
return this.orderByName ? _.orderBy(this.toBeShown, 'name', 'asc') : this.toBeShown;
}
},
mounted() {
this.fetchData();
},
methods: {
fetchData: function() {
const myRequest = new Request("https://api.punkapi.com/v2/beers");
fetch(myRequest)
.then(response => {
return response.json();
})
.then(data => {
this.items = data;
console.log(this.items);
})
.catch(error => {
console.log(error);
});
},
getMissingImg(index) {
return this.images[index];
},
nextPage(){
if(this.currentPage < this.totalPages) this.currentPage++;
},
prevPage(){
this.currentPage = this.currentPage - 1 || 1;
},
goTodetail(prodId) {
let proId=prodId
this.$router.push({name:'blog',params:{Pid:proId}})
},
index.js
import Vue from 'vue'
import Router from 'vue-router'
import home from '#/components/home'
import blog from '#/components/blog'
import randomize from '#/components/randomize'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: home,
props:true
},
{
path: '/blog/:Pid',
name: 'blog',
component: blog,
props:true
},
{
path: '/randomize/',
name: 'randomize',
component: randomize,
props:true
},
]
})
You would benefit from using vuex as it will keep your state at the application level (as opposed to component data which keeps each component state at the component level).
Setting up vuex requires a bit more work and has a learning curve, but unless you won't grow your app to a medium/large size it will in the long term benefit you by decreasing the overall complexity of your app.
In short, all components from your app can access the state stored in vuex without having to deal with props. And from any component, you can dispatch actions implemented in your vuex store to alter the vuex state. Vuex will help keeping your components focused on presenting data and capturing user actions.
To ease setting up a Vue app with vue-router and vuex, you could choose to build your app with nuxt.js which is a framework that provides you with vue+vue-router+vuex with no effort. Nuxt.js will also help setting up server side rendering which would be beneficial to SEO if your app is to be indexed by search engines.
I have made cricket web app using ReactJS, NodeJS, MongoDB. On frontend I am displaying data from JSON objects per page I want to display 16 JSON objects. I am using .slice(0,16) method to slice JSON objects from REST API which returns 577 objects. Now how can I implement pagination for page 2,3,4,5.
For page 2 -> slice(17,33)
For page 3 -> slice(34,49)
and so on...
Below is my code and screenshot of my webapp:
in content.js :
import React, { Component } from 'react';
import './content.css';
class Content extends Component {
constructor(props){
super(props);
this.state = {
matches:[],
loading:true
};
}
componentDidMount(){
fetch('api/matches')
.then(res => res.json())
.then(res => {
console.log(res)
this.setState({
matches:res.slice(0,16),
loading:false
})
})
}
renderMatches() {
return this.state.matches.map(match => {
return (
<div class="col-lg-3">
<div id="content">
<p class="match">MATCH {match.id}</p>
<h4>{match.team1}</h4>
<p>VS</p>
<h4>{match.team2}</h4>
<div class="winner">
<h3>WINNER</h3>
<h4>{match.winner}</h4>
</div>
<div class="stats">
<button type="button" class="btn btn-success">View Stats</button>
</div>
</div>
</div>
);
})
}
render() {
if (this.state.loading) {
return <div>>Loading...</div>
}
return (
<div>
<div class="row">
{this.renderMatches()}
</div>
</div>
);
}
}
export default Content;
In Pagination.js :
import React, { Component } from 'react';
class Pagination extends Component {
render() {
return (
<div>
<div class="container">
<h2>Pagination</h2>
<p>The .pagination class provides pagination links:</p>
<ul class="pagination">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
</div>
</div>
);
}
}
export default Pagination;
Screenshot for more clarity :
enter image description here
How about store all the matches in the component state and only slice during rendering, based on the page query string?
In componentDidMount(), after fetching the data, simply set the state with all the matches:
this.setState({
matches: res,
})
Then, given you want to control the pagination with a query string in the url, like:
?page=2
You can filter the matches in render():
const RECORDS_PER_PAGE = 16;
const page = parseInt(getPageNumber(), 10) || 1; // defaults to 1 if no page query specified
const offset = (page - 1) * RECORDS_PER_PAGE
// do your mapping (do this in your renderMapping() method)
const matchComponents = this.state.matches.slice(0 + offset, RECORDS_PER_PAGE + offset).map(/* ... */);
where getPageNumber() is a method that gives you the query string value for page:
// Use your own query string parser if you wish
getPageNumber() {
return window.location.search // <-- gives you access to the query parameters
.slice(1) // remove the `?`
.split("&")
.reduce((queryHash, queryParam) => {
// e.g. ?page=3 becomes ["page", "3"]
const query = queryParam.split('=');
return Object.assign({}, queryHash, ({
[query[0]]: query[1] || null
}));
}, {}).page;
}