React: Mapping a component with local storage data - javascript

I'm currently creating a history list component for a form in a react app and am having some trouble with the local storage.
Essentially, I want the app to render a list of past inputs from the user's local storage data. My current idea is based in duplicating the local storage data is a state variable.
const [history, setHistory] = useState([]);
On form submit I call this function with the form input as the parameter (the input is a single string)
const setLocalStorage = (input) => {
const hist = JSON.parse(localStorage.getItem('history')) || [];
console.log(hist)
hist.push(input)
localStorage.setItem('history', JSON.stringify(hist))
setHistory(hist);
}
This is meant to put the history from local story into hist, push the input that was just submitted into the existing array hist, and update the local storage with the new value. The state variable should then be updated with the most updated array of strings with the setHistory(hist) call.
Also, I want local storage to be pulled on first render so I can use that data to render the history list on initial load. I have a useEffect hook for this as shown:
useEffect(() => {
setHistory(JSON.parse(localStorage.getItem('history')))
console.log(history)
}, []);
The problem I'm facing is that the state never seems to get updated? I can instead do a console log for JSON.parse(localStorage.getItem('history')) and get the local storage array returned but this of course isn't helpful for data usage. I know that the local storage is properly being pulled from this but I'm unable to update the state for some reason. I need the state updated so I can conditionally render and use the array for mapping each item on the history list. When I console log "history" I get an empty array.
TL;DR
Concisely, what is the cleanest method to have local storage and state values maintain equivalency? Hope my post was clear enough to understand!
I'm remaking and updating a regular JS app on React for practice so I'm able to provide a live link of how I want this simple component to work.
https://giovannimalcolm.github.io/weather-dashboard/

The second returned parameter of useState is similar to the this.setState which is asynchronous. You may see that state is not changed even setHistory is called. Passing function instead of the value will avoid this issue as it will be executed after the state is updated. This might be useful for better understanding Passing function to setState()
useEffect(() => {
const hist = JSON.parse(localStorage.getItem('history'))
setHistory(prevHistory => [...prevHistory, ...hist])
}, []);

Related

How to Tie a Single localStorage Value Reactively to Redux?

I have a standard React/Redux app, that when loads, initially pulls localStorage values (via useEffect hook that triggers the reducers, etc.), and then stores/saves them when changed.
However, I have legacy code, that stores and saves one of these localStorage values, and saves it directly.
It's all on the same domain so the localStorage value is saved via the React app or legacy code. But I need a way for the React/Redux end to reactively update if localStorage is updated from the legacy code.
You can handle Window: storage event like so:
useEffect(() => {
const localStorageListener = (event) => {
const value = JSON.parse(window.localStorage.getItem('sampleValue'))
dispatch(updateValue(value));
};
window.addEventListener('storage', localStorageListener);
return () => {
window.removeEventListener('storage', localStorageListener);
};
}, []);
alternatively you could window.onstorage property... but IMO listener seems cleaner in React.

React - UseEffect hook with Redux store in dependency array and which updates the state ends in an endless loop

I have an array of certain objects in my redux store and I retrieve it like so:
const storeExpenses = useSelector(({ expenses }: RootState) => expenses.items));
I then save those expense objects also in the components local state since I have to further filter them without wanting to change them in the store.
const [expenses, setExpensesInState] = useState<Expense[]>(storeExpenses);
Now, when my store expenses are updated somewhere else, I want to refresh the local state as well, like so:
useEffect(() => {
setExpensesInState(storeExpenses));
}, [storeExpenses]);
However this results in an endless loop of the useEffect hook.
My assumption is that when I use setExpensesInState, I trigger a redraw of the component which then sets the expensesInStore variable which in turn triggers again the useEffect and so on. Is this assumption correct or am I misunderstanding anything else? And how would I resolve this to achieve what I need?

Fetch graphql query result on render and re-renders BUT LAZILY

I have a React Apollo app and what I am trying to do is that I have a component that renders some data using charts. For this data, I have some filters that I save in the local state of the component (Using hooks)
const [filters, setFilters] = useState(defaultFilters);
Now what I want is that whenever the component mounts, fetch the data using the default filters. But I also want to re-fetch data when the user updates the filters AND CLICKS ON SUBMIT and I'd fetch the results using new filters.
Since I also want to fetch the results on filter update, I am using useLazyQuery hook provided by apollo
const [getData, {data}] = useLazyQuery(GET_DATA_QUERY, { variables: {filters} });
useEffect(getData, []); // this useEffect runs only when the component mounts and never again
But, what happens is whenever my state, filters, updates the getData function is automatically run! ALWAYS! (BEHIND THE SCENE)
How do I handle such cases, where I want to fetch results on mounting and re-rendering.
I have tried using useQuery and refetch provided by it but I get the same problem there, whenever I update the state, the component rerenders and the useQuery hooks is run and makes the call. (That's how I believe it runs)
How do I fix my current code. Calling the getData function inside the useEffect function makes it run on every re-render.
I think I the problem defined in this stackoverflow-question is somewhat similar to mine.
Part of the problem is that you really have two different states that you're trying to utilize a single hook for. You have state that represents your inputs' values in the UI, and then you have state that represents the filters you want to actually apply to your charts. These are two separate bits of state.
The simplest solution is to just do something like this:
const [inputFilters, setInputFilters] = useState(defaultFilters)
const [appliedFilters, setAppliedFilters] = useState(inputFilters)
const { data } = useQuery(GET_DATA_QUERY, { variables: { filters: appliedFilters } })
const handleSubmit = () => setAppliedFilters(inputFilters)
const handleSomeInputChange = event => setInputFilters(...)
This way, you use inputFilters/setInputFilters only to manage your inputs' state. When the user clicks your submit button, the appliedFilters are set to whatever the inputFilters are at the time, and your query will update to reflect the new variables.

Two way data binding nested Form state to localStorage in React

I'm creating a form builder for users to create questions. Each question can have an infinite number of sub-questions and I want to save the data to localStorage.
What would be the best approach to dynamically update localStorage based on the state of each question/sub-question in the form since localStorage stores JSON? Is there a way to take Reacts one-way data flow principle and have its state run in parallel with local storage?
I think you can define a state variable in your form component at the container level and call localStorage.set('MY_QUESTIONS', questionsObj) whenever there is an update to the form so that the localStorage object is synced with the state variable.
For example, you can call it in the callback of every state change:
handleQuestionsUpdate() {
this.setState({
formQuestions: newQuestions
}, () => {
localStorage.setItem('MY_QUESTIONS', this.state.formQuestions)
});
}

How to maintain the state of React/Redux application across page refreshes?

I am new to react js.
I have two routes like A & B. Now i am passing some values from A to B as props. If B page is refreshed, then all props values from A is gone and B page is not rendering. I am using react with redux.
mapDispatchToProps & mapStateToProps functions are used to pass values between A & B routes as props.
For example: Route A has done some calculations and store the values in redux state and Route B is exported as connect(mapStateToProps, mapDispatchToProps)(B), by using mapStateToProps in which A's state values are passed to B as props.
Please suggest me the best way to handle browser refresh on above mentioned use case and also if any other best way to pass the values between routes. Thanks in advance.
Your question talks about two different concerns. First is passing props from one page to another in a React/Redux application, and second is maintaining the application state when the page is refreshed.
You've described the correct method of passing data between two routes in a redux based application.
Which brings us to the second concern.
How to maintain the state of a React/Redux application when the page is refreshed?
When a React/Redux application is refreshed, it gets initialised again and the redux store gets it's default values.
If you wish to maintain the app state across page refreshes or across different sessions, you need to store the state somewhere, and load it when the app initialises.
We can divide this problem into three parts:
Where to store the data
How to store redux state
How to reload the data when the application is initialised
Let's look at each sub-problem individually.
Where to store the data?
You can use the Web Storage API to store data within the user's browser. This API provides 2 mechanisms to store data:
sessionStorage: Stored data is preserved as long as the browser is open, including page reloads and restores.
localStorage: Data is preserved until it is cleared by the user or the application. It persists even if the browser is closed and reopened.
Both sessionStorage and localStorage allow you to store key-value pairs in the browser, and both provide the same set of functions to manage data.
For sessionStorage (example taken from MDN):
// Save data to sessionStorage
window.sessionStorage.setItem('key', 'value');
// Get saved data from sessionStorage
var data = window.sessionStorage.getItem('key');
// Remove saved data from sessionStorage
window.sessionStorage.removeItem('key');
// Remove all saved data from sessionStorage
window.sessionStorage.clear();
For localStorage:
// Save data to localStorage
window.localStorage.setItem('key', 'value');
// Get saved data from localStorage
var data = window.localStorage.getItem('key');
// Remove saved data from localStorage
window.localStorage.removeItem('key');
How to store redux state?
As you are already aware, Redux provides a createStore function which takes our root reducer and returns the application store.
The store object holds the entire application store, and provides a few methods including one to register a listener.
store.subscribe(listener) can be used to add a change listener to the store, which will get called every time the store gets updated.
We will add a listener to the store, which will save the application state to localStorage.
Try adding this in the file where you create your store using createStore:
/**
* This function accepts the app state, and saves it to localStorage
* #param state
*/
const saveState = (state) => {
try {
// Convert the state to a JSON string
const serialisedState = JSON.stringify(state);
// Save the serialised state to localStorage against the key 'app_state'
window.localStorage.setItem('app_state', serialisedState);
} catch (err) {
// Log errors here, or ignore
}
};
/**
* This is where you create the app store
*/
const store = createStore(rootReducer);
/**
* Add a change listener to the store, and invoke our saveState function defined above.
*/
store.subscribe(() => {
saveState(store.getState());
});
How to reload the stored data, and restore the application state when the app is initialised again?
When we create our app store using createStore, we have the option to pass an initial state to the store using the second parameter to the function.
When the application starts up, we will check the localStorage for any saved data. If we find it, we will send it as the second parameter to createStore.
This way, when the app finishes initialising, it will have the same state as it did before the page was refreshed or the browser was closed.
Try adding this in the file where you create your store using createStore:
/**
* This function checks if the app state is saved in localStorage
*/
const loadState = () => {
try {
// Load the data saved in localStorage, against the key 'app_state'
const serialisedState = window.localStorage.getItem('app_state');
// Passing undefined to createStore will result in our app getting the default state
// If no data is saved, return undefined
if (!serialisedState) return undefined;
// De-serialise the saved state, and return it.
return JSON.parse(serialisedState);
} catch (err) {
// Return undefined if localStorage is not available,
// or data could not be de-serialised,
// or there was some other error
return undefined;
}
};
/**
* This is where you create the app store
*/
const oldState = loadState();
const store = createStore(rootReducer, oldState);
That's it! Now, combine the last two blocks of code, and your application has the ability to maintain state across page refreshes, or even across browser restarts.
Hope this helps.
Cheers!
:)
you can try redux-persist or redux-storage ,
when you initialize the store
createStore(reducer, [preloadedState], [enhancer]),
you can get the data and assign it to preloadedState
Try using react-router and this will render components based on route.
When the app is initialized, app should fetch data and update the store with the required information.
Eg: In the below example, When IntilizeApp component is mounting compute the information required and update the store by dispatching the actions. Use react's life cycle method like componentWillMount to compute.
import {Router, Route, hashHistory} from 'react-router'
// import initializeApp
// import ComponentA
// import ComponentB
<Router history={hashHistory}>
<Route path="/" component={InitializeApp}>
<Route name="A" path="A" component={ComponentA} />
<Route name="B" path="B" component={ComponentB} />
</Route>
</Router>

Categories