persist data in context file from local storage in React? - reactjs

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!

Related

A setState on a component causes an infinite loop

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

how to clear data of useState when route get changed

I have maintained state by using useState hook in react. I want to clean value that is getting maintain in state when route get changes.
For example - I have 4 routes declared in react based project. which are as below
<Router>
<Layout>
<Route exact path="/" component={Home}></Route>
<Route exact path="/defineFacilities" component={DefineFacilities}></Route>
**<Route exact path="/createNewModel/:id" component={ModelFormsContainer}></Route>**
<Route exact path="/viewExistingModels" component={ViewExistingModels}></Route>
<Route exact path="/importNewModel" component={ImportNewModel}></Route>
</Layout>
I have maintained state in ModelFormsContainer component. I want to clean state values when user move to other routes. Currently when I move to other route and back ModelFormsContainer component then I noticed that my state are still available.
I wasn't able to reproduce the issue you describe, but if you need to do something when the route changes you can listen for changes to the route/location via the history object.
history.listen
Starts listening for location changes and calls the given callback
with an Update when it does.
// To start listening for location changes...
let unlisten = history.listen(({ action, location }) => {
// The current location changed.
});
// Later, when you are done listening for changes...
unlisten();
In the ModelFormsContainer access the passed history prop instantiate a listener when the component mounts, pass it a callback that updates the state.
Example:
useEffect(() => {
const unlisten = history.listen(() => {
console.log("route changed!!");
// apply business logic to set any component state
});
console.log("ModelFormsContainer mounted");
return unlisten;
}, []);
If ModelFormsContainer is a class component then obviously use componentDidMount and componentWillUnmount and save unlisten as a class instance variable, i.e. this.unlisten.

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

Move between pages without losing pages content - React JS

I'm trying to navigate between pages without losing pages data. For instance, I have a page that contains many input fields, if a user filled all these fields and tried to move to another page and return back to the first one, all the inputs are gone. I am using react-router-dom but didn't find out a way to prevent that.
What I've done till now :
import { Route, Switch, HashRouter as Router } from "react-router-dom";
<Router>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/hello-world" exact component={HellWorld} />
</Switch>
</Router>
Home Component :
navigateToHelloWorld= () => {
this.props.history.push('/hello-world')
};
Hello World Component :
this.props.history.goBack();
I don't know that that can be supported in such generality. I would just store all state variable values in localStorage, and restore from there when values are present on component render (when using useState then as the default value). Something like this:
const Home = () => {
const [field, setField] = useState(localStorage.field || '');
const handleUpdate = (value) => {
setField(value);
localStorage.field = value;
}
// also add a submit handler incl. `delete localStorage.field`;
return ... // your input fields with handleUpdate as handler.
}
Generally, all you need to do is to store your data in someplace, either a component that doesn't unmount, like the component you are handling your routes in, which is not a good idea actually but it works!
another way is to use some kind of state manager like 'mobx','redux','mst', or something which are all great tools and have great documentation to get you started
another alternative is to store your data in the browser, for your example session storage might be the one to go for since it will keep data until the user closes the tab and you can read it in each component mount.

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