React Context API
See original GitHub issueIssue Description
Hi devs,
I’m trying to implement an authentication required route which would ask an AuthContext
if the user is logged in and I think I found some limitations with how the Route component works:
import React from "react";
import Route from "found/lib/Route";
import HttpError from "found/lib/HttpError";
import LoadingOverlay from "./LoadingOverlay";
import LinearProgress from "@material-ui/core/LinearProgress";
import { AuthContext } from "./auth-context";
export default class AuthRequiredRoute extends Route {
render({ Component, props, error, match, resolving }) {
// Couldn't useContext because hooks are only allowed in function components
return (
<AuthContext.Consumer>
{({ loggedIn }) => {
if (!loggedIn)
throw new HttpError(401, "No JWT found in localStorage");
// This error hasn't been caught by renderError yet, that's why we need to throw it
if (resolving && error) {
throw new HttpError(error.status, error);
}
if (Component && props) {
return <Component {...props} />;
}
if (match.route.loadingOverlay) {
return <LoadingOverlay />;
}
return <LinearProgress style={{ marginTop: "68px" }} id="cargando" />;
}}
</AuthContext.Consumer>
);
}
}
The problem with the above approach is
react-dom.development.js:520 Warning: A context consumer was rendered with multiple children, or a child that isn't a function. A context consumer expects a single child that is a function. If you did pass a function, make sure there is no trailing or leading whitespace around it.
warningWithoutStack @ react-dom.development.js:520
updateContextConsumer @ react-dom.development.js:15661
beginWork @ react-dom.development.js:15881
performUnitOfWork @ react-dom.development.js:19745
workLoop @ react-dom.development.js:19786
renderRoot @ react-dom.development.js:19866
performWorkOnRoot @ react-dom.development.js:20811
performWork @ react-dom.development.js:20721
performSyncWork @ react-dom.development.js:20695
requestWork @ react-dom.development.js:20550
scheduleWork @ react-dom.development.js:20357
enqueueSetState @ react-dom.development.js:11475
push.../../node_modules/react/cjs/react.development.js.Component.setState @ react.development.js:336
_callee$ @ createBaseRouter.js:219
tryCatch @ runtime.js:63
invoke @ runtime.js:290
prototype.(anonymous function) @ runtime.js:116
step @ asyncToGenerator.js:21
(anonymous) @ asyncToGenerator.js:32
Promise.then (async)
step @ asyncToGenerator.js:31
(anonymous) @ asyncToGenerator.js:32
Promise.then (async)
step @ asyncToGenerator.js:31
(anonymous) @ asyncToGenerator.js:39
F @ _export.js:43
(anonymous) @ asyncToGenerator.js:18
resolveMatch @ createBaseRouter.js:302
componentDidMount @ createBaseRouter.js:118
commitLifeCycles @ react-dom.development.js:17636
commitAllLifeCycles @ react-dom.development.js:19141
callCallback @ react-dom.development.js:147
invokeGuardedCallbackDev @ react-dom.development.js:196
invokeGuardedCallback @ react-dom.development.js:250
commitRoot @ react-dom.development.js:19360
completeRoot @ react-dom.development.js:20893
performWorkOnRoot @ react-dom.development.js:20816
performWork @ react-dom.development.js:20721
performSyncWork @ react-dom.development.js:20695
requestWork @ react-dom.development.js:20550
scheduleWork @ react-dom.development.js:20357
scheduleRootUpdate @ react-dom.development.js:21061
updateContainerAtExpirationTime @ react-dom.development.js:21087
updateContainer @ react-dom.development.js:21155
push.../../node_modules/react-dom/cjs/react-dom.development.js.ReactRoot.render @ react-dom.development.js:21468
(anonymous) @ react-dom.development.js:21620
unbatchedUpdates @ react-dom.development.js:20938
legacyRenderSubtreeIntoContainer @ react-dom.development.js:21616
render @ react-dom.development.js:21691
./src/index.js @ index.js:14
__webpack_require__ @ bootstrap:83
checkDeferredModules @ bootstrap:45
webpackJsonpCallback @ bootstrap:32
(anonymous) @ main.chunk.js:1
react-dom.development.js:520 Warning: A context consumer was rendered with multiple children, or a child that isn't a function. A context consumer expects a single child that is a function. If you did pass a function, make sure there is no trailing or leading whitespace around it.
warningWithoutStack @ react-dom.development.js:520
updateContextConsumer @ react-dom.development.js:15661
beginWork @ react-dom.development.js:15881
performUnitOfWork @ react-dom.development.js:19745
workLoop @ react-dom.development.js:19786
callCallback @ react-dom.development.js:147
invokeGuardedCallbackDev @ react-dom.development.js:196
invokeGuardedCallback @ react-dom.development.js:250
replayUnitOfWork @ react-dom.development.js:18976
renderRoot @ react-dom.development.js:19899
performWorkOnRoot @ react-dom.development.js:20811
performWork @ react-dom.development.js:20721
performSyncWork @ react-dom.development.js:20695
requestWork @ react-dom.development.js:20550
scheduleWork @ react-dom.development.js:20357
enqueueSetState @ react-dom.development.js:11475
push.../../node_modules/react/cjs/react.development.js.Component.setState @ react.development.js:336
_callee$ @ createBaseRouter.js:219
tryCatch @ runtime.js:63
invoke @ runtime.js:290
prototype.(anonymous function) @ runtime.js:116
step @ asyncToGenerator.js:21
(anonymous) @ asyncToGenerator.js:32
Promise.then (async)
step @ asyncToGenerator.js:31
(anonymous) @ asyncToGenerator.js:32
Promise.then (async)
step @ asyncToGenerator.js:31
(anonymous) @ asyncToGenerator.js:39
F @ _export.js:43
(anonymous) @ asyncToGenerator.js:18
resolveMatch @ createBaseRouter.js:302
componentDidMount @ createBaseRouter.js:118
commitLifeCycles @ react-dom.development.js:17636
commitAllLifeCycles @ react-dom.development.js:19141
callCallback @ react-dom.development.js:147
invokeGuardedCallbackDev @ react-dom.development.js:196
invokeGuardedCallback @ react-dom.development.js:250
commitRoot @ react-dom.development.js:19360
completeRoot @ react-dom.development.js:20893
performWorkOnRoot @ react-dom.development.js:20816
performWork @ react-dom.development.js:20721
performSyncWork @ react-dom.development.js:20695
requestWork @ react-dom.development.js:20550
scheduleWork @ react-dom.development.js:20357
scheduleRootUpdate @ react-dom.development.js:21061
updateContainerAtExpirationTime @ react-dom.development.js:21087
updateContainer @ react-dom.development.js:21155
push.../../node_modules/react-dom/cjs/react-dom.development.js.ReactRoot.render @ react-dom.development.js:21468
(anonymous) @ react-dom.development.js:21620
unbatchedUpdates @ react-dom.development.js:20938
legacyRenderSubtreeIntoContainer @ react-dom.development.js:21616
render @ react-dom.development.js:21691
./src/index.js @ index.js:14
__webpack_require__ @ bootstrap:83
checkDeferredModules @ bootstrap:45
webpackJsonpCallback @ bootstrap:32
(anonymous) @ main.chunk.js:1
react-dom.development.js:15671 Uncaught TypeError: render is not a function
at updateContextConsumer (react-dom.development.js:15671)
at beginWork (react-dom.development.js:15881)
at performUnitOfWork (react-dom.development.js:19745)
at workLoop (react-dom.development.js:19786)
at HTMLUnknownElement.callCallback (react-dom.development.js:147)
at Object.invokeGuardedCallbackDev (react-dom.development.js:196)
at invokeGuardedCallback (react-dom.development.js:250)
at replayUnitOfWork (react-dom.development.js:18976)
at renderRoot (react-dom.development.js:19899)
at performWorkOnRoot (react-dom.development.js:20811)
if I’m not wrong, ReactDOM can’t find a render method because the Route is not a React.Component; it’s just a plain class. It’s worth mentioning that I tried all sorts of simpler consumer functions with different formats, in case of leading or trailing space errors but all of them ended up with the same warning and errors.
The next thing I tried is to move the auth logic to a function component wrapper of Component
in order to useContext
:
import React, { useContext } from "react";
import Route from "found/lib/Route";
import HttpError from "found/lib/HttpError";
import LoadingOverlay from "./LoadingOverlay";
import LinearProgress from "@material-ui/core/LinearProgress";
import { AuthContext } from "./auth-context";
const AuthWrapper = ({ Component, props, error, match, resolving }) => {
const { loggedIn } = useContext(AuthContext);
if (!loggedIn) throw new HttpError(401, "No JWT found in localStorage");
// This error hasn't been caught by renderError yet, that's why we need to throw it
if (resolving && error) {
throw new HttpError(error.status, error);
}
if (Component && props) {
return <Component {...props} />;
}
if (match.route.loadingOverlay) {
return <LoadingOverlay />;
}
return <LinearProgress style={{ marginTop: "68px" }} id="cargando" />;
};
export default class AuthRequiredRoute extends Route {
render(routeProps) {
return <AuthWrapper {...routeProps} />;
}
}
The problem with this approach is that the HttpError
is not caught by the router’s renderError
, perhaps because as mentioned in React Error Boundaries docs:
Finally, I tried extending my AuthRequiredRoute from React.Component to add an error boundary but that wouldn’t even match the current path.
-
Is there a way I can share some context with
Route
s dynamically without having to use a Redux router? -
Wouldn’t it be better to make the
Route
a function component in order to use hooks and other React abilities?
Issue Analytics
- State:
- Created 4 years ago
- Comments:17 (6 by maintainers)
Yeah, so that exact pattern isn’t quite supported with Found Relay –
render
needs to return synchronously and can’t return a promise.So per @hisapy’s example above, we do something like have a wrapping component that handles auth, which then renders the router, and does something like
matchContext={{ loggedIn: !!accessToken }}
or the equivalent.Then the route
render
methods can checkcontext.loggedIn
and throw theRedirectException
if needed.You don’t need to deal with that – that’s what the router handles right now.