In a Next.js app (full-featured, not next export) that uses React Context for state management and the file-system based router, how can you implement advanced routing?
I want to have preconditions for certain pages, so for instance if you try to load /foo but the Context doesn't have a given property set correctly, it'll route you to /bar.
The actual logic is complex and varies by page, so I'm looking for an approach that's easy to maintain.
Note that these preconditions are not authorization-related, so they do not need to be enforced server-side. It's more like "you need to fill out this form before you can go here."
The use of Context imposes some constraints:
Context must be accessed in a React component or in a custom Hook
Using a custom server for routing is not an option, as that would lose the Context - it has to use client-side routing
The current Context has to be checked (I tried decorating useRouter, but if the Context was changed right before router.push, the custom Hook saw the old values)
Update: It's also good to avoid a flash when the page loads before rerouting happens, so a side goal is to return a loading indicator component in that case.
I believe you can create a HOC and wrapped every pages with you HOC that takes arguments e.g. { redirects: '/foo' }
// pages/bar.tsx
const Page = () => {...}
export default RouteHOC({ redirects: '/foo' })(Page)
the HOC file will be something like this
// hoc/RouteHOC.tsx
const RouteHOC = ({ redirects }) => (WrappedComponent) => {
// you can do your logic here with the context.. even filling up a form here
// too also can.. (like returning a modal first before the real Component).
// useEffect work here too..
const { replace } = useRouter()
// then after you want to replace the url with other page
replace(redirects)
return WrappedComponent
}
This is pretty okay to be maintainable I think. You just create all the logic in HOC and when you want to update the logic - you just have to edit it in 1 file.
Well this is one option I can think of when reading your question - sorry if I misunderstood it in any way. There will always be a better way out there as we all know we can improve and adapt to new situation every seconds :D. Cheers 🥂!!
You can do this.
const Component = () => {
const example = useExample()
return <div id='routes'>
<a href='/example1'>Example 1</a>
{example.whatever && <a href='/example2'>Example 1</a>}
</div>
}
Related
I'm new to Next.js and I'm using it to perform server side rendering on the landing page.
The landing page has: 1 generic component that's the same to every user and 1 component that is specific for each user.
Is it possible to perform server side rendering on the generic component, and client side rendering on the specific one?
Thank you.
Yes, you can do client-rendering for any component in your hierarchy. Client rendering usually means that when the component first renders, it fires off some asynchronous request for data (from an API, etc).
In your SSR page, just have your user-specific component not render anything on the initial render (except maybe some loading UI). Then include a useEffect hook that triggers the API call and sets state (local or global state as appropriate) which will trigger your component to re-render with the user-specific data.
During SSR, only the loading state will render. As soon as the component is mounted, the useEffect will trigger and the user-specific data will load and the component will re-render.
Overly simplistic example:
const UserGreeting = () => {
const [name, setName] = setState();
useEffect(() => {
getUserNameAsync().then((data) => {
setName(data.name);
})
}, [setName])
if (!name) {
return <div>...</div>
}
return (
<div>Welcome, {name}</div>
)
}
To make a page both dynamic and static at the same time is possible.
the solution for dynamic: you have to use react useState then useEffect to send the request after unloading fishing on the client side
but first must use next.js api getStaticProps() make the page static user's first visit
I'm using CodeceptJS and I'm trying to write a custom helper that asserts an text and clicks "OK". This dialog pops up as a iframe modal to consent with cookies.
If I write following steps in my scenario
I.amOnPage('/some-path');
within({frame: '#iframeID'}, () => {
I.see('Headline text for dialog');
I.click('OK');
});
// ...
...my test seems to work just fine.
But when I make an custom helper out of that and configure it properly so I can use it:
const { Helper } = codeceptjs;
class CookieConsent extends Helper {
consentWithCookies() {
const { Puppeteer } = this.helpers;
within({frame: '#iframeID'}, () => {
Puppeteer.see('Headline text for dialog');
Puppeteer.click('OK');
});
}
}
module.exports = CookieConsent;
...and use it as a step:
I.amOnPage('/some-path');
I.consentWithCookies();
// ...
...it doesn't seem to work as the consent dialog doesn't get clicked away as it was when implementing this directly in the scenario. According to some console.log() debugging the within callback doesn't get called at all. Console doesn't throw any errors about undefined within or anything suspicious.
I suspect that using within in a custom helper isn't working or I'm doing something wrong that I can't figure out from the documentation.
This warning at documentation doesn't really clarify when within is being used incorrectly, and using await doesn't help the problem.
within can cause problems when used incorrectly. If you see a weird behavior of a test try to refactor it to not use within. It is recommended to keep within for simplest cases when possible. Since within returns a Promise, it may be necessary to await the result even when you're not intending to use the return value.
iFrames can be a pain to work without when it comes down to automation. There are a number of factors that can make an iFrame unreachable to a framework such as cross-domain iFrames, commonly used for increased security on the content served.
Now to fix your issue, all you have to do is use switchTo() - Docs in CodeceptJS which is a function available for all helpers made available. The order should be
I.switchTo('your iframe');
..... some actions here;
I.switchTo(); // You do this so that you get out of the iFrame context when done
What is best practice to change content on a page without creating a route?
BlazeLayout.render('mainLayout', { top: 'header', menu: 'menu', main: 'dashboard', bottom: 'footer' });
How can i hide/show template components inside the dashboard without creating a new route? Should this be done in helpers using some sort of if/else logic in the html and using helper for on button click? Let's say i want to show different content inside dashboard template based on button clicks (href).
Please provide a best practice and good solution that is easy with lots of components.
How can i hide/show template components inside the dashboard without
creating a new route? Should this be done in helpers using some sort
of if/else logic in the html and using helper for on button click?
You can do that but you should be aware of some points to keep your code clean and modular:
Try to wrap parts of your dashboard into own templates to keep the code clean
Use ReactiveDict in favor of many ReactiveVar instances
Wrap recurring parts in templates, too to reduce duplicate code
Register recurring helpers globally or in the most upper template of your Dashboard
Subscribe on the parent template to data that is shared across all parts of the dashboard and subscribe to local data in the respective components
Use autorun and subscription.ready() and display a loading indicator until the subscription is ready. Don't wait to have everything loaded before rendering as this may reduce the UX dramatically.
Let's say i want to show different content inside dashboard template
based on button clicks (href).
You can attach a data attribute to the button, that has a specific id of the target to be toggled:
<template name="dashboardComponent">
<a href class="toggleButton" data-target="targetId">My Link</a>
</template>
You can then read this id and toggle it's state in your ReactiveDict:
Template.dashboardComponent.events({
'click .toggleButton'(event, templateInstance) {
event.preventDefault();
// get the value of 'data-target'
const targetId = $(event.currentTarget).attr('data-target');
// get the current toggle state of target by targetId
const toggleState = templateInstance.state.get( targetId );
// toggle the state of target by targetId
templateInstance.state.set( targetId, !toggleState );
}
});
In your template you can then ask to render by simple if / else:
<template name="dashboardComponent">
<a href class="toggleButton" data-target="targetId">My Link</a>
{{#if visible 'targetId'}}
<div>target is visible</div>
{{/if}}
</template>
And your helper is returning the state:
Template.dashboardComponent.helpers({
visible(targetName) {
return Template.instance().state.get(targetName);
 }
});
There could be the problem of sharing the state between parent and child templates and I suggest you to avoid Session where possible. However as beginner it is a lot easier to first use Session and then work towards a more decoupled (parameterized templates) solution step by step.
Please provide a best practice and good solution that is easy with
lots of components.
This is a high demand and it is your competency to work towards both! However here is a short peek into this:
Best practice is what works for you plus can work for others in other use cases. Try to share your work with others to see where it will fail for their use case.
Using routes has the advantage, that you can use query parameters to save the current view state in the url. That adds the advantage, that on reloading the page or sharing via link, the page state can be fully restored.
easy with lots of components is a contradiction and I don't know if you expect some magical puff that solves this complexity for you. As a software engineer it is your competency to abstract the complexity into smaller pieces until you can solve the problem within certain boundaries.
I'm currently getting used to using FlowRouter after a while using Iron Router and trying to set up some best practices. I'm subscribing to my collection at a template level.
Previously I've waited for a template to render using onRendered and then targeted my input field and applied focus(), however I am now trying to only show my template in Blaze when the subscriptions are ready using the following (please excuse the Jade but I think it's pretty clear in this case)
template(name="subjectNew")
unless Template.subscriptionsReady
+spinner
else
form
input(type="text" name="name")
So the basic idea is that until the subscriptions are ready the spinner shows. The issue I'm having is that now even when the template renders, the focus won't apply. I've tried various methods of wrapping it in an autorun call but not sure the best way of trying to target the first field when combined with this approach?
Template.subjectNew.onRendered(function() {
console.log('rendered');
$('input').first().focus();
});
Is it possible?
Many thanks for any ideas.
Your subjectNew is considered rendered even when it is only showing the spinner. Just stick your:
form
input(type="text" name="name")
Into a separate template and then attach your focus code to the onRendered handler of that other template.
template(name="subjectNew")
unless Template.subscriptionsReady
+spinner
else
+myForm
template(name="myForm")
form
input(type="text" name="name")
js:
Template.myForm.onRendered(function(){
$('input').focus()
});
I think using an autorun would be a good approach but then you would have to employ Tracker.afterFlush() to wait to set the focus after the form is rendered.
Something like:
Template.subjectNew.onRendered(function() {
this.autorun(() => {
if (this.subscriptionsReady()) {
Tracker.afterFlush(() => $('input').first().focus());
}
});
});
If I want to use Google analytics and at the same time make my site SEO friendly in React, what is the best way?
At the moment I use react-router and a flux pattern where I change the route and then in componentDidMount I fire an action that loads my data through ajax and then updates the store which emits the change and finally I re-render the components that are affected. During the ajax loading I dispatch an event so that my store knows ajax is loading and render a spinner in my component.
I have noticed that when I send my tracking data to Google the ajax has not finished loading and I only send the new page URL not the title or any other data which I load through ajax (I guess this is bad from an SEO perspective and it's definitely bad for my GA data).
This is my GA setup (I use react-ga):
Router.run(routes, Router.HistoryLocation, function(Handler, state) {
ga.pageview(state.pathname);
React.render(<Handler />, document.body);
});
Typical component setup (which allows me to render the correct data based on the URL):
componentDidMount: function() {
ItemStore.addChangeListener(this._onChange);
if(itemSlug) {
ItemActions.loadItemBySlug(this.props.slug);
}
}
I want to be able to have a single point of GA tracking if that is possible. I also want the SEO handling to be correct for the page:
Page title
OG data
H1
etc ...
What is the best approach to solve this?
Should I use the willTransitionTo option in react-router for this? (is it possible to use a loading spinner if I opt for this solution?)
statics: {
willTransitionTo: function (transition, params, query, callback) {
// LOAD AJAX HERE ?
callback();
}
}
How would I go about the willTransistionTo solution in a proper way, can't seem to find any good examples that relate?
Should I add more code, or is it clear what I'm trying to achieve?
Trigger the ga.pageview after the store gets the new data and renders.
The appropriate way to handle SEO in react is to render at the server side isomorphically with React.renderToString, so that any search engine will see the page in its complete state, rather than just Google's spider and only on those occasions when you manually call it.
This was one of the original design criteria of React.