Next.js: Router.push with state - javascript

I'm using next.js for rebuilding an app for server side rendering.
I have a button that handles a search request.
In the old app, the handler was this one:
search = (event) => {
event.preventDefault();
history.push({
pathname: '/results',
state: {
pattern: this.state.searchText,
}
});
}
In the results class, I could get the state date with this.props.location.state.pattern.
So now I'm using next.js:
import Router, { withRouter } from 'next/router'
performSearch = (event) => {
event.preventDefault();
Router.push({ pathname: '/results', state: { pattern: this.state.searchText } });
};
In the results class, I use
static async getInitialProps({req}) {
return req.params;
}
I'm not sure if I have to add this to my server.js:
server.get('/results', (req, res) => {
return app.render(req, res, '/results', req.params)
})
However, the function getInitialProps throws an error because req is undefined. Long text, short question: how to pass state or params to another page without using GET parameters?

In next.js you can pass query parameters like this
Router.push({
pathname: '/about',
query: { name: 'Someone' }
})
and then in your next page (here in /about page), retrieve the query via the router props, which needs to be injected to Component by using withRouter.
import { withRouter } from 'next/router'
class About extends React.Component {
// your Component implementation
// retrieve them like this
// this.props.router.query.name
}
export default withRouter(About)

If you want your url remain clean, make a small addition to Prithwee Das's answer like below.
Router.push({
pathname: '/about',
query: { name: 'Someone' }
}, '/about');
Now you can access props in your component using props
...
const YourComponent = (props) => {
useEffect(() => {
console.log(props.router.query.name);
}, [props.router.query]);
return (
<React.Fragment>
...
</React.Fragment>
);
};
...

I don't know whether this supports SSR, but I had to do it as follows to avoid the error cannot read property 'query' of undefined.
This uses useRouter hook to get access to the url, imported as below.
import { useRouter } from 'next/router'
Assume you want to pass data {name:'Someone'} from Component A to Component B.
In Component A,
const router = useRouter();
router.push(
{ pathname: "/path_of_component_b", query: { name: "Someone" } },
"path_of_component_b"
);
In Component B,
const router = useRouter();
useEffect(() => {
alert(router.query.name); // Alerts 'Someone'
}, [router.query]);

If you want 'clean' urls, one way to go about it is to add onClick handler to your link and store required information in context/redux store. It easy to implement if you already have one.
<Link href='...'>
<a onClick={()=>{dispatch(....)}}>Link<a/>
<Link>

Related

How to combine react-router-dom's setSearchParams with route that has a param in a way that allows correct back button usage?

I have a set of routes that look like this:
{
path: '/things',
element: <MainLayout />,
children: [
{
path: 'search',
element: <MainSearch />,
},
{
path: 'search/:thingId',
element: <ThingLayout />,
},
{
path: '',
element: <Navigate to="search" replace />,
},
],
},
Wherein the idea is you search for something on MainSearch, are given results, click it, and are brought to a page /things/search/:thingId for that Thing.
However, ThingLayout has a tab paradigm happening too, which is set by search params, and the user clicking tabs within that component.
ThingLayout.tsx
import { useParams, useSearchParams } from 'react-router-dom';
const ThingLayout = () => {
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get('tab');
// When the user first comes here from general search route,
// set the default tab
useEffect(() => {
if (!activeTab) {
setSearchParams({ tab: DEFAULT_TAB });
}
}, []);
// ...
}
My trouble is, I need (I believe) the generalized /search/:thingId route so that all my various /search/:thingId?tab=someTab routes resolve to this component, which then has code to check which tab is set via searchParams and then render the proper child component, but if a user presses the back button, the URL changes to /search/:thingId and then immediately back to /search/:thingId?tab=defaultTab. I tried using navigate rather than setSearchParams to change the URL:
import { useNavigate } from 'react-router-dom';
// ...
const navigate = useNavigate();
navigate(`?tab=${DEFAULT_INVENTORY_PARTS_TAB}`)
But I had the same issue: when coming from search, first the URL would be /search/:thingId, then it would become ?tab=defaultTab.
I've searched through the react router docs, as well as looked at a great many stackoverflow questions, and I'm thinking now maybe I just am following a bad pattern? Is my method of tab navigation compatible with the "right" way to use React Router? How can I combine the general :id route with my manipulated searchParams route?
My react router version is "react-router-dom": "^6.2.1"
The setSearchParams works similar to the navigate function but only for the URL queryString. It takes an options object that is the same type argument as navigate.
declare function useSearchParams(
defaultInit?: URLSearchParamsInit
): [URLSearchParams, SetURLSearchParams];
type ParamKeyValuePair = [string, string];
type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams;
type SetURLSearchParams = (
nextInit?: URLSearchParamsInit,
navigateOpts?: : { replace?: boolean; state?: any }
) => void;
Pass a navigateOpts object that sets the navigation type to be a redirect.
import { useParams, useSearchParams } from 'react-router-dom';
const ThingLayout = () => {
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get('tab');
// When the user first comes here from general search route,
// set the default tab
useEffect(() => {
if (!activeTab) {
searchParams.set("tab", DEFAULT_TAB);
setSearchParams(searchParams, { replace: true });
}
}, []);
// ...
}
The answer is to use the navigate function provided by react-router-dom's useNavigate hook, which has a replace parameter that can be passed to it, to replace the current item in history.
So my code that checks to see if there is a tab selected, and if not, sets the search parameter, now navigates to the URL with the proper search parameter set, rather than setting the search parameter directly and only.
import { useParams, useSearchParams } from 'react-router-dom';
const ThingLayout = () => {
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get('tab');
// When the user first comes here from general search route,
// set the default tab
useEffect(() => {
if (!activeTab) {
setSearchParams({ tab: DEFAULT_TAB });
}
}, []);
// ...
}
becomes
import { useSearchParams, useNavigate } from 'react-router-dom';
const ThingLayout = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const activeTab = searchParams.get('tab');
// When the user first comes here from general search route,
// set the default tab
useEffect(() => {
if (!activeTab) {
navigate(`?tab=${DEFAULT_TAB}`, {replace: true});
}
}, []);
// ...
}

Using client-only routes with page templates coming from Contentful

Goal
I am looking to use client-only routes for content under a certain URL (/dashboard). Some of this content will be coming from Contentful and using a page template. An example of this route would be {MYDOMAIN}/dashboard/{SLUG_FROM_CONTENTFUL}. The purpose of this is to ensure projects I have worked on at an agency are not able to be crawled/accessed and are only visible to 'employers' once logged in.
What I have tried
My pages are generated via gatsby-node.js. The way of adding authentication/client-only routes has been taken from this example. Now the basics of it have been setup and working fine, from what I can tell. But the private routes seem to only work in the following cases:
If I'm logged in and navigate to /dashboard
I'm shown Profile.js
If I an not logged in and go to /dashboard
I'm shown Login.js
So that all seems to be fine. The issue comes about when I go to /dashboard/url-from-contentful and I am not logged in. I am served the page instead of being sent to /dashboard/login.
exports.createPages = async ({graphql, actions}) => {
const { createPage } = actions;
const { data } = await graphql(`
query {
agency: allContentfulAgency {
edges {
node {
slug
}
}
}
}
`);
data.agency.edges.forEach(({ node }) => {
createPage({
path: `dashboard/${node.slug}`,
component: path.resolve("src/templates/agency-template.js"),
context: {
slug: node.slug,
},
});
});
}
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions;
if(page.path.match(/^\/dashboard/)) {
page.matchPath = "/dashboard/*";
createPage(page);
}
};
My auth.js is setup (the username and password are basic as I am still only developing this locally):
export const isBrowser = () => typeof window !== "undefined";
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {};
const setUser = (user) =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user));
export const handleLogin = ({ username, password }) => {
if (username === `john` && password === `pass`) {
return setUser({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
});
}
return false;
};
export const isLoggedIn = () => {
const user = getUser();
return !!user.username;
};
export const logout = (callback) => {
setUser({});
call
};
PrivateRoute.js is setup the following way:
import React from "react";
import { navigate } from "gatsby";
import { isLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/dashboard/login`) {
navigate("/dashboard/login");
return null;
}
return <Component {...rest} />;
};
export default PrivateRoute;
dashboard.js has the following. The line <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />, I have tried a couple of things here - Statically typing the route and using the exact prop, using route parameters such as /:id, /:path, /:slug :
import React from "react";
import { Router } from "#reach/router";
import Layout from "../components/Layout";
import Profile from "../components/Profile";
import Login from "../components/Login";
import PrivateRoute from "../components/PrivateRoute";
import Agency from "../templates/agency-template";
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />
<PrivateRoute path="/dashboard/profile" component={Profile} />
<PrivateRoute path="/dashboard" />
<Login path="/dashboard/login" />
</Router>
</Layout>
);
export default App;
And finally agency-template.js
import React from "react";
import { graphql, Link } from "gatsby";
import styled from "styled-components";
import SEO from "../components/SEO";
import Layout from "../components/Layout";
import Gallery from "../components/Gallery";
import GeneralContent from "../components/GeneralContent/GeneralContent";
const agencyTemplate = ({ data }) => {
const {
name,
excerpt,
richDescription,
richDescription: { raw },
images,
technology,
website,
} = data.agency;
const [mainImage, ...projectImages] = images;
return (
<>
<SEO title={name} description={excerpt} />
<Layout>
<div className="container__body">
<GeneralContent title={name} />
<Gallery mainImage={mainImage} />
<GeneralContent title="Project Details" content={richDescription} />
<div className="standard__images">
<Gallery projectImages={projectImages} />
</div>
<ViewWebsite>
<Link className="btn" to={website}>
View the website
</Link>
</ViewWebsite>
</div>
</Layout>
</>
);
};
export const query = graphql`
query ($slug: String!) {
agency: contentfulAgency(slug: { eq: $slug }) {
name
excerpt
technology
website
images {
description
gatsbyImageData(
layout: FULL_WIDTH
placeholder: TRACED_SVG
formats: [AUTO, WEBP]
quality: 90
)
}
richDescription {
raw
}
}
}
`;
export default agencyTemplate;
I assume that gating content from a CMS is possible with Gatsby but I might be wrong given it is an SSG. I may be misunderstanding the fundamentals of client-only. The concepts in React and using Gatsby are still very new to me so any help or guidance in achieving the goal would be appreciated.
What I ended up doing
So the answer I marked was the one that 'got the ball rolling'. The explanation of what was happening with state and requiring either useContext or redux helped me understand where I was going wrong.
Also, the suggestion to use web tokens prompted me to find more information on using Auth0 with the application.
Once I had got out of the mindset of creating pages using Gatsby (Through a template, via gatsby-node.s), and instead doing it in a 'React way' (I know Gatsby is built with React) by handling the routing and GraphQL it became clearer. Along with the authentication, all I ended up doing was creating a new <Agency /> component and feeding the data from GraphQL into it and updating the path with my map().
return (
<>
<Router>
<DashboardArea path="/dashboard/" user={user} />
{agencyData.map(({ node }, index) =>
node.slug ? (
<Agency key={index} data={node} path={`/dashboard/${node.slug}`} />
) : null
)}
</Router>
</>
);
I assume that in your PrivateRoute component, you're using the isLoggedIn check incorrectly. importing and using isLoggedIn from auth.js will run only initially and will not act as a listner. What you can do is that store the value of isLoggedin in global state variable like(useContext or redux) and make a custom hook to check for the login state. Secondly avoid accessing localStorage directly, instead use the global state managment (useContext, redux) or local state managment (useState, this.state).
Note: that when ever you go to a route by directly pasting url in browser, it always refreshes the page and all your stored state is reinitialized. This may be the reason why you may be experiencing this issue. The browser does not know that you had been previously logged in and therefore it always validates once your application is mounted. What you can do is that you can store isLoggedIn state in browser's localstore. Personally I like to use redux-persist for that.
export const useGetUser = () => { //add use infront to make a custom hook
return useSelector(state => state.gatsByUser) // access user info from redux store
};
export const handleLogin = ({ username, password }) => {
//suggestion: don't validate password on client side or simply don't use password,
//instead use tokens for validation on client side
if (username === `john` && password === `pass`) {
dispatch(setUserInfo({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
isLoggedIn: true,
}));
return true;
}
return false;
};
// adding 'use' infront to make it a custom hook
export const useIsLoggedIn = () => {
//this will act as a listner when ever the state changes
return useSelector(state => state.gatsByUser?.isLoggedIn ?? false);
};
export const logout = (callback) => {
const dispatch = useDispatch(); // redux
dispatch(clearUserInfo());
};
Now in private route do
import React from "react";
import { navigate } from "gatsby";
import { useIsLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
const isLoggedIn = useIsLoggedIn();
if (!isLoggedIn) {
return navigate("/dashboard/login");
}
return <Component {...rest} />;
};
export default PrivateRoute;
It looks like you're server-side rendering dashboard/[url] in gatsby-node.js/createPages()? IIRC those routes will have higher precedence than dynamic routes (which you specify with #reach/router in dashboard.js).
Plus, the content of those routes are currently publicly available. If you want to keep them truly private, you should query Contentful graphql API directly on the client side (via fetch() or use apollo client, urql, etc.), instead of relying on Gatsby's graphql server.
I would do the follows:
Removing the dashboard/[url] portion in your gatsby-node.js
Configure your web host so that all routes matches '/dashboard/*' will redirect to '/dashboard'
If you happen to host your static site on Netlify, you'd create a _redirects with this, assuming you configure Gatsby to create nice url:
# /static/_redirect
/dashboard/* /dashboard 200
A possible simpler way that match your current setup is gating content at web host level. You can configure nginx to protect /dasboard/* with basic auth. However maintaining/updating password is a pain & modern hosting solution don't really allow user to configure that.
Netlify offers its own authentication solution that you could look into.
I've had the same issue earlier and I couldn't get exact functionality with Private Routes.
In my case, I created two separate Layouts for Public and Private Routes and built the authentication to Private Layout. Logged-in user data were linked to a redux store (First I used Context, then moved to Redux). In Private routes with the Private Layout, it redirected the guest users to the Login page and redirected them to the same page after login.
Private layout is something like this:
import React from "react";
import { navigate } from "gatsby";
import { useSelector } from "react-redux";
const PrivateLayout = ({children}) => {
const isLoggedIn = useSelector(state => state.user.isLoggedIn);
useEffect(() => {
if (!isLoggedIn) {
// redirect the user to login page.
// I'm sending the current page's URL as the redirect URL
// so that I can take the user back to this page after logging in.
}
}, [isLoggedIn])
if (!isLoggedIn) return null;
return <>
{...header}
{children}
{...footer}
</>
}
export default PrivateLayout;
Not sure if this workaround suits you. If it does, I can give you more info.

using react router with next.js

I am learning how to use Next.js/React for my application. I am currently researching the topic of routing and I had a few questions. I know that you can use react-router for React (I used vue-router before). However, I do not know if I need to use react-router with Next.js and how I would use it if I could. I am currently using a pages directory to hold pages to redirect to. How can I redirect to different pages in React/Next?
Here is some sample code to implement it in:
Class Login extends Component {
state = {
user: {},
}
loginUser = (e) => {
loginUser(this.state.user)
.then(response => {
console.log(response);
if (response['username']) {
console.log('yippee!');
}
});
}
}
After yippee, I want to redirect to /home which is in the pages folder.
For your first question I need to say: No, you don't need react-router in Nextjs it will use something called file-system based router which you can read more about it here
So after you set up your routes if you want to Navigate to them you have two options:
first using the Link Component from next/link: more about it here
second using the router from next/router which you can Navigate around like useHistory from react-router: more about it here
example from the doc:
import { useRouter } from 'next/router'
function ActiveLink({ children, href }) {
const router = useRouter()
const style = {
marginRight: 10,
color: router.asPath === href ? 'red' : 'black',
}
const handleClick = (e) => {
e.preventDefault()
router.push(href)
}
return (
<a href={href} onClick={handleClick} style={style}>
{children}
</a>
)
}
export default ActiveLink
So in your case, using this is how you can redirect:
import { withRouter } from 'next/router'
Class Login extends Component {
state = {
user: {},
}
loginUser = (e) => {
loginUser(this.state.user)
.then(response => {
console.log(response);
if (response['username']) {
console.log('yippee!');
//here is what you need:
this.props.router.push('/your-route');
}
});
}
}
export default withRouter(Login)

react keep data in sync

I have a real-time filter structure on a page. The data from my inputs are kept in my State but also as a URL query so that when someone opens the page with filters in the URL the right filters are already selected.
I'm struggling to find a stable way to keep these 2 in sync. Currently, I'm getting the data from my URL on load and setting the data in my URL whenever I change my state, but this structure makes it virtually impossible to reuse the components involved and mistakes can easily lead to infinite loops, it's also virtually impossible to expand. Is there a better architecture to handle keeping these in sync?
I would recommend managing the state of the filters in the view from query params. If you use react-router, you can use query params instead of state and in the render method get params need for view elements. After change filters you need implement redirect. For more convenience it may be better to use qs module. With this approach you will also receive a ready-made parameter for request to backend.
Example container:
const initRequestFields = {someFilterByDefault: ''};
class Example extends Component{
constructor(props) {
super(props);
this.lastSearch = '';
}
componentDidMount() {
this.checkQuery();
}
componentDidUpdate() {
this.checkQuery();
}
checkQuery() {
const {location: {search}, history} = this.props;
if (search) {
this.getData();
} else {
history.replace({path: '/some-route', search: qs.stringify(initRequestFields)});
}
}
getData() {
const {actionGetData, location: {search}} = this.props;
const queryString = search || `?${qs.stringify(initRequestFields)}`;
if (this.lastSearch !== queryString) {
this.lastSearch = queryString;
actionGetData(queryString);
}
}
onChangeFilters = (values) => {
const {history} = this.props;
history.push({path: '/some-route', search: qs.stringify(values)});
};
render() {
const {location: {search}} = this.props;
render(<Filters values={qs.parse(search)} onChangeFilers={this.onChangeFilters} />)
}
}
This logic is best kept in the highest container passing the values to the components.
For get more info:
Query parameters in react router
Qs module for ease work with query
If you worry about bundle size with qs module
This answer used React Hooks
You want to keep the URL with the state, you need a two way sync, from the URL to the state (when the component mount) and from the state to the URL (when you updating the filter).
With the React Router Hooks, you can get a reactive object with the URL, and use it as the state, this is one way- from URL to the component.
The reverse way- update the URL when the component changed, can be done with history.replace.
You can hide this two way in a custom hook, and it will work like the regular useState hook:
To use Query Params as state:
import { useHistory, useLocation} from 'react-router-dom'
const useQueryAsState = () => {
const { pathname, search } = useLocation()
const history = useHistory()
// helper method to create an object from URLSearchParams
const params = getQueryParamsAsObject(search)
const updateQuery = (updatedParams) => {
Object.assign(params, updatedParams)
// helper method to convert {key1:value,k:v} to '?key1=value&k=v'
history.replace(pathname + objectToQueryParams(params))
}
return [params, updateQuery]
}
To use Route Params as state:
import { generatePath, useHistory, useRouteMatch } from 'react-router-dom'
const useParamsAsState = () => {
const { path, params } = useRouteMatch()
const history = useHistory()
const updateParams = (updatedParams) => {
Object.assign(params, updatedParams)
history.push(generatePath(path, params))
}
return [params, updateParams]
}
Note to the history.replace in the Query Params code and to the history.push in the Route Params code.
Usage: (Not a real component from my code, sorry if there are compilation issues)
const ExampleComponent = () => {
const [{ user }, updateParams] = useParamsAsState()
const [{ showDetails }, updateQuery] = useQueryAsState()
return <div>
{user}<br/ >{showDetails === 'true' && 'Some details'}
<DropDown ... onSelect={(selected) => updateParams({ user: selected }) />
<Checkbox ... onChange={(isChecked) => updateQuery({ showDetails: isChecked} })} />
</div>
}
I published this custom hook as npm package: use-route-as-state

React Redux Loading bar for react router navigation

So I'd like to implement a loading bar just like github has. It should start loading on a click to another page and finish when it arrived.
I'm using material-ui and for the loader react-progress-bar-plus.
I tried to use react-router's lifecycle hooks, namely componentDidUpdate and componentWillReceiveProps to set the state to be finished.
For start, I attached an onTouchTap function to the menu items but it just does not want to work properly.
What is the best way to implement this feature?
You can use router-resolver with react-progress-bar-plus.
See this example:
http://minhtranite.github.io/router-resolver/ex-4
The usage example:
// app.js
//...
import {RouterResolver} from 'router-resolver';
//...
const routes = {
path: '/',
component: App,
indexRoute: {
component: require('components/pages/PageHome')
},
childRoutes: [
require('./routes/Example1Route'),
require('./routes/Example2Route'),
require('./routes/Example3Route')
]
};
const renderInitial = () => {
return <div>Loading...</div>;
};
const onError = (error) => {
console.log('Error: ', error);
};
ReactDOM.render(
<Router routes={routes}
history={history}
render={props => (
<RouterResolver {...props} renderInitial={renderInitial} onError={onError}/>
)}/>,
document.getElementById('app')
);
And:
// components/pages/PageExample1.js
import React from 'react';
import Document from 'components/common/Document';
class PageExample1 extends React.Component {
static resolve() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('simple data');
}, 2000);
});
};
static propTypes = {
response: React.PropTypes.string.isRequired
};
render() {
return (
<Document title='Example1 | Router resolver' className='page-ex-1'>
<h1>Example 1: {this.props.response}</h1>
</Document>
);
}
}
export default PageExample1;
I made a small package react-router-loading that allows you to show loading indicator and fetch some data before switching the screen.
Just use Switch and Route from this package instead of react-router-dom:
import { Switch, Route } from "react-router-loading";
Add loading props to the Route where you want to wait something:
<Route path="/my-component" component={MyComponent} loading/>
And then somewhere at the end of fetch logic in MyComponent add loadingContext.done();:
import { LoadingContext } from "react-router-loading";
const loadingContext = useContext(LoadingContext);
const loading = async () => {
//fetching some data
//call method to indicate that fetching is done and we are ready to switch
loadingContext.done();
};

Categories

Resources