Passing props to the handler of React router in the server - javascript

I'm following the example reported in the react-router guide
var App = React.createClass({
render: function () {
return <div>Hi</div>;
}
});
var routes = (
<Route handler={App} path="/" />
);
// if using express it might look like this
app.use(function (req, res) {
// pass in `req.url` and the router will immediately match
Router.run(routes, req.url, function (Handler) {
var content = React.renderToString(<Handler/>);
res.render('main', {content: content});
});
});
Quite simple, isn't it? Instead this is my code:
export function index(req: express.Request, res: express.Response, next: Function) {
Router.run(routes, req.url, (Handler, state) => {
fs.readFileAsync(
path.join(__dirname, '..', 'mockData/airport-codes.csv'),
'utf-8'
).then((content) => {
return csv.parseAsync(content);
}).then((parsedContent: Array<string[]>[]) => {
ResponseHelper.renderTemplate('index', res, {
output: React.renderToString(React.createElement(Handler, {
header: [
"ID", "Type", "Name", "Latitude (deg)", "Longitude (deg)", "Elevation", "Continent", "Country ISO", "Region ISO", "Municipality", "GPS Code", "IATA Code", "Local Code"
],
initialData: parsedContent
}))
});
}).catch(next);
});
}
Basically what I do is get data and pass to the Handler to initialise the component. This is the file route in react:
import { Route } from "react-router";
import * as React from "react";
import Excel from "../components/Excel";
export default (
<Route handler={Excel} path="/" name="excel" />
);
and this is the entrypoint in the frontend
import * as React from "react";
import * as Router from "react-router";
import routes from "./shared/routes";
Router.run(routes, Router.HistoryLocation, (Root, state) => {
React.render(<Root />, document.getElementById('app'));
});
My problem is that when I start the application I get this warning:
Warning: Failed propType: Required prop `header` was not specified in `Excel`. Check the render method of `Router`.
Warning: Failed propType: Required prop `initialData` was not specified in `Excel`. Check the render method of `Router`.
and consequently an error. Do you know which is the right way to pass props in this case?
EDIT
error message
Uncaught SyntaxError: Unexpected token &
1../components/Excel # bundle.js:4s # bundle.js:1e # bundle.js:1(anonymous function) # bundle.js:1

The lifecycle of the isomorphic app like yours is that first, the server runs the JS code and return an HTML string expression of the rendered result. It mounts that to the DOM and then loads your frontend app on top of that. Of course the DOM should be identical so it doesn't need to really rerender anything, but it does mount all of the JS event handlers, React code, etc to the window.
It will still process the Router.run method and match the routes, but it will be the same page that was loaded from the server. However, after that point, the front-end has to replicate the server side functionality (ie fetching the data, passing to the routed component).
So, in your frontend code you need to fetch the required data that you would normally do on the server, and pass it down to the <Root /> component as props.
Check out this demo, specifically the app/server.js (server side render) and app/client.js (client side render). Hope this helps!
React Router Mega Demo

Related

Prevent falsy request from React Router loader function

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

redirect all `*` to specific path in nextjs

I have a single landing page nextJs application it is possible to redirect all * to specific routes as we do in react-router like this how can I do exactly the same in nextJs
<BrowserRouter>
<Routes>
<Route path={ROUTES.ROOT} element={<Registry />} />
<Route path={ROUTES.ALL} element={<Navigate to={ROUTES.ROOT} />} />
</Routes>
</BrowserRouter>
export const ROUTES = {
ALL: '*',
ROOT: '/registry',
};
what I have done so far is that I'm able to redirect a specific route to specific route but not able to redirect all routes to a specific route
const nextConfig = {
async redirects() {
return [
{
source: '/path', // not able to "*" the route
destination: '/registry', // Matched parameters can be used in the destination
permanent: false,
},
];
},
};
module.exports = nextConfig;
Unfortunately, nextJs doesn't seems to have a proper way to handle this kind of redirection inside the nextConfig, But if you want to redirect any 404 page to home, what you can do is:
Create a custom 404 page inside the pages, note that your page must be named as 404
Add this snippet in the 404 file.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function Custom404() {
const router = useRouter()
useEffect(() => {
router.replace('/')
})
return null
}
With that any not found route should redirect to home page.
See this discussion on github
edit:
One last thing, if you want to handle some kind of logic when a user visit some route and redirect if fail, you can do so with getServerSideProps:
Add the async function getServerSideProps in the page where you want to handle some kind of logic before render the page:
// Page where you want to handle the logic
// data is the props that comes from getServerSideProps
function Page({ data }) {
// Render data...
}
// This gets called on every request
export async function getServerSideProps() {
// fetch some data from external API
const res = await fetch(`https://someapi/fetchData`)
const data = await res.json()
if(!data) {
// Here we redirect the user to home if get some error at the API
return {
redirect: {
destination: '/',
permanent: false
}
}
}
// Otherwise pass the data to Page as props
return { props: { data } }
}
export default Page
It's just an example but you got the idea, if you want to learn more about this, read the docs here

React/Node app not working on Chrome "Error running template: Invariant Violation: Invalid hook call"

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.

'Functions are not valid as a React child' when trying to dynamically include Components for SSR bundle

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:

React Router server rendering with server props

Here's an example from the docs.
import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from './routes'
serve((req, res) => {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// You can also check renderProps.components or renderProps.routes for
// your "not found" component or route respectively, and send a 404 as
// below, if you're using a catch-all route.
res.status(200).send(renderToString(<RouterContext {...renderProps} />))
} else {
res.status(404).send('Not found')
}
})
})
I have an object like this:
let reactServerProps = {
'gaKey': process.env.GA_KEY,
'query': req.query,
}
I'm trying to pass this object into react router here
res.status(200).send(renderToString(<RouterContext {...renderProps} {...reactServerProps} />))
And I can't seem to provide access to the variables from within my components.
The issue is that <ReactContext /> only passes some react-router pre-defined props down to the component tree that it builds rather than the custom props you might expect in a normal component.
There are a few workarounds to this issue, though none of them are particularly pretty. The most widely used I think I've seen is to wrap <ReactContext /> with a component whose sole purpose is to make use of React's context feature & pass contextual data down to its children rather than props.
So:
import React from 'react';
export default class DataWrapper extends React.Component {
getChildContext () {
return {
data: this.props.data
};
}
render () {
return this.props.children;
}
}
Then in your express server:
// Grab the data from wherever you need then pass it to your <DataWrapper /> as a prop
// which in turn will pass it down to all it's children through context
res.status(200).send(renderToString(<DataWrapper data={data}><RouterContext {...renderProps} /></DataWrapper>))
And you should then be able to access that data in your child components:
export default class SomeChildComponent extends React.Component {
constructor (props, context) {
super(props, context);
this.state = {
gaKey: context.data.gaKey,
query: context.data.query
};
}
};
I know it used to be possible to make use of the createElement method to set some custom props and pass them through to your child routes in a similar fashion, but I'm not positive that's still valid in newer versions of react-router. See: https://github.com/reactjs/react-router/issues/1369
UPDATE: It is still possible to use middleware to pass additional values into route components. via: https://github.com/reactjs/react-router/issues/3183
And you should be able to make use of props over context:
function createElementFn(serverProps) {
return function(Component, props) {
return <Component {...serverProps} {...props} />
}
}
Then add createElement in your <RouterContext /> like so, passing it your serverProps:
res.status(200).send(renderToString(<RouterContext {...renderProps} createElement={createElementFn(serverProps)} />))
And access them in any of your child components with a simple this.props.gaKey & this.props.query
A much simpler solution is to put whatever data you want in props.routes and it will be passed to your component where you can access it directly through
props.routes.[whatever you passed in]
So in the serve function you can do
props.routes.reactServerProps = {
'gaKey': process.env.GA_KEY,
'query': req.query
}
and in your component you can access it with props.routes.reactServerProps.
See the very last comment by Ryan Florence. https://github.com/ReactTraining/react-router/issues/1017
He forgot the (s). Won't work with route.

Categories

Resources