How To Update Structure of Redux State Persisted In LocalStorage - javascript

I save a part of my Redux state in localStorage so that I can persist it across page refreshes.
How do I update the structure of the state in my localStorage if my Redux state structure changes?
Example:
This is my combineReducer file:
// combineReducer
const rootReducer = combineReducers({
usersPage: usersPageReducer,
form: formReducer
})
The original default state in my usersPageReducer looks like:
// usersPageReducer.js
defaultUsersPageState = {
selectedColumns: ["name", "email"],
}
I load and save to localStorage using the following methods:
// localStorage.js
export const loadState = () => {
try {
const serializedState = localStorage.getItem('state')
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState)
} catch (err) {
return undefined
}
}
export const saveState = (state) => {
try {
const serializedState = JSON.stringify(state)
localStorage.setItem('state', serializedState)
} catch (err) {
// to define
}
}
In my main file, I subscribe to the store, and save the usersPage slice of the state. And I create the store using initial state from localStorage:
// main.js
const persistedState = loadState();
const initialState = { ...persistedState };
const store = createStore(
rootReducer,
initialState,
);
store.subscribe(() => {
saveState({
usersPage: store.getState().usersPage
})
})
Now imagine I want to change my usersPageReducer default state so that it also keeps track of sorting.
// usersPageReducer.js
defaultUsersPageState = {
selectedColumns: ["name", "email"],
sorting: {"name": "asc"}
}
The above won't work because at the start of the app, we hydrate the redux initial state from localStorage. The old initial state beats out the usersPageReducer defaultUsersPageState.
Since localStorage doesn't know about the sorting key for usersPageReducer, and we never use the default state, the sorting key is never added to the redux state.
Is there a way to hydrate the redux store so that it initializes every reducer using it's default state (as if we weren't using localStorage), then spreads the stored state?

There are two possible solution I can suggest you:-
1. Use a variable to keep track of the version of your application (even better redux-version ). Now whenever value of this variable is changed, your application will clear-out the user's LocalStorage and will use the brand new structure of your redux ( or its sub-part ) provided by defaultUsersPageState rather than loading the available ( stale ) data from it.
You can achieve this in multiple ways, mine is
// localStorage.js
export const loadState = (CurrVersion) => {
try {
// check if version matches or not ...
// for first time localStorage.getItem('appVersion') will be null
if (CurrVersion !== localStorage.getItem('appVersion')){
localStorage.removeItem('state') //
// update the appVersion in LS with current version
localStorage.setItem('appVersion', CurrVersion)
return undefined;
}
else{
const serializedState = localStorage.getItem('state')
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState)
}
} catch (err) {
return undefined
}
}
And in main.js
// change it whenever breaking changes are added and it makes sense
const CurrVersion = 'MY-APP-VERSION-2.0'
const persistedState = loadState(CurrVersion);
const initialState = { ...persistedState };
const store = createStore(
rootReducer,
initialState,
);
store.subscribe(() => {
saveState({
usersPage: store.getState().usersPage
})
})
Check explicitly if the structure of the persisted data is changed or not. If changed, you can delete the former object and use the default initial state provided which has updated structure. But keep in mind that this check needs to done recursively (or deeply), you may find lodash useful for this.
P.S : Use npm packages built for this problem e.g react-persist if you have big projects .

The hash method can be used to implement this.
Compute the hash of the initial redux keys on the application load and store it in the local storage of the browser
Now calculate the hash of the redux keys present in the application and compare it with the hash present in the browser's local storage.
If they both match, then no action needs to be taken.
If they don't, then either log them out or update the current redux store in the local storage
sha256 hash can be used to accomplish this
Example
import { sha256 } from "js-sha256";
const hash = sha256(digest);
INPUT
{"auth":"{\"email\":null,\"token\":null,\"error\":null,\"loading\":false,\"authRedirectPath\":\"/dashboard\",\"role\":null,\"clientId\":null,\"registerMessage\":null,\"signupCreds\":{}}
OUTPUT
29e9cceee4e2df4c53d134dedd0d171939caaab412b17332b3d864693dc62780

Related

Creating a var referencing a store from redux

I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store

redux reducer not re-rendering components

Context: I am trying to make a web client that uses react redux and socket.io. The design is inspired by whatsapp and is honestly just a fun little side project I am using to learn react and redux.
The main issue is I have a ActiveChat component that does not re-render upon the store changing and recognizing the change . Redux Devtools even shows the diff and change in state.
The component has been connected:
//Redux Mapping for Store and Actions
const mapStateToProps = state => {
return { activeChat: state.activeChat };
};
const mapDispatchToProps = dispatch => {
return {
updateActiveChat: chat => dispatch(updateActiveChat(chat))
}
}
const activeChatConnected = connect(mapStateToProps,mapDispatchToProps)(ActiveChat)
export default activeChatConnected;
I had the idea that I may somehow not be keeping the state pure as this is my first tango with state immutability in javascript and was hoping i'd receive help for the commmunity
The code is available here : https://github.com/YourFavouriteOreo/ChatClient ( Feedback is ALWAYS welcome as I am trying to get better at javascript )
The code snippet in question specifically is :
# src/reducers
const rootReducer = (state = initialState,action) => {
switch(action.type){
case SELECT_ACTIVE:
// Select Active Chat so as to display chat content
var newActive = Object.assign(state.chats[action.payload.index])
newActive["index"]= action.payload.index
return {...state,activeChat:newActive}
case UPDATE_CHAT:
// Update store with new Chat Content
var chats = Object.assign(state.chats)
chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...state,chats}
default:
return state
}
}
I have currently hotfixed this by setting state right after the action but this is not ideal as once I'll be using sockets , setState being async could lead to certain issues.
inputHandler = logs => {
// Handle Input from chatInput
var newState = this.state.chatLogs;
newState.push(logs);
//Redux Action
this.props.updateActiveChat(logs)
console.log(this.state.chatLogs);
// BAD HOTFIX
this.setState({});
};
Edit: This was asked so I will add it here . The return {...state,chats} does in-fact get result into return {...state, chats:chats}
EDIT2:
// actions
import {SELECT_ACTIVE, UPDATE_CHAT} from "../constants/action-types"
export const selectActiveChat = selected => ({type: SELECT_ACTIVE, payload:selected})
export const updateActiveChat = chat => ({type: UPDATE_CHAT, payload:chat})
EDIT 3 :
// render function for activeChat component
render() {
if (this.state != null){
return (
<div className="column is-8 customColumn-right">
<div className="topColumn">
<h1 style={{fontFamily:"Quicksand,sans-serif", fontWeight:"bold", fontSize:"1.1rem"}}> {this.state.chatName} </h1>
<p style={{fontFamily:"Roboto,sans-serif",marginLeft: "0.75rem",lineHeight:"1"}}> Chat Participants </p>
</div>
<ChatContent
chatLogs={this.props.activeChat.chatLogs}
isTyping={this.state.isTyping}
/>
<ChatInput postSubmit={this.inputHandler} />
</div>
);
}
else {
return <NoActiveChat/>
}
}
componentWillReceiveProps(newProps){
// Change Props on Receive
console.log("das new props");
this.setState({
chatName: newProps.activeChat.chatName,
chatLogs: newProps.activeChat.chatLogs,
isTyping: newProps.activeChat.isTyping
})
}
I managed to fix it by executing concat differently in the reducer. I don't understand how this changed the end result since the object still changes but this seemed to fix .
Instead of :
case UPDATE_CHAT:
// Update store with new Chat Content
console.log("Update chat action executed");
var chatState = Object.assign({},state)
chatState.chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...chatState}
I did:
case UPDATE_CHAT:
// Update store with new Chat Content
console.log("Update chat action executed");
var chatState = Object.assign({},state)
chatState.chats[state.activeChat.index].chatLogs = chatState.chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...chatState}
EDIT: It turns out that Object.assign() does a shallow clone, this means that nested objects are references rather than copies . If you want to do a deepClone . You can use cloneDeep from lodash, I realized this with other problems I ended up getting later with other functions .
The new case handler is as follows in my solution.
_ is my import for lodash
case UPDATE_CHAT:
// Update store with new Chat Content
console.log("Update chat action executed");
var chatState = _.cloneDeep(state)
chatState.chats[state.activeChat.index].chatLogs = chatState.chats[state.activeChat.index].chatLogs.concat(action.payload)
return {...chatState}

Redux reducer - on state change

I want to save the current state of a specific reducer into session storage without explicitly calling it on every action.
const { handleActions } = require("redux-actions");
// How do i make this logic run on any state change without adding it to every action?
const onStateChange = (state) => sessionStorage.save(keys.myReducer, state);
const myReducer = handleActions
({
[Actions.action1]: (state, action) => (
update(state, {
prop1: {$set: action.payload},
})
),
[Actions.action2]: (state, action) => (
update(state, {
prop2: {$set: action.payload},
})
)
},
//****************INITIAL STATE***********************
{
prop1: [],
prop2: []
}
);
Is there a way to catch the state change event for a specific reducer?
I know of store.subscribe, but that's for the entire store, i'm talking about listening to a specific reducer change
Thanks
Unfortunately you can't watch specific parts of your store because your store is actually an only reducer (combineReducers return a single big reducer). That said you can manually check if your reducer has changed by doing state part comparison.
let previousMyReducer = store.getState().myReducer;
store.subscribe(() => {
const { myReducer } = store.getState();
if (previousMyReducer !== myReducer) {
// store myReducer to sessionStorage here
previousMyReducer = myReducer;
}
})
You can use redux-saga to listen for specific actions and persist the store (and do your other logice) when they are fired. This seems like a good fit for your needs.
It's not exactly tied to specific store changes, but rather to a set of actions. However, I think this model (listening for actions, as opposed to state changes) is better suited to the redux design.
The nice thing is you don't have to change any code outside of the saga. I've used this one for autosaving form data and it worked great.
Here's an example of setting up a saga to persist on a few redux-form actions; note that it's not limited to persisting - you can do whatever you want during a saga.
function* autosaveSaga(apiClient) {
yield throttle(500,
reduxFormActionTypes.CHANGE,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_PUSH,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_POP,
saveFormValues,
apiClient);
}
If you want to listen to all action types, or do custom filtering of which actions fire your persistence code, you can pass a pattern, or a matching function to takeLatest:
yield takeLatest(
'*',
saveFormValues,
apiClient);
More on what you can pass to take, takeLatest etc can be found in the docs for take.

Add logic to the store?

I have a redux application with a "campaign" reducer/store.
Currently I have repeated code to check if a specific campaign is loaded or needs an API call to fetch details from the DB. Much simplified it looks like this:
// Reducer ----------
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
items: {... campaignList}
}
}
// Component ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Now I don't like to repeat the complex condition for needFetch. I also don't like to have this complex code in the mapStateToProps function at all, I want to have a simple check. So I came up with this solution:
// Reducer NEW ----------
const needFetch = (items) => (id) => { // <-- Added this function.
if (!items[id]) return true;
if (!items[id].meta) return true;
if (!items[id].meta.loaded && !items[id].meta.loading) return true;
return false;
}
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
needFetch: needFetch(campaignList), // <-- Added public access to the new function.
items: {... campaignList}
}
}
// Component NEW ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: state.campaign.needFetch(campaignId), // <-- Much simpler!
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Question: Is this a good solution, or does the redux-structure expect a different pattern to solve this?
Question 2: Should we add getter methods to the store, like store.campaign.getItem(myId) to add sanitation (make sure myId exists and is loaded, ..) or is there a different approach for this in redux?
Usually computational components should be responsible for doing this type of logic. Sure your function has a complex conditional check, it belongs exactly inside your computational component (just like the way you currently have it).
Also, redux is only for maintaining state. There's no reason to add methods to query values of the current state inside your reducers. A better way would be having a module specifically for parsing your state. You can then pass state to the module and it would extract the relevant info. Keep your redux/store code focused on computing a state only.
Your approach is somewhat against the idiomatic understanding of state in redux. You should keep only serializable data in the state, not functions. Otherwise you loose many of the benefits of redux, e.g. that you can very easily stash your application's state into the local storage or hydrate it from the server to resume previous sessions.
Instead, I would extract the condition into a separate library file and import it into the container component where necessary:
// needsFetch.js
export default function needsFetch(campaign) {
return campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading);
}
// Component ----------
import needsFetch from './needsFetch';
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: needsFetch(campaign),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);

How do I share readonly state between two or more reducers

I need access to user editable state from two or more reducers. Is there a way to access state controlled by another reducer without passing it to the reducer through the action's payload? I want to avoid having every action send user settings to reducers.
State
{
userSettings: {
someSetting: 5
},
reducer1State: {
someValue: 10 // computed with userSettings.someSetting
},
reducer2State: {
someOtherValue: 20 // computed with userSettings.someSetting
}
}
From the reducer1 I would like to get at userSettings.someSetting using something like the following:
function update(state={}, action) {
if (action.type === constants.REDUCER_1.CALCULATE) {
return _.assign({}, state, {
someValue: 2 * GETSTATE().userSettings.someSetting
});
}
...
I do not want to have to send userSettings from the action like this:
export function calculate(userSettings) {
return {
type: constants.REDUCER_1.CALCULATE,
userSettings: userSettings
};
}
One of the golden rules of Redux is that you should try to avoid putting data into state if it can be calculated from other state, as it increases likelihood of getting data that is out-of-sync, e.g. the infamous unread-messages counter that tells you that you have unread messages when you really don't.
Instead of having that logic in your reducer, you can use Reselect to create memoized selectors that you use in your connectStateToProps function, to get your derived data, e.g. something along the line of this:
const getSomeSettings = state => state.userSettings.someSetting;
const getMultiplier = state => state.reducer1.multiplier;
const getSomeValue = createSelector([getSomeSettings, getMultiplier],
(someSetting, multiplier) => {
});
const mapStateToProps(state) => {
return {
someValue: getSomeValue(state)
}
}
const MyConnectedComponent = connect(mapStateToProps)(MyComponent);

Categories

Resources