What am I doing wrong with React context? - reactjs

Quick context, I am making a character-building app (think D&D Beyond). Users can log in, use the builder to generate a character, and then later recall and level-up that character. I'm hosting the data on MongoDB and using Realm Web for CRUD on the database. Based on this guide, I'm using React Context to keep track of the logged-in user as they navigate the site. I'm trying to create a widget in the NavMenu that will allow me to see the logged-in user for dev purposes (later, I can make a "Welcome, [name]!"-type greeting), but I keep getting the error that "mongoContext.user" is null. This makes sense, as the Context is set to null with useState, but as soon as the page renders there is an init() function which anonymously logs in the user if they aren't already. I can't seem to understand why this is happening, even after looking at 4 different articles on how to use React Context. Code Snippets below:
App.js
const [client, setClient] = useState(null);
const [user, setUser] = useState(null);
const [app, setApp] = useState(new Realm.App({id: process.env.REACT_APP_REALM_APP_ID}));
//Anonymously logs in to mongoDB App instance on page render
useEffect(() => {
const init = async () => {
if (!user) {
setUser(app.currentUser ? app.currentUser : await app.logIn(Realm.Credentials.anonymous()));
}
if (!client) {
setClient(app.currentUser.mongoClient('mongodb-atlas'));
}
}
init();
}, [app, client, user]);
//Function to render a given component and pass it the mongoContext prop
const renderComponent = (Component, additionalProps = {}) => {
return (
<MongoContext.Consumer>
{(mongoContext) => <Component mongoContext={mongoContext} {...additionalProps} />}
</MongoContext.Consumer>
);
}
return (
<MongoContext.Provider value={{app, client, user, setClient, setUser, setApp}}>
<Layout>
<Switch>
<Route exact path="/" render={() => renderComponent(Home)} />
... other Routes ...
</Switch>
</Layout>
</MongoContext.Provider>
);
}
As outlined in the guide, each Route receives a render function which wraps it in the Context.Consumer component and sends context. I couldn't figure out if this was possible with the Layout component (since it is itself a component wrapper), so instead I used React.useContext() inside the component I need context for: (the component relationship is Layout -> NavMenu -> UserDetail)
UserDetail.jsx
const UserDetail = (props) => {
const mongoContext = React.useContext(MongoContext);
return (
<div>
<h1>Logged in as: {mongoContext.user.id}</h1>
</div>
);
}
This is the component which gives the "mongoContext.user is null" error. The other components seem to use context just fine, as I have done some Create operations with the anonymous user. I can see that it probably has something to do with how the other components are rendered with the Consumer wrapper, but according to every guide I've read, the React.useContext in the UserDetail component should work as well. Thanks for any help, this has completely stymied the project for now.

Related

Create wrapper to check user role in App.js component routes

I know the question is very general but I cannot find the correct way and terms to search this online to see how it can be done.
My problem is that I have an app that users log in and then a user role is added to a context element (like if the user is admin or simple user etc).
I can restrict access to the components by checking the user role and redirect. somerhing like
return user==="admin"?<div>...</div>: "redirect to no access component"
Is a way to create a wrapper that will wrap every Route in the App.js and perform this check instead.
normally you should restrict the access in the router of your application.
but to anwser the question you can create a hooks that checks if user is admin or else redirect to the no access component using react-router:
const useCheckUserPermission = (permission) => {
// get the user from the context
useEffect(() => {
if(user === permission) // redirect to no access component
}, [])
return [];
}
then you can use it like this:
const Component = (props) => {
useCheckUserPermission("admin");
return // return your component
}
If its a page then you can follow the following Approach:
While attaching Routes to Router you can create custom Route component which will check the condition.
Like:
import { Route } from "react-router-dom";
const PrivateRouteAdmin=({path,component:Component...rest})=>{
const user = useContext(AuthContext).userType;// any global state which you have used
return user==='admin'?
<Route path={path} {...rest}
render={(props)=>{
return <Component {...props} />
}}/>: <Redirect to={"/"}/>
}
You can use it in you code as follows:
<BrowserRouter>
<PrivateRouteAdmin exact path={'/protected'} component={ProtectedPage} />
</BrowserRouter>
If you want to apply some condition on specific page element, then you can use the approach which you are following otherwise make another component for that purpose too like:
const AdminContent = ({children)=> {
const user = useContext(AuthContext).userType;// any global state which you have used
return user==='admin'?children:<Redirect to={"/"}/>
}
and you can call the component anywhere you want like:
<AdminContent> <div>protected</div></AdminContent>

How to get browser information for react components in nextjs?

I am writing a react component in nextjs that needs some information about the browser environment before it can be rendered. I know I can get this information from the user agent string after page load with a useEffect like so...
const MyComponent = () => {
const [browser, setBrowser] = useState(null);
useEffect(() => {
const ua = utils.getBrowserFromUA()
setBrowser(ua)
});
if (!browser) {
return <div>Loading...</div>
}
return (
<div>
<SpecialComponent browser={browser} />
</div>
);
};
Is it possible to get these values before the react component renders, before the pageLoad event perhaps? I really only need to calculate these values once and potentially share the values with other components. I can't find anything in the docs apart from running scripts with the beforeInteractive flag but I'm not sure how I would get the data to my component.

Transferring data from one page to another

I have a page called Games in my React app and my React Routes are in the App.js file. On that Games page, if I click myButton, I want it to go to another page called Analyze Game and then populate some variables there. But when I normally open Analyze Game (without clicking myButton), it defines and populates a bunch of state and variables. The reason is so that you can analyze a game you manually enter, rather than picking from the list on the Games page. So I am a bit puzzled on how I can transfer the game data from the Games page to the Analyze Game page and then populate some variables there only if someone came from the games page.
I found this link that shows you can use history.push to get some data that was passed from one page to another: React-router - How to pass data between pages in React?


But how do you then only populate the variables on the Analyze Game page if you came from the games page?
Would you set a flag or something? What is best practice?
This is a good question, and normally can be done in two ways. I'll briefly mention them. Of course, there're other ways ;)
props
React likes props, so if you can solve it with that, that should be your first approach.
const App = () => {
const [value, setValue] = useState(...)
return (
<Router>
<Route1 render={() => <GamePage value={value} />} />
<Route2 render={() => <AnalyzePage value={value} />} />
</Router>
)
}
If you want to change this value inside either Game or Analyze, pass the setValue inside as well.
context
If you have very nested components inside the route, sometimes people like to use a context.
const App = () => {
const [value, setValue] = useState(...)
return (
<Context.Provider value={{ value, setValue}}>
<Router>
<Route1 render={() => <GamePage />} />
<Route2 render={() => <AnalyzePage />} />
</Router>
</Context.Provider>
)
}
const ComponentInsideGame = () => {
const { value } = useContext(Context)
...
}
global
If it turns out you want to send something behind React, for instance you don't want to trigger render at all for these shared data. You could do a global context with a ref.
const App = () => {
const ref = useRef({
gameConfiguration: ...,
runGame: () => {}
))
return (
<Router>
<Route1 render={() => <GamePage />} />
<Route2 render={() => <AnalyzePage />} />
</Router>
)
}
const ComponentInsideGame = () => {
const { current } = useContext(Context)
current.gameConfiguration = {...}
current.runGame()
...
}
route state
The link you provided is another way with browser state, however this actually isn't a react way, it's a browser way IMHO. But you know what, anyway works for you is fine :)

Cant figure out how to create a 'basic' HOC for a react / NextJS application

I have built an application that works well, but i now need to add some logic into each page that checks to see if the user has a subscription and if they do then send them to payment page to make a payment.
I have a hook (using SWR) that gets me the user session and within the hook it returns a boolean for isSubscribed. I intend to use this.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
An above example is what i currently do. But this solution requires me to copy pasta everytime to the page which obviously i can do, but it's no way efficient. I know that HOC exists and from what i know i an use a HOC to do this. But i have no idea how to write one that would fit this purpose.
As an added benefit, it would be useful to add the session as a prop to the 'new component' so that i dont have to call the hook twice.
Thanks for all and any help.
p.s. i mention it in the title, but i'm using NextJS. Not sure if this has any baring (i dont think it does, but worth mentioning)
You can create a wrapper HOC such as following;
const withSession = (Component: NextComponentType<NextPageContext, any, {}>) => {
const Session = (props: any) => {
const session = useSession();
if (session.isLoading) {
return <>Loading..</>
}
else {
return <Component {...props} />
}
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Session.getInitialProps = Component.getInitialProps;
}
return Session;
};
And to use it in your page or component, you can simply do like;
const UserDetailPage: React.FC = (props) => {
// ...
// component's body
return (<> HI </>);
};
export default withSession(UserDetailPage);
I think this problem doesn't necessary require a HOC, but can be solved with a regular component composition. Depending on your actual use case, it may or may not be a simpler solution.
We could implement a Session component that would leverage the useSession hook and conditionally render components passed via the children prop:
const Session = props => {
const { isLoading } = useSession();
if (isLoading) {
return "Loading...";
}
return props.children;
};
Then nest the Page component into the Session:
const GuardedPage: React.FC<PageProps> = props => {
return (
<Session>
<Page {...props} />
</Session>
);
};
I see the question has already been answered, just wanted to suggest an alternative. One of the benefits of this approach is that we can wrap an arbitrary tree into the Session, and not just the Page.
Are you trying to return a page loading screen component and direct the user to the appropriate page based on thier subscription status? or isLoading handles one event and isSubscribed handles another?
Let's define (HOC) higher order component for the sake of your problem. By using HOC, logic can be modularized and redistributed throughout components. This HOC your creating should have the capability to call different methods on a single data source or one method to be applied across multiple components. For instance say you have an API component with 5 end points (login, subscribe, logout, unsubsubscribe) the HOC should have the ability to utilize any of the endpoints from any other component you use it in. HOC is used to create an abstraction that will allow you to define logic in a single place.
Your code calls one singular method to check if the session is in use of display the content of a page based on user subscription and page loading. Without seeing the components you are trying to use I can not determine the state that needs to be passed? but I will give it shot.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
First thing I see wrong in above code as a use case for an HOC component you have no export statement to share with other components. Also, why use 2 return statements for isLoading unless both conditions need to be checked (isLoading & isSubscribed) also, are these conditional statements depended on each other or seprate functions that can be called separately from another source? if you posted more of your code or the components you are pasting this into it would help?
To use this as an HOC in NEXT is essentially the same as react.
Dependant logic
const session = useSession(props);
// ad constructor and state logic
...
if(session.isLoading) {
return this.setState({isLoading: true});
} else {
return this.setState({isSubscribed: false});
}
Separate logic
const session = useSession(props);
// ad constructor and state logic
...
isLoading () => {
return this.setState({isLoading: true});
}
isSubscribed() => {
return this.setState({isSubscribed: true});
}
or something like this that uses routes...
import React, { Component } from 'react';
import { Redirect, Route } from 'react-router-dom';
export const HOC = {
isState: false,
isSubscribed(props) {
this.isState = false;
setTimeout(props, 100);
},
isLoading(props) {
this.isState = true;
setTimeout(props, 100);
}
};
export const AuthRoute = ({ component: Component, ...rest}) => {
return (
<Route {...rest} render={(props) => (
HOC.isAuthenticated === true ? <Component {...props} /> : <Redirect to='/' />
)}/>
)};
}
If you could share more of you code it would be more helpful? or one of the components you are having to copy and paste from your original HOC code. I would be easier than stabbing in the dark to assist in your problem but I hope this helps!
Cheers!

How do I stop nested React components that dispatch Redux actions which update state from getting stuck in an infinite loop?

I am having an issue with my nested react components getting stuck in an infinite loop.
The outer component is a DashboardLayout. To separate redux logic from 'pure' layout logic I have divided the compnent as follows
DashboardLayout/
index.js
DashboardLayout.js
The dashboard layout is mapped to the route /user
The index.js is (roughly) as follows
const Dashboard = () => {
const { submissions } = useSubmissionsPreloader()
const dispatch = useDispatch()
const { pathname } = useLocation()
useEffect(() => {
if (pathname === '/dashboard') dispatch(replace('/dashboard/tasks'))
}, [dispatch, pathname])
return pathname === '/user' ? null : (
<DashboardLayout submissions={submissions} selected={pathname} />
)
}
DashboardLayout.js is roughly as follows
const DashboardLayout = ({
submissions,
selected
}) => (
<Container>
<SegmentedController
tabs={[
{ path: '/dashboard/submissions', title: 'My Submissions' },
{ path: '/dashboard/tasks', title: 'My Tasks' }
]}
selected={selected}
/>
<Switch>
{dashboardRoutes.map(({ path, loader, exact }) => (
<Route key={path} path={path} component={loadable({ loader })} exact={Boolean(exact)} />
))}
</Switch>
<h4>Submissions ({submissions.length})</h4>
<Table
headers={submissionsHeaders}
rows={submissions.map(submissionsToRows)}
/>
</Container>
)
This all works fine if the sub-component being mounted doesn't affect the redux state. However if we take one of the sub-components as an example
Tasks/
index.js
Tasks.js
index.js is as follows
const Tasks = () => {
const { tasks } = useTasks()
return <PureTasks tasks={tasks} />
}
and Tasks.js is simply this (doesn't actually care about the tasks yet)
const Tasks = () => (
<>
<p>Tasks assigned to me go here</p>
</>
)
The problem is that the useTasks is using a useEffect hook to dispatch a loadTasks action, a saga picks it up, makes an API call, and then dispatches loadTasksSuccess with the loaded tasks. The reducer for that updates the tasks state with the tasks pulled from the api
useTasks
export const useTasks = () => {
const tasks = useSelector(getTasks)
const dispatch = useDispatch()
const doTasksLoad = useCallback(() => dispatch(tasksLoad()), [dispatch])
useEffect(() => {
doTasksLoad()
}, [doTasksLoad])
return { tasks }
and the relevant bit of the saga
function* worker({ type }) {
switch (type) {
case 'TASKS_LOAD':
try {
const tasks = yield call(loadTasks) // api call returns tasks
yield put(tasksLoadSuccess(tasks))
} catch (err) {
yield put(tasksLoadFail(err))
}
/* istanbul ignore next */ break
default:
break
}
}
Nothing controversial there.
The issue is that the change to the state causes the layout to re-render which causes the nested component to re-render which triggers the tasksLoad action again which triggers the tasksLoadSuccess action, which changes the state (tasksLoad sets isLoading to true and tasksLoadSuccess sets it to false again) and this causes an infinite loop.
I've got a gut feeling I ought to be using something like useMemo or useRef to somehow stop the constant re-rendering, but so far I'm not quite getting that to work either.
This general mechanism is fairly core to the way I was planning on building the app so I'd like to get it right. If the nested component only reads from the state and doesn't change it then no re-rendering happens so a brute force approach would be to get the dashboard to simply preload everything it thinks it might need. But that seems ugly to me.
Has anyone got any suggestions as to a better way to approach this?
I have very less experience with Redux but i think const doTaskLoad is being assigned a function. If that is so then...
I think the problem over here is that you are using a function as a element in the dependency array of useEffect , as per the rules of React Render , every time a render happens every function is a new reference , hence React considers it as a new element and keeps on re rendering the value.
May i suggest using a primtive value for the dependency array
export const useTasks = () => {
const tasks = useSelector(getTasks)
const dispatch = useDispatch()
const doTasksLoad = useCallback(() => dispatch(tasksLoad()), [dispatch])
useEffect(() => {
doTasksLoad()
}, [doTasksLoad])
return { tasks }
I worked out what was wrong, with thanks to #cory-harper whose comment above pointed me in the right direction.
The Tasks component was indeed being reloaded. This is due to how my dashboardRoutes were being described.
I render the individual routes as
{dashboardRoutes.map(({ path, loader, exact }) => (
<Route key={path} path={path} component={loadable({ loader })} exact={Boolean(exact)} />
))}
which is all well and good, and standard practice, when rendering entire scenes. The loadable component wrapper ensures that the component is lazily loaded, and the route itself was being defined via a utility function makeRoute which is as follows
const makeRoute = ([path, scene, permissionRequired = null, exact = true]) => ({
path,
loader: () => import(`scenes/${scene}`),
permissionRequired,
exact
})
So the loader asynchronously imports the correct component which is fine for an entire scene, but for the nested component it made react think the component was new each time, thus the useEffect was being called each time, thus the infinite loop.
By removing the fancy-pants lazy/asynchronous loading (not needed as these sub-components are tiny) and replacing that loop with
<Switch>
<Route path="/dashboard/submissions" component={Submissions} exact />
<Route path="/dashboard/tasks" component={Tasks} exact />
</Switch>
the components no longer look unique each load, and everything works as expected.

Resources