Vue.JS - Both 'history' and 'abstract' router? - javascript

I am creating a VueJS app in which the user fills out a 5-step form.
These steps are routed to /step-1 through /step-5 in the Vue Router. However, I would like the site to return to the index page (/) when refreshing the page.
I could use abstract mode for this – but the result page is generated from the following url: /result/:userid in which I need the state to be history in order to be able to get the userid from the URL (and then do a post request to the server).
I also want this URL to be accessible even after finishing the form, so abstract here is not an option unfortunately.
So – is it possible to use both modes? Refresh the page to index.html when refreshing the form-pages, but then use history mode to render the result?

You cannot do this. It is either history or abstract but not both. Having said this, there are a couple of things you can do.
Approach 1: Use history mode with steps as query params
So instead of having routes like /step-1 or /step-2, use then as part of query params. So you will have routes like:
Index route: example.com/?step=1, example.com/?step=2
Result route: example.com/result/:userId
Approach 2: Use abstract mode with higher order component
Here, you will have a router with abstract but it will only serve as a state router and won't help with any browser URL manipulation.
Build a higher order component like AppComponent where you will have your own regular expressions to determine the route. It would look like:
// Should match route /result/:userId
const IS_RESULT = new RegExp('/result/(\\d+)$');
// User RegExp groups
const IS_STEP = new RegExp('/step-(\\d+)$');
export default class AppComponent extends Vue {
// Use Vue.js created hook
created() {
const path = window.location.pathname;
// Top level routing of the application
if (IS_STEP.test(path)) {
// Do something. You are in step-1/step-2/etc.
} if (IS_RESULT.test(path)) {
// Do something. You are in /result/:userId
// Get userId
const groups = IS_RESULT.exec(path);
const userId = groups[1];
} else {
// Goto Error page
throw new Error('Unknown route');
}
}
}
Approach 3: Use Multi-page SPA
Here, you will create two single page application. The first app will have routes /step-1, /step-2, etc. You will use abstract mode for this. The second application will have /result/:userId route with history mode.
In this architecture, when a user is on step-5, instead of pushing a new state to the router, you will use HTML5 history API to change the router and then cause a forced page refresh. Also, there are other ways you achieve this.

You can simply have a redirect in native javascript where you call it window.location.href('yourHomePage.com') and it will do a full refresh.

Related

React Router v5 accompanied with Code Splitting, and Data Prefetching with the use of Server Side Rendering

I have a project where use react-router v3 only for one reason. The reason is the need of server side rendering with data prefetching and the most convenient way to do this is to keep centralized route config in an object or array and loop over the matching elements to fetch the data from the API on the server side. The data later is going to be passed to the client with the response HTML and stored in variable of JSON format string.
Also application uses code splitting, however with the use of babel-plugin-transform-ensure-ignore on sever side I can directly fetch the components instead of lazy loading and the native import method will be used only on client side.
Nevertheless, above-mentioned structure isn't working with react-router v5, as it's little bit difficult, since I can't use #loadable/components, as react-router official documentation suggests. Per my observation #loadable/components just generates the HTML on the server side instead of giving me the components in which I implement the fetch method responsible for server side logic.
Therefore, I would like to ask you the good structure for webpack + react-router v5 + ssr + data prefetch + redux + code splitting
I see it's quite complicated and no universal solution, however I may be wrong.
Any direction or suggestion is appreciated.
I have never tried #loadable/components, but I do similar stuff (SSR + code splitting + data pre-fetching) with a custom implementation of code splitting, and I believe you should change your data pre-fetching approach.
If I got you right, your problem is that you are trying to intervene into the normal React rendering process, deducing in advance what components will be used in your render, and thus which data should be pre-fetched. Such intervention / deduction is just not a part of React API, and although I saw different people use some undocumented internal React stuff to achieve it, it all fragile in long term run, and prone to issues like you have.
I believe, a much better bullet-proof approach is to perform SSR as a few normal rendering passes, collecting in each pass the list list of data to be pre-fetch, fetching them, and then repeating the render from the very beginning with updated state. I am struggling to come up with a clear explanation, but let me try with such example.
Say, a component <A> somewhere in your app tree depends on async-fetched data, which are supposed to be stored at some.path of your Redux store. Consider this:
Say you start with empty Redux store, and you also have you SSR context (for that you may reuse StaticRouter's context, or create a separate one with React's Context API).
You do the very basic SSR of entire app with ReactDOMServer.renderToString(..).
When the renderer arrives to render the component <A> somewhere in your app's tree, no mater whether it is code-splitted, or not, if everything is set up correctly, that component will have access both to Redux store, and to the SSR context. So, if <A> sees the current rendering happens at the server, and there is no data pre-fetched to some.path of Redux store, <A> will save into SSR context "a request to load those data", and renders some placeholder (or whatever makes sense to render without having those data pre-fetched). By the "request to load those data" I mean, the <A> can actually fire an async function which will fetch the data, and push corresponding data promise to a dedicated array in context.
Once ReactDOMServer.renderToString(..) completes you'll have: a current version of rendered HTML markup, and an array of data fetching promises collected in SSR context object. Here you do one of the following:
If there was no promises collected into SSR context, then your rendered HTML markup is final, and you can send it to the client, along with the Redux store content;
If there are pending promises, but SSR already takes too long (counting from (1)) you still can send the current HTML and current Redux store content, and just rely on the client side to fetch any missing data, and finish the render (thus compromising between server latency, and SSR completeness).
If you can wait, you wait for all pending promises; add all fetched data to the correct locations of your Redux store; reset SSR context; and then go back to (2), repeating the render from the begining, but with updated Redux store content.
You should see, if implemented correctly, it will work great with any number of different components relying on async data, no matter whether they are nested, and how exactly you implemented code-splitting, routing, etc. There is some overhead of repeated render passes, but I believe it is acceptable.
A small code example, based on pieces of code I use:
SSR loop (original code):
const ssrContext = {
// That's the initial content of "Global State". I use a custom library
// to manage it with Context API; but similar stuff can be done with Redux.
state: {},
};
let markup;
const ssrStart = Date.now();
for (let round = 0; round < options.maxSsrRounds; ++round) {
// These resets are not in my original code, as they are done in my global
// state management library.
ssrContext.dirty = false;
ssrContext.pending = [];
markup = ReactDOM.renderToString((
// With Redux, you'll have Redux store provider here.
<GlobalStateProvider
initialState={ssrContext.state}
ssrContext={ssrContext}
>
<StaticRouter
context={ssrContext}
location={req.url}
>
<App />
</StaticRouter>
</GlobalStateProvider>
));
if (!ssrContext.dirty) break;
const timeout = options.ssrTimeout + ssrStart - Date.now();
const ok = timeout > 0 && await Promise.race([
Promise.allSettled(ssrContext.pending),
time.timer(timeout).then(() => false),
]);
if (!ok) break;
// Here you should take data resolved by "ssrContext.pending" promises,
// and place it into the correct paths of "ssrContext.state", before going
// to the next SSR iteration. In my case, my global state management library
// takes care of it, so I don't have to do it explicitly here.
}
// Here "ssrContext.state" should contain the Redux store content to send to
// the client side, and "markup" is the corresponding rendered HTML.
And the logic inside a component, which relies on async data, will be somewhat like this:
function Component() {
// Try to get necessary async from Redux store.
const data = useSelector(..);
// react-router does not provide a hook for accessing the context,
// and in my case I am getting it via my <GlobalStateProvider>, but
// one way or another it should not be a problem to get it.
const ssrContext = useSsrContext();
// No necessary data in Redux store.
if (!data) {
// We are at server.
if (ssrContext) {
ssrContext.dirty = true;
ssrContext.pending.push(
// A promise which resolves to the data we need here.
);
// We are at client-side.
} else {
// Dispatch an action to load data into Redux store,
// as appropriate for your setup.
}
}
return data ? (
// Return the complete component render, which requires "data"
// for rendering.
) : (
// Return an appropriate placeholder (e.g. a "loading" indicator).
);
}

Nextjs - first level dynamic route

I want to have user profiles in my app at domain.com/:username .How do I do it? creating a new folder in /pages will create a new URL section like /user/:username which I don't want.
Just name your file inside pages as [username].js
Creating a dynamic route
You can create a dynamic route by putting the filename in brackets. For instance:
pages/[id].js
pages/[slug].js
pages/posts/[slug].js
pages/[author]/[slug].js
pages/[author]/bio.js
Notice how the dynamic route can be at any level. It doesn't simply need to be at the root of the pages folder, and it doesn't need to be the last level of the url either. Also notice that you can name the dynamic route anything you want. You're not restricted to just using [id] or [slug].
In your case, use pages/[username].js
Accessing the route name
If you create a dynamic route at pages/posts/[slug].js, any page at https://example.com/posts/slug-of-the-post will lead to that page. However, you likely want different content on each page (i.e., that the user is on https://example.com/posts/slug-of-the-post, not https://example.com/posts/slug-of-a-different-post. For this, you need to access the contents of the route.
Inside the page
You can use the router to get the name of the route.
// pages/posts/[slug].js
import { router } from 'next/router'
export default function PostPage() {
const router = useRouter()
const slug = router.query.slug // "slug" because that was the name of the file
return <>{/* page contents */} </>
}
So on page https://example.com/posts/slug-of-the-post, the variable slug will be slug-of-the-post.
If you used [id].js, use router.query.id instead.
GetStaticProps/GetServerSideProps
Using the server-side functions, the process is very similar.
// pages/posts/[slug].js
// bottom of the file
export async function getServerSideProps(context) {
const slug = context.params.slug
return {
props: {},
}
}
Docs
By the way, I'd also recommend checking out the docs if you haven't already:
https://nextjs.org/docs/routing/dynamic-routes

Keep param consistent on location change ReactJs

I am using react router v4 to change location in ReactJs.
this.props.history.push("/profile");
<Link to="/profile" />
The above code works fine.
Now I want to keep a param consistent in URL http://localhost:3000?source=test by using the same code as above.
One approach is that I find all the occurrences in the code and add condition that if params source=test exist then append it to the the URL as well but this approach doesn't look fine to me as I have add condition on every redirect, Link and history.push
Second approach that I find is that use of listener on location update given by react router
In my Main Route file
class App extends Component {
componentDidMount() {
this.unlisten = this.props.history.listen((location, action) => {
if (/source=ep/.test(this.props.location.search)) {
location.search = _startsWith(location.search, "?") ? location.search + "&source=test" : "?source=test"
}
});
}
}
With this approach I can easily append the params in search query of react router but the param doesn't show up in URL.
the URL looks like this http://localhost:3000/profile and When I get search params from react-router console.log(this.props.location.search) it shows the param source=test and it's exactly what I want but In this case if user refreshes on this page the search params lost from react-router as well because it's not in the URL.
Can you guys help me to keep source=test consistent even in URL.

Re-evaluate dynamic import on vue-route change

I am making an admin panel that is used to manage several websites. My vue-router routes are all prefixed with the website slug and I have a beforeEach on the router that sets the website_slug in the vuex state accordingly.
Each site will use most of the same components, but there will also be several sites that have custom page components.
For this case I have made a function called importOrDefault like this:
const importOrDefault = (componentName, store) => {
return import(/* webpackChunkName: "[request]" */ `./Pages/${store.getters.website.slug}/${componentName}`)
.catch(() => import(/* webpackChunkName: "[request]" */ `./Pages/_shared/${componentName}`));
};
Which I use in my router config like this:
{
path: '/:website_slug/',
name: 'dashboard',
component: () => importOrDefault('Dashboard', store)
}
This setup works great on load. However, when I navigate to another :website_slug, the component is not re-evaluated and the same component as the previous site is loaded. The state has changed though, so this is not the problem.
Is there a way that I can make the router component re-evaluate the function or am I missing some obvious functionality I can use for this?
Vue Router tries to reuse components as much as it can. Have you tried adding beforeRouteUpdate to your component to explicitly set the data (vs using mounted or something else)? You can also set the key property of the component to something explicit (like the website_slug value) to ensure the component isn't reused.
I personally prefer using key when I can.
See: https://github.com/vuejs/vue-router/issues/1490
beforeRouteUpdate method: https://jsfiddle.net/Linusborg/L7hscd8h/2208/
key method: https://jsfiddle.net/Linusborg/L7hscd8h/1528/
I have tried several solutions, but couldn't get the route component to re-evaluate. Eventually, I loaded the available websites before bootstrapping Vue and generated all possible routes for all websites.
This way, the same page for another website is a unique route and the component is evaluated for every website.

How to use js code to forward reactjs-router?

when u seem to require forward in reactjs we used to put a Link or button ,and then consumer click it ,then the page reload corresponding path of router . Now,I want the page turn to other path of router when I request a post and the status of response is 200. It can't use a link or button ,could reactjs use code to do this? like : CertainComponent.setPath(otherPath); CertainComponent.turn;
My English is poor , it may be not clear to delive the function which i want to do.
Thanks!
I think you want to use this.context.router.push('/users/12')
https://github.com/reactjs/react-router/blob/master/docs/API.md#routercontext
First of all the component you are using has to be a container, which means you have to create it with connect() so it has access on store and dispatch:
let MyComponent = ({ dispatch, store }) => {
store.routing.push('url');
}
export default connect()(MyComponent)
And this depends how you named the router key on your store when you combined the routing reducer from redux-router on your store.
There is also another alternative, where you could do
import { hashHistory } from 'react-router';
hashHistory.push('next-url');

Categories

Resources