Single Page Web App that DOESN'T change url - javascript

OK, I know I'm going against conventional wisdom here, but here goes. I'm learning how to build a SPA using react and redux, and things are going well. I've incorporated react-router, because it seemed to be the accepted thing to do, but now I'm having issues. I want my SPA to act like a real app where the user will always come in at the landing page, and then go from there. As the user navigates from page to page, various things are fetched and stored. Some pages require authorization, for example, others don't. The problem is that when I use react router the URL changes at each route. So then, when the user comes back at a subsequent visit through say a bookmark, the app tries to jump to the page they left at, rather than at the entry point where I need them to go. This screws up a bunch of stuff underneath the hood and they get gobbledegook.
I've never really understood why it is conventional wisdom to "never break the back button" and always reflect everything the user does in the url. This seems like such a dogma, it's difficult to find ANY alternative ways of thinking. I have had times when I do a google search, for example, and go to a site where I click around for awhile, don't find what I'm looking for and then want to use the back button to go back to my google search results, but instead it goes click-click-click back through every step I made IN THAT SITE. Bleh. I eventually have to close that tab and start over. This happens to me all the time.
In mobile apps, the user is quite used to always arriving at a landing page, and clicking around from there. Why can't web apps act the same way?
So my question is: how would I implement that in a react-redux application? I have an internal "back" button that the user can click to go to a previous "page" within the app. I'm thinking I could keep the "history" in redux store, and write some goBack methods and such WHICH DON'T ALTER THE URL. Are there any examples that use this approach?
Any advice much appreciated.

As you said, it's probably not a good idea to go against the grain and create your own back button.
That said, if you don't want those features of react-router, it shouldn't be very difficult to implement your own router.
You could, for example, model your redux state with a history list,
{
history: ["home"],
home: { ... },
otherPage: { ... },
}
Then in your top-level render function just determine the page and render,
import HomeComponent from 'components/HomePage'
import OtherPageComponent from 'components/OtherPageComponent'
const pages = {
'home': HomeComponent,
'otherPage': OtherPageComponent
}
export class App extends React.Component {
...
render() {
const CurrentPage = this.props.history[this.props.history.length-1];
return (
<CurrentPage />
)
}
}
export default App;
You'll then need a reducer/action to change routes,
// reducer.js
export default function reducer(state, action) {
switch(action.type) {
case 'CHANGE_ROUTE':
return {
...state,
history: state.history.concat([action.route])
}
case 'GO_BACK':
return {
...state,
history: history.slice(0,-1)
}
default:
return state
}
}
// action.js
export const changeRoute = (route) => ({ type: 'CHANGE_ROUTE', route });
export const goBack = () => ({ type: 'GO_BACK'});
You could then change the route on any page by dispatching the relevant action,
// some-component.js
import { goBack, changeRoute } from 'action.js'
...
// in some render method
<button onClick={() => dispatch(goBack())}>
Go Back
</button>
<button onClick={() => dispatch(changeRoute('somePage'))}>
Go to some page
</button>
You would probably want to package the onClick stuff together in some kind of <Link> component like react-router.
Good luck!

There are two challenges with what you propose. First, is that without URL's, the user cannot bookmark their location. And second, the app is operating within a browser window and the browser window has a back button (and forward, and history).
I am working on a SPA that after the initial bootstrap only ever makes JSON requests through a RESTful api. This works beautifully but does require extra work to manage the page history. Without it users get frustrated that they are 'exited' from the application back to the login page when they hit the 'back' button in the browser.
The page history we maintain, however, is one we control and so it can be managed to the granularity we want. There are many api calls that don't change the URL. Unfortunately, with many frameworks they expect you to operate within a 'page' mindset, hopefully something that will change soon. (Personally, I am contemplating just disabling the back button so the user views it more as an application.)
I think the point that you are making, and with which I agree is that we are building applications and being constrained to the 'traditional' concept of pages makes the concept of "SINGLE page application" somewhat less than what it should be.

Related

Rerun nuxt route middleware if store getter changes without reloading the page?

Given following middleware, whats the best way to rerun the logic when ever store.getters.authenticated changes, and not only on the initial load.
middleware/auth.js
export default function ({ store, redirect }) {
if (!store.getters.authenticated) {
return redirect({ name: "login" })
}
}
You asked how to rerun it inside of the middleware itself, which is the 2nd part of a possible middleware trigger (like if the user is not authenticated anymore while staying on the same page) and without any specific action, like when using polling or websockets I thought.
Meanwhile, the 1st part is the easiest: call the middleware globally (in nuxt.config.js) to trigger it on each page navigation.
If he stays on the same page, you can also move with the router as you did but at this point, checking if there is an error in your axios is maybe more appropriate since it's the initiator.
I also do like to use this.$nuxt.refresh() to trigger all the checks when switching accounts globally, helps re-running all those tasty fetch() hooks.

Redirect existing website link to app expo react native

I have an app that has some payment sandbox. I am using "expo-linking" Linking.openURL(sandboxURL) to open the unique payment link on a browser everytime user wants to buy the product.
If the payment is cancelled the sandbox redirects to "https://mywebsitedomain/payment/cancel"
If the payment is successful the sandbox redirects to "https://mywebsitedomain/payment/successful"
What I want to achieve is I want to the browser to redirect to my app's payment cancel and payment successful page whenever "https://mywebsitedomain/payment/cancel" and "https://mywebsitedomain/payment/successful" links get triggered in the browser.
How do I do that? I got suggestions like using deeplinking. in such case I dont necessarily need to create any link, I just need the existing website link to be redirected to my app. In that case what would be the ideal configuration?
Thank you.
In this case I would recommend using WebView instead of Linking, because Linking doesn't really give you any kind of controls to the flow after user opens an URL in the browser, DeepLinking would theoretically work, but it would open the app from the splash screen and then you would have to navigate the user manually to the screen you want him to be, I don't think this is the proper solution though..
Instead I would suggest opening an URL in Webview and injecting the custom javascript code. In this way, you would keep the user in your app and maintain the control to the flow. react-native-webview which is deprecated module and should be added through package manager has onMessage and injectedJavaScript props, and using these could solve this issue.
As an Example :
Create separate screen which contains only webview, stretched to the full screen.
Import the screen in your stacknavigator with appropriate name.
When navigating to it, pass webview props as navigation params.
Afterwards:
const jsCode = `
document.addEventListener("click", () => {
window.ReactNativeWebView.postMessage(JSON.stringify({type: "click", message : "goBack"}))
})`;
const onMsg = (event) => {
const res = JSON.parse(event.nativeEvent.data);
if (res.message === "goBack") {
navigation.goBack();
}
}
<WebView
source={{ uri: params.uri, html: params?.html }}
androidHardwareAccelerationDisabled
javaScriptEnabled
injectedJavaScript={jsCode}
javaScriptCanOpenWindowsAutomatically
collapsable
onMessage={onMsg}
/>
this is an example code which closes the webview whenever the user clicks something in there, instead you could check window.location.href, post it as a message to your app and if it equals to https://mywebsitedomain/payment/cancel do something you want to do :)
UPDATE
as mentioned in the comments, for this concrete task webview offers onNavigationStateChange prop, which is meant to determine the navigation updates in the webview. This might be a better option, as some of the websites might not allow you to inject custom javascript code. But however, javascript injection is also possible and would also be one of the solutions here.
As it's a bit hard for explaining here, I've set up an example app here, which uses both of the approaches described above:
https://github.com/RocKer004/webview-example-app
App opens react native navigation docs, if you click on React Native Express (first option) it'll take you back and close the webview (this is handled using the onNavigationStateChange prop), if you click the last option there webview is also going to be closed, but this is handled by javscript injection this time. Otherwise, you're free to browse the web.
For more info, check :
https://github.com/react-native-webview/react-native-webview

Google Analytics on React JS site outputting incorrect data (broken sessions?)

We have a site with most of the content managed by Wordpress, however when the user navigates to search pages (user searches for a product), it's handled by React JS.
It's all on the same domain, so the user never knows that they are interfacing with two different applications.
Google Analytics on the site, however, doesn't seem to perceive sessions correctly. It's logging entrances (landing pages) to the site as search pages with rather long URLs:
There are thousands of landing pages like this, and the site is new, so there's no way this is all traffic is coming in from external links
Referrer path for all of these sessions is "(not set)"
Internal IP addresses are filtered
The traffic is coming from various sources/mediums, suggesting that sessions are somehow breaking (screenshot below)
Currently, GA is set up with GTM. I tried using this to fire the GTM tag in React.
Also attempted making the GA tag within GTM fire on browser history changes rather than page views (history changes fire when in React, normal page views in Wordpress). But the issue still persists with these modifications.
Note that these sessions are not specific to any one browser:
The issue you're experiencing comes from the fact upon search, you are switching your entry point and doing a hard refresh of your page to the React app. Even though the domain doesn't seem to change, it's still considered by the browser as a fresh page load and thus showing like so in your analytics, as shown by this request:
You haven't really told if you were using react-router in your app (I'm assuming you are given the different paths), a way to get around the problem would be to use react-ga instead of the default GA script and leverage the onUpdate callback of react-router.
First import and initialize the module with your GA id:
import ReactGA from 'react-ga'
ReactGA.initialize('UA-000000-01')
Then in your routes configuration, add the onUpdate property on the <Router> and create a method that will call the google analytics with only the pathname so you won't end up with all the query parameters that are quite obnoxious in the dashboard.
const onUpdate = () => {
ReactGA.set({ page: window.location.pathname })
ReactGA.pageview(window.location.pathname)
}
<Router onUpdate={onUpdate}>
...
</Router>
Still, if you want to keep track of the user search, I would recommend using events instead, and doing something like the following upon search:
ReactGA.event({
category: 'User',
action: 'Search',
value: 'your formatted search value'
})
It will also give you the ability to format the value of the search any way you want, which would be more readable for you than query parameters.

Backbone js prevent url hash change when back/forward

I am working on a very basic SPA using Backbone.js. My app has few routes. Among them there are 2 that give me issues: the index route ("/#index") and menu route ("/#mainmenu").
A simple workflow in my app is as follows: the user fills a form -> clicks to login -> trigger ajax request -> if login successful go to "/#mainmenu" route. if login failed, remain on "/#index" route.
On "/#mainmenu" if the user clicks on logout -> ajax request -> if logout success go to "/#index". if logout failed remain on "/#mainmenu".
The issues that I am struggling with are:
A clean way to trigger transition to "/#mainmenu" after successful login (I currently use router.navigate("mainmenu", {trigger: true}); but read that should avoid using this approach, in derrick bailey's article https://lostechies.com/derickbailey/2011/08/28/dont-execute-a-backbone-js-route-handler-from-your-code/ )
A clean way to prevent the user to go back to the "/#index" when pressing Back button in the browser from "/#mainmenu" route. I will also would like to preserve the url hash to reflect the current view.
Prevent the user to go forward to "/#mainmenu" after successful logout.
Is that even possible to prevent url hash change when clicking browsers back/forward buttons?
When I say "clean" I refer to "what are the best practices?". I partially solved some issues by saving url hashes and restore the appropriate hash (by router.navigate(currentRoute, {replace: true}); ) but I feel that it's a hacky approach.
Any feedback is welcome and much appreciated.
One way to solve this problem is by applying an async before filter on the routes that require an auth status check before the actual callback route is executed.
For example:
https://github.com/fantactuka/backbone-route-filter
The philosophy of avoiding {trigger: true} is based on the fact that when the router gets triggered with this flag, the entire initialization procedure for that route gets triggered. You will lose the benefit of having previously defined appstates because the app will have to re-initialize all content while this work had alrady been done before.
In practice, I think that it is useful to assess what your web app actually does. If losing appstate isn't an issue because the views you want to render are entirely new, then I don't see a problem with creating a client side redirect that re-inintializes your app.
If, on the other hand, your app has many views already rendered for which you want to maintain the same state as before, you can listen for an auth state event on each component that requires it, and make only those views re-render accordingly if they need to.
I don't think there's anything wrong with triggering routes, have been doing this without any issue for 2+ years. It all boils down to your requirements, read the article looks like a lot of work to me.
There are multiple ways to do this. First, you can disable back/forward buttons using window.history.forward(). Second, my favourite, is to do the processing in Router#execute. A sample might look like :
execute: function(callback, args, name) {
if (!loggedIn) {
goToLogin();
return false; //the privileged route won't trigger
}
if (callback) callback.apply(this, args);
}

Ember.js How can the URL represent every possible router state?

On the documentation for routing at emberjs.com, it states
Is the user currently logged in? Are they an admin user? What post are they looking at? Is the settings screen open? Are they editing the current post?
In Ember.js, each of the possible states in your application is represented by a URL.
I can't seem to understand: How is it supposed to happen to have every possible of the above states is represented by a URL?
Explanation:
As far as i can see, the Ember Router is a strictly hierarchical tree structure. That makes perfect sense to me for straightforward URLs, e.g.
user/:user_id/posts/:post_id/comments
which is the locator for the comments of a single post.
Now, where would the login state of the user go? Wouldn't that create multiple URLS for the same resource, or does that not matter?
If your settings screen is a modal that can be accessed from anywhere on the site, how would that be reflected in the url?
I'm not asking for a workaround solution, but just wanted to get some opinions of how this is actually meant in the guides or what the best practises are.
Great question Conrad.
I'll offer some experience from the multiple projects I've done in Ember.
Rarely do I add the user to the route, there's a good chance you aren't going to be sending down anything but the current logged in user (unless of course you're building an admin resource). So defining the user id in the url is probably incorrect, since it doesn't define the current page, it defines the user who was viewing that current page.
The login state would be unrelated to the other resource routes. It would live at the root, and after login you would redirect to an authorized route. In the event that someone navigates directly to an authorized route, there are some good patterns for pausing that transition, navigating to the login route, then upon a valid login redirecting to the authorized route.
Generally you have multiple routes for a single resource.
App.Router.map(function() {
this.resource('login');
this.resource('post', { path: '/post/:post_id' }, function() {
this.route('edit');
this.route('someViewWithTheSameResource');
this.route('someViewWithTheSameResource2');
this.resource('comments', function() {
this.route('new');
this.route('update');
});
});
});
The grand-daddy issue is modals. Originally that was just a don't handle it in the url, there is no nice way to handle this without having to add a million routes under different settings. With the upcoming change of query params you can modify settings at a root level without having to muck up the entire resource tree. This is still in beta, so it may not quite work as expected, but the goal is to be able to handle such a use case.
http://emberjs.com/guides/routing/query-params/

Categories