A setState on a component causes an infinite loop - reactjs

I have my App.js that cause an infinite loop:
function App() {
const [sessionUuid, setSessionUuid] = useState('')
return (
<div className="App">
<Header setSessionUuid={setSessionUuid}/>
<Routes>
<Route path='/about' element={<About />} />
<Route path="/" element={<Home />} />
<Route path="/wine" element={<Wine sessionUuid={sessionUuid} />} />
</Routes>
<Footer />
</div>
);
}
The Header component presents a login or a logout button, depending if a user is logged on. Only if the user do a login or a logout, the component trigger the setSessionUuid function.
useEffect(() => {
if (user !== '')
setSessionUuid(user.sessionUuid)
else
setSessionUuid('')
}, [user, setSessionUuid])
This causes an infinite rendering on the App component.
I found this workaround on stackoverflow, Wrap the setSessionUuid function in a wrapperSetParentState = useCallback function, and pass the wrapperSetParentState to the login component. but it doesn't solve the problem, and the infinite rendering remained
const wrapperSetParentState = useCallback(val => {
setSessionUuid(val);
}, [setSessionUuid]);
What I want to achieve, is that only the component on the route wine will be rendered differently depending on the value of sessionUuid

the setSeesionUuid is causing the useEffect to loop forever.
You can remove it from the dependencies array.
useEffect(() => {
if (user !== '')
setSessionUuid(user.sessionUuid)
else
setSessionUuid('')
}, [user])
add //eslint-disable-next-line if eslint is giving you a warning.
setSessionUuid shouldn't change and cause the effect to run again but from my experience with a recent project the setState function can be omitted from the useEffect Deps array. user is needed in the dependencies because it could change and be a different value.

Has been a nightmare, but I solved it and I want to share with everyone involved where the error was.
First of all, I create a sandbox that you can visit at https://codesandbox.io/s/bitter-water-jsblw?file=/src/App.js:367-374: as you can see, it run like a charme ;-) There is no need for strange things, useEffects or other unnecessary stuffs.
So, the error couldn't be there.
Then, I removed the only thing that was in my App.js and not in the sandbox. I didn't insert it in the initial message because .... it was there for months without problems and was quite invisible for me: Could any invisible thing create a problem? YES, IT CAN!
The instruction was
const langs = navigator.languages; //["en-US", "zh-CN", "ja-JP"]
const userLanguageCode = langs[0].length > 2 ? langs[0].substring(0, 2) : langs;
i18n.changeLanguage(userLanguageCode);
And, precisely, it was the i18n.changeLanguage(userLanguageCode); that created the problem. I deleted it and everything runs perfectly, as in the sandbox.
Thanks to everybody for the support, and I hope that this misadventure will offer food for thought for everyone.
Thank you again

Related

React Router - Placing components by iteration not working

Given the following code:
const AdmNews = React.lazy(() => import('./components/adm/AdmNews'));
const AdmPeople = React.lazy(() => import('./components/adm/AdmPeople'));
const AdminRoutes = [
{path:"/route/admin/news",component:<AdmNews/>},
{path:"/route/admin/people",component:<AdmPeople/>},
]
{AdminRoutes.map((el)=>{
return <Route path={el.path}
element={
<React.Suspense fallback={<>...</>}>
{el.component}
</React.Suspense>
}/>
})}
I'm trying to iterate over objects in order to create the routes in react, and when I click on the matching buttons, they understand the path property, and it reacts accordingly, however, the component is not loading. Is is right to have the component inside the object as show above: component:<AdmNews/>? (No errors are shown.)
PD. If I place the code manually without iteration, it works, so I suspect of component:<AdmNews/> not being the right approach.

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

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!

Why is my custom hook not re-initialised on path change?

I am using ReactRouter to route the application (BrowserRouter at the top level) with a Switch that includes all the routes.
In my use-case I want to be able to handle paths that include the path-parameters (bedId) and navigate between different sub-paths (i.e. /beds/:bedId/, /beds/:bedId/info/) as well asa case where the path is (/beds/-).
I also want to be able to direct user to a different "bed" while they are already on some bed, so /beds/bed1/info -> /beds/bed2, and so on...
My BedView component is responsible for routing within that /beds/:bedId path like so:
// App.jsx (fragment)
<Switch>
<Route
path="/"
exact
render={() => (<Redirect to="/beds/-"/>)}
/>
<Route
path="/beds/-"
exact
component={SomeOtherComponent}
/>
<Route
path="/beds/:bedId"
component={BedView}
/>
</Switch>
The problem occurs when I try to use a hook that relies on the current path-parameter to fetch the latest data (i.e. call to /beds/bed1 will result in a call to http://myapi.com/beds/bed1/timeseries). The useLatestData hook is called from the BedView component, which look like so:
// BedView.jsx (fragment)
export default function BedView(props) {
const {bedId} = props.match.params;
let {path, url} = useRouteMatch('/beds/:bedId');
const bedData = useLatestData({
path: `/beds/${bedId}/timeseries`,
checksumPath: `/checksum/timeseries`,
refresh: false
});```
if(!bedData){
return <Loading/>;
}
return (
<Switch>
<Route exact path={path}>
<Redirect to={`${url}/info`}/>
</Route>
<Route exact path={`${path}/info`} >
<SomeComponent info={bedData.info} />
</Route>
</Switch>
}
...and the useLatestData hook is available here.
The problem is the fact that upon redirecting from /beds/bed1/info to /beds/bed2/info, the hook does not update its props, even though the BedView component seems to be re-rendering. I have created a version of the hook that 'patches' the problem by adding an useEffect hook in my custom hook, to detect the change in path (as supplied in the function arguments) and set data to null, but then the behaviour changes on the BedView.jsx's end - making the following check fail:
if(!bedData){
return <Loading/>;
}
I'm not entirely sure which part is the culprit here and any guidance would be much appreciated! As far as I'm aware, the hook is no re-initialised because the path change still results in the same component. There is also one caveat, once I change the BrowserRouter to include the forceRefresh flag, everything works fine. Naturally, I don't want my page to refresh with every redirect though...
try this:
const {bedId} = props.match.params;
let {path, url} = props.match;

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