Transferring data from one page to another - reactjs

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 :)

Related

What am I doing wrong with React context?

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.

persist data in context file from local storage in React?

I am using React context to store a variable called lesson which typically holds a number 1-7. I am trying to persist the lesson number when the page is refreshed. so far I can see that the correct number is set to local storage when I click the Link, but then I get errors due to context.lesson being undefined when I refresh Lesson.js. (The lesson variable is initially set through an event in Menu.js.) Essentially the state "lesson" is clearing/undefined on refresh.
My logic:
Click the Link which sets the lesson in state & local storage
On refresh, run a useffect that gets the local storage value, and
sets the lesson state as that.
Menu.js
<Link
to={`/Module/${index + 1}`}
onClick={() => {context.setLesson(index + 1);}}
/>
Context.js
const [lesson, setLesson] = useState();
Lesson.js
useEffect(() => {
localStorage.setItem("lesson", JSON.stringify(context.lesson));
});
useEffect(() => {
const data = localStorage.getItem("lesson");
if (data) {
context.setLesson(JSON.parse(data));
}
}, []);
context.lesson logs as undefined.
I used a custom hook to solve my problem. moved lesson out of context, and into App.js.
//initial lesson state
const [lesson, setLesson] = useLocalStorage("lesson", "0");
//set lesson state
function setLessonHandler(param) {
setLesson(param);
}
return (
<AppProvider>
<Switch>
<Route exact path="/">
<Menu setLesson={setLessonHandler} />
</Route>
<Route path="/Module">
<Lesson lesson={lesson} />
</Route>
</Switch>
</AppProvider>
unfortunately i cant use a server for this problem so local storage it is!

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.

React app, API call that fetches routes for app

Hello I am using react and redux, i have my action creator that fetches the routes of the page and i creat the routing with them this way:
First in app.js im calling the action creator (using mapDispatchToProps) in UseEffect and passing the result (mapStateToProps) to the Routes component:
useEffect(() => {
fetchMenu();
}, []);
<Routes menu={menu} />
Then in Routes.js:
{menu.map((item) => (
<PrivateRoute
key={item.id}
exact
path={item.link}
parentClass="theme-1"
component={(props) => selectPage(item.linkText, props)}
/>
))}
The problem is that if I refresh the page, there is a little delay between the api call and the render of the page, so for one second the browser shows "NOT FOUND PAGE" and then instantly redirect to the route. How can I make it work properly? Thank you !
Basically what you want is to be able to know that the data hasn't been loaded yet, and render differently based on that. A simple check would be see if the menu is empty. Something like this:
export const Menu = ({ menu, fetchMenu }) => {
useEffect(() => {
fetchMenu();
}, []);
if ( menu.length > 0 ) {
return <Routes menu={menu} />
} else {
return <MenuLoading />
}
}
A more advanced setup would be able to tell the difference between an empty menu due to an API error and a menu that's empty because it's still loading, but to do that you would need to store information about the status of the API call in the state.

React Hook setState passed as prop is firing, but not actually changing the state

I am trying to set up a Login function for an app I am building and I am having trouble getting the 'user' object to update when I successfully login. What is strange is with 'console.log()' I know that the function is firing, and even shows the correct info passed into the function, but then state does not update. I know it isn't updating, because I made a button to change my 'isAuthenticated' status to 'true' and that works perfectly, and that same 'console.log()' shows the exact same info. I'm confident that it can work as a class, but I am really trying to make this a hooks only app to gain more practice. I'm sure I am breaking one of the rules of hooks, but am unsure how to fix it.
// App.js
function App() {
const [auth, setAuth] = React.useState({
isAuthenticated: false,
user: null, // Expects Cognito User Token
});
const setAuthStatus = authStatus => {
setAuth({...auth, isAuthenticated: authStatus});
console.log(authStatus, auth)
};
const setUser = user => {
setAuth({...auth, user: user});
};
const authProps = { // Wrapping for easy pass down
isAuthenticated: auth.isAuthenticated,
user: auth.user,
setAuthStatus: setAuthStatus,
setUser: setUser,
};
return (
<BrowserRouter>
<NavBar auth={authProps}/>
<button onClick={() => authProps.setAuthStatus(!auth.isAuthenticated)}>Click</button>
<Switch>
<Route exact path={"/"} component={LandingPage}/>
<Route exact path={"/main"} render={(props) => <MainWindow {...props} auth={authProps}/>}/>
<Route exact path={"/register"} render={(props) => <Register {...props} auth={authProps}/>}/>
<Route exact path={"/register/success"} component={RegisterSuccess}/>
<Route exact path={"/login"} render={(props) => <Login {...props} auth={authProps}/>}/>
<Route exact path={"/logout"} render={(props) => <Logout {...props} auth={authProps}/>}/>
<Route component={NotFound}/>
</Switch>
<Footer/>
</BrowserRouter>
);
}
export default App;
I wrapped up the functions and the state variables and passed them down. That button is a tester button, which works fine.
Login.jsx
function Login(props) {
// styles stuff, and form validation thingies
const handleSubmit = async event => {
event.preventDefault();
clearErrorState();
try {
const user = await Auth.signIn(values.username, values.password);
props.auth.setAuthStatus(true); // *** Fires, but no state change ***
props.auth.setUser(user);
props.history.push("/main")
} catch (error) { // Sometimes returned as error, or error.message
let err = null;
!error.message ? err = {"message": error} : err = error; // Normalize
setValues({
...values, errors: {cognito: err}
});
}
};
I even made a sample button on the Login.jsx form and that was able to change the state with the exact same 'props.auth.setAuthStatus' function using the same lambda style that is in the App.js file.
I have been unable to find any examples or code that seems to run into the same issue. There are examples of setState inside try / catch blocks but none where you pass it from a parent.
As an aside, I decided not to use Context or Redux for this App at this moment, but if I need one of them to make this work then please let me know and I'll gladly switch it up. I haven't used them, but its only because the App isn't going to be that big.
Alright, figured it out and I hope this will help others. This is a specific hook issue and I am sad that it took me forever to solve.
If I had Redux Dev Tools up I would have solved this in five minutes.
I finally made a dummy button that showed me the State after Login and found my issue. Because I had made my 'auth' state have 'isAuth' and 'user' I was making two separate calls to change the state when I logged in. In a Class based React, each setState call just overwrites the value you are changing, however since Hooks requires you to copy the original state and then also change your value I was sending two state changes at the same time.
Due to the fun of how state changes asynchronously, the second setState call would grab the original state and put the Cognito user in, while still having the 'isAuth' equal to 'false' from the original state and then overwrite the first setState call that changed it to 'true'.
I fixed that by putting each state in its own state: [auth, setAuth] and [user, setUserToken] so each has its own separate call.
TLDR: When sending multiple 'setState' calls to the same state object using hooks, each 'setState' call might end up overwriting your other calls with old information. Split your states up if you need each to update in the same function.

Resources