I'm building isomorphic application using ReactJS with react-router module for routing purposes on server side.
From its guide about using react-router on server:
(req, res) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
//...
else if (renderProps) {
res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
}
//...
})
}
There is almost no information about this RoutingContext. So it's a bit unclear for me how it works. Is it some kind of replacement for Router component from react-router (used on top of other routes)?
Any help in understanding will be really appreciated!
React router v4
in the new version (v4) it has been updated to createServerRenderContext. This works in very different way than previously but is much more concise as it also get rid of the need for using 'match'.
this code example is to be applied as express middleware:
import React from 'react';
import { renderToString } from 'react-dom/server';
import { ServerRouter/* , createServerRenderContext */ } from 'react-router';
// todo : remove line when this PR is live
// https://github.com/ReactTraining/react-router/pull/3820
import createServerRenderContext from 'react-router/createServerRenderContext';
import { makeRoutes } from '../../app/routes';
const createMarkup = (req, context) => renderToString(
<ServerRouter location={req.url} context={context} >
{makeRoutes()}
</ServerRouter>
);
const setRouterContext = (req, res, next) => {
const context = createServerRenderContext();
const markup = createMarkup(req, context);
const result = context.getResult();
if (result.redirect) {
res.redirect(301, result.redirect.pathname + result.redirect.search);
} else {
res.status(result.missed ? 404 : 200);
res.routerContext = (result.missed) ? createMarkup(req, context) : markup;
next();
}
};
export default setRouterContext;
react-lego is an example app that shows how to do universal rendering using createServerRenderContext
RoutingContext is an undocumented feature and will be replaced by RouterContext in v2.0.0. Its role is to synchronously render the route component.
It is simply a wrapper around your component which inject context properties such as history, location and params.
React router v4
in the new version (v4) it has been deleted to createServerRenderContext. This works in very different way than previously but is much more concise.
this little code example is to be applied.
import { StaticRouter } from'react-router-dom'
const context = {}
const mockup = renderToString(
<Provider store = {store}>
<IntlProvider locale = {locale} messages = {messages[locale]}>
<StaticRouter location={request.url} context={context}>
<ModuleReactWithPages />
</StaticRouter>
</IntlProvider>
</Provider>
)
Now it's a layer of itself when it's a 404
Related
I use React Router 6.4.2 and it's API - createBrowserRouter.
I have a default request that should be done only once when user reach main route ('/').
Request requires auth token (Amplify) so i use protected routes. No token - redirect to './auth/.
Router:
const router = createBrowserRouter([
{
element: (
<RequireAuth>
<AppLayout />
</RequireAuth>
),
children: [
{
path: '/',
loader: getDefaultData,
element: <MainPage />,
},
],
},
{ path: '/auth', element: <Authenticator /> },
{
path: '*',
element: <NotFoundPage />,
},
]);
export const AppRouter = () => {
return <RouterProvider router={router} fallbackElement={<AppLoader centered />} />;
};
RequireAuth:
export const RequireAuth = ({ children }: Props) => {
const location = useLocation();
const { route } = useAuthenticator((context) => [context.route]);
if (route !== 'authenticated') {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
return <>{children}</>;
};
GetDefaultData:
export const getDefaultData = async () => {
store.dispatch(
getData({
someVariables,
})
);
};
What i faced: when not authenticated user try to reach main route ('/'), he reach it for a moment before he will be redirected to './auth/ and React Router run getDefaultData from loader that fails on getting-auth-toker step.
What i expect: React Router will skip getDefaultData for that case. Looking for a way how to tell React Router about that in a beautiful way.
P.S. I know that i can add auth check with return inside getDefaultData function (generally it happens but not from scratch but on getting-auth-token).
I know about shouldRevalidate but not sure that it can help me in that case.
UPD. provided a codesandbox for that
https://codesandbox.io/s/amazing-matsumoto-cdbtow?file=/src/index.tsx
Simply try remove '/auth' from url manually and check console.
UPD. created an issue about that https://github.com/remix-run/react-router/issues/9529
Got an answer from Matt Brophy in github:
Fetching is decoupled from rendering in 6.4, so loaders run before any rendering logic. You should lift your "requires authorization" logic out of the RequireAuth component and into your loaders and redirect from there. Redirecting during render is too late 😄
Also note that all loaders for a matched route route run in parallel, so you should check in each loader for now (just like in Remix). We plan to make this easier to do in one place in the future.
Original answer:
https://github.com/remix-run/react-router/issues/9529
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.
I am getting an error in my react/node/meteor application. In particular, the app fails to load on Chrome, but works properly on all other browsers (edge, firefox). On Chrome I get a 500 error and the page does not load. On the terminal, where I am running the app, I get this:
(webapp_server.js:1010) Error running template: Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks...
I know that there are three common causes of this error as we see here:
https://reactjs.org/warnings/invalid-hook-call-warning.html
But I am using "react": "^16.8.0", "react-dom": "^16.8.0", and I don't use react native.
I don't believe I am improperly using hooks.
I ran this:
meteor npm ls react
and got back the following response:
`-- react#16.8.6
I have reset my machine and still the problem persists: Edge works fine, Chrome fails to load.
Here is the code that is having the error:
import React from 'react';
import MeteorLoadable from 'meteor/nemms:meteor-react-loadable';
import acceptLanguage from 'accept-language';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { Meteor } from 'meteor/meteor';
import { onPageLoad } from 'meteor/server-render';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider, getDataFromTree } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { StaticRouter, Route } from 'react-router-dom';
import Helmet from 'react-helmet';
import fetch from 'node-fetch';
import App from '/app/ui/components/smart/app';
import HeaderTitle from '/app/ui/components/smart/header/header-title';
import LanguagePicker from '/app/ui/components/dumb/language-picker';
import Routes from '/app/ui/routes';
import { primaryLocale, otherLocales } from '/app/intl';
const locales = primaryLocale ? [primaryLocale, ...otherLocales] : otherLocales;
acceptLanguage.languages(locales);
const render = async (sink) => {
const ssrClient = new ApolloClient({
link: new HttpLink({
uri: Meteor.absoluteUrl('/graphql'),
fetch,
}),
cache: new InMemoryCache(),
ssrMode: true,
});
const preferredLocale = acceptLanguage.get(sink.request.headers['accept-language']);
let locale = otherLocales.find(l => sink.request.url.path.startsWith(`/${l}`));
let prefix = locale;
// /app-shell is a special route that does no server-side rendering
// It's used by the service worker for all navigation routes, so after the first visit
// the initial server response is very quick to display the app shell, and the client
// adds in the data.
// In the case of a first visit or a robot, we render everything on the server.
if (sink.request.url.path === '/app-shell') {
sink.appendToBody(`<script>window.__APOLLO_STATE__=${JSON.stringify(ssrClient.extract())};</script>`);
sink.appendToBody(`<script>window.__PREFERRED_LOCALE__='${preferredLocale}';</script>`);
sink.appendToBody(MeteorLoadable.getLoadedModulesScriptTag());
return;
}
// We first check if we need to redirect to a locale
// We can only do this is there isn't a primary locale.
if (!primaryLocale) {
if (!locale) {
sink.setStatusCode(307);
sink.redirect(`/${preferredLocale || otherLocales[0]}${sink.request.url.path}`);
return;
}
} else if (!locale) {
// If there's no locale prefix, we use the primaryLocale instead
locale = primaryLocale;
prefix = '';
}
const ServerApp = ({ component, context }) => (
<MeteorLoadable.Capture>
<StaticRouter location={sink.request.url} context={context}>
<ApolloProvider client={ssrClient}>
<Route
path={`/${prefix}`}
render={props => <App component={component} {...props} locale={locale} section="app" />}
/>
</ApolloProvider>
</StaticRouter>
</MeteorLoadable.Capture>
);
// Load all data from local server
const context = {};
await getDataFromTree(<ServerApp component={Routes} context={context} />);
// Elements that we want rendered on the server
const sheet = new ServerStyleSheet();
sink.renderIntoElementById('header-title', renderToString(sheet.collectStyles(<ServerApp component={HeaderTitle} context={context} />)));
sink.renderIntoElementById('header-lang-picker', renderToString(sheet.collectStyles(<ServerApp component={LanguagePicker} context={context} />)));
sink.renderIntoElementById('main', renderToString(sheet.collectStyles(<ServerApp component={Routes} context={context} />)));
// Append helmet and styles
const helmetResult = Helmet.renderStatic();
['title', 'meta', 'link', 'script'].forEach(k => sink.appendToHead(helmetResult[k].toString()));
sink.appendToHead(sheet.getStyleTags());
// Append user's preferred locale
sink.appendToBody(`<script>window.__PREFERRED_LOCALE__='${preferredLocale}';</script>`);
// Append Apollo data
sink.appendToBody(`<script>window.__APOLLO_STATE__=${JSON.stringify(ssrClient.extract())};</script>`);
// Append preloaded ReactLoadabe modules
sink.appendToBody(MeteorLoadable.getLoadedModulesScriptTag());
};
Meteor.startup(async () => {
await MeteorLoadable.preloadComponents();
onPageLoad(render);
});
In particular it is this line that is returning the error:
await getDataFromTree(<ServerApp component={Routes} context={context} />);
I commented that line out and the app seems to work fine now. I don't know if I need it and it should not be causing problems. This code is copied line for line from a starter kit and I don't know what this code is doing.
Note:
This code is taken from the following starter kit:
https://github.com/timothyarmes/ta-meteor-apollo-starter-kit
It looks like this did disobey hooks and so I changed the following code:
await getDataFromTree(<ServerApp component={Routes} context={context} />);
To this:
const aServerApp = () => (
<ServerApp component={Routes} context={context} />
);
await getDataFromTree(aServerApp);
and it seems to work fine.
From what I understand Nextjs resolves URLs by mapping them to their respective file in pages folder. So pages/about-us.js will be accessable via href="/about-us".
I want to create multiple languages but don't wanna duplicate the necessary components/JS files. So assuming I have an about-us.js with following content:
<Head title={meta}/>
<Nav/>
<MainContent language={lang}/>
<Footer/>
How can I map /pl/about-us to the /about-us.js in the root of pages without creating another about-us.js in /pages/pl/..?
One of the solution I can think of is to pass the language as a query param
Example
// code for page/about-us.js page
import { withRouter } from 'next/router';
const AboutUs = ({ router }) => {
const { lang } = router.query;
return <div>Welcome to next.js! Language = {lang}</div>;
};
export default withRouter(AboutUs);
so If you got to about-us?lang=pl it will show
Welcome to next.js! Language = pl
Or instead of parsing language inside every page, you can use custom app.js with the code something like this
// custom _app.js
import React from 'react'
import App, { Container } from 'next/app'
export default class MyApp extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
state = {
language: undefined
};
componentDidMount() {
const { router } = this.props;
this.setState({ language: router.query.lang });
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<Component {...pageProps} language={this.state.langugage} />
</Container>
)
}
}
so every page will have language passed as a param.
Hope this helps.
UPDATE:
to make a custom routing you need to check disabling file-system routing and write some custom server routing
From my express route I'm trying to pass a component to use in a render function, that handles SSR.
Express Route:
import SettingsConnected from '../../../client/components/settings/settings-connected';
function accountTab(req, res, next) {
sendToRenderApp(req, res, { profileInfo }, url, SettingsConnected);
}
Render helper:
export const sendToRenderApp = (req, res, storeObj = {}, urlPath, componentFunc) => {
const store = configureStore(storeObj);
const dynamicComponent = componentFunc;
const component = (
<Provider store={store}>
<React.Fragment>
<dynamicComponent />
</React.Fragment>
</Provider>
);
const sheet = new ServerStyleSheet();
const app = renderToString(sheet.collectStyles(component));
Error:
"Warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it."
Things I've already had a look at include this answer below, but I'm not sure how to wrap such a function inside (what I presume) the Provider component?
Functions are not valid as a React child. This may happen if you return a Component instead of from render
ANSWERING OWN QUESTION:
Turned out to be a problem with the client's hydrate. This piece of code above was fine all along - :facepalm: