Saving only a particular piece of state in localStorage w/ Redux - javascript

How to save in localStorage using Redux, only a particular piece of state?
For example, my state in list reducer is defined as follows:
state = {
companies: [],
currentDisplay: '',
recordNotFound: false,
}
This is my combineReducer file:
const rootReducer = combineReducers({
list: listReducer,
form: formReducer
})
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
}
}
And after browser reloads I want only companies: [{obj1}, {obj2}, ...] array to be preloaded and the rest of state reset to default values f.e. currentDisplay: '' to be equal ''.
Right now responsible for this operation code looks like this:
store.subscribe(() => {
saveState({
list: store.getState().list
})
})
And it stores the whole list obviously...
I guess I could easily reset these parameters in React using setState(), but would like to do this properly.

You can save just the companies parameter on localStorage if you don't need the other parameters to be loaded.
store.subscribe(() => {
saveState({
companies: store.getState().list.companies
})
})

Related

State is being resetted due to incorrect load function

I'm new to react-redux and have some trouble persisting the state, mainly problem with loading the state from localstorage. I can save data to localstorage with no problem, however the data inside redux dev tools resets on refresh. The data in localstorage is as it should be.
I speculate that the loadstate function is not working properly, thus not correctly fetching data from localstorage.
The reason for using (key, data) rather than (state) is that i don't get the error "objects are not valid as a react child", but it should work as good as using (state).
My localstorage.js
export const saveState = (key, data) => {
try {
const serialized = JSON.stringify(data);
localStorage.setItem(key, serialized);
} catch (err) {
// Ignore errors.
}
}
export const loadState = () => {
try {
const serializedState = localStorage.getItem('state');
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState);
} catch (err) {
return undefined;
}
};
my subscribe method:
store.subscribe(() => {
const state = store.getState();
Object.keys(state).forEach(
key => {saveState(key, state[key])}
)
})
My store class:
import allReducers from './reducers'
import {createStore} from 'redux';
import { loadState } from './localStorage';
export const store = createStore(allReducers, loadState(), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
It’s because you are trying to get from storage entire state but you are saving it by keys that’s why loadState is not returning state. Try something like this
store.subscribe(() => {
const state = store.getState();
saveState('state', state);
});

Async/Await seems to have different behaviour in class methods and function expressions (React Hooks, Redux-Thunk)

I'm migrating a class-based react system to hooks, and I'm facing some challenges which I can't understand.
Take a look at the snippet below:
async onSearchforOptions(elementId) {
await this.props.onFetchOperatingSystems()
//(3) [{…}, {…}, {…}]
console.log(this.props.operatingSystems)
}
In this method, I am dispatching an action to update the redux state, and right after this I'm logging the result to make sure the information was fetched and updated in the redux state.
The problem is that in an application which uses functional components, the result doesn't seem the same. Instead of updating the redux state and recovering the info right after, it simply doesn't seem to update the state, even if I'm using "await" and the very same actions and reducers the class component is using:
const onSearchforOptions = async (elementId) => {
await props.onFetchOperatingSystems()
//[]
console.log(props.operatingSystems)
}
My connection for both components (the class component and the functional component):
const mapStateToProps = state => {
return {
operatingSystems: state.operatingSystemReducer.operatingSystems
}
}
const mapDispathToProps = dispatch => {
return {
onFetchOperatingSystems: () => dispatch(actions.fetchOperatingSystems())
}
}
export default connect(mapStateToProps, mapDispathToProps)(productsForm)
My actions:
export const fetchOperatingSystemsStart = () => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_START
}
}
export const fetchOperatingSystemsFail = (error) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_FAIL,
error: error
}
}
export const fetchOperatingSystemsSuccess = (operatingSystems) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS,
operatingSystems: operatingSystems
}
}
export const fetchOperatingSystems = () => {
return dispatch => {
dispatch(fetchOperatingSystemsStart())
return axios.get(url)
.then(response => {
const fetchedData = []
for (let key in response.data) {
fetchedData.push({
...response.data[key],
id: response.data[key].id
})
}
dispatch(fetchOperatingSystemsSuccess(fetchedData))
})
.catch(error => {
if (error.response !== undefined) dispatch(fetchOperatingSystemsFail(error.response.data))
else dispatch(fetchOperatingSystemsFail(error))
})
}
}
My Reducer:
const initialState = {
operatingSystems: [],
loading: false
}
const fetchOperatingSystemsStart = (state) => {
return updateObject(state, { loading: true })
}
const fetchOperatingSystemsSuccess = (state, action) => {
return updateObject(state, { operatingSystems: action.operatingSystems, loading: false })
}
const fetchOperatingSystemsFail = (state) => {
return updateObject(state, { loading: false })
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_OPERATING_SYSTEMS_START: return fetchOperatingSystemsStart(state)
case actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS: return fetchOperatingSystemsSuccess(state, action)
case actionTypes.FETCH_OPERATING_SYSTEMS_FAIL: return fetchOperatingSystemsFail(state)
default: return state
}
}
export default reducer
updateObject function:
export const updateObject = (oldObject, updatedProperties) => {
const element = {
// The values of the object oldObject are being spread, at the same time the values of
// updatedProperties are (I'm taking out the attributes of both objects with the spread operator).
// In this case, since the names of the attributes are the same,
// the attributes (which were spread) of the first object will have their values replaced
// by the values of the second object's attributes.
...oldObject,
...updatedProperties
}
return element
}
My Goal:
Accoding to the snippet below, my goal is to dynamically search for options and update it in my form, which is in the component state.
const onSearchforOptions = async (elementId) => {
let elementUpdated
switch (elementId) {
case 'operatingSystem': {
await props.onFetchOperatingSystems()
console.log(props.operatingSystems)
elementUpdated = {
'operatingSystem': updateObject(productsForm['operatingSystem'], {
selectValue: {
value: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.id : undefined,
label: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.name : undefined
},
elementConfig: updateObject(productsForm['operatingSystem'].elementConfig, {
options: props.operatingSystems
})
})
}
break
}
case 'productType': {
await props.onFetchProductTypes()
elementUpdated = {
'productType': updateObject(productsForm['productType'], {
selectValue: {
value: props.selectedElement.productType ? props.selectedElement.productType.id : undefined,
label: props.selectedElement.productType ? props.selectedElement.productType.name : undefined
},
elementConfig: updateObject(productsForm['productType'].elementConfig, {
options: props.productTypes
})
})
}
break
}
default: break
}
const productsFormUpdated = updateObject(productsForm, elementUpdated)
setProductsForm(productsFormUpdated)
}
The props object passed to the render function initially is not going to be mutated; rather the props passed to your component on its next render will be updated. This is more in keeping with the flux architecture. You fire-and-forget an action, the reducer runs, and then your component is re-rendered with new props.
Before, this same thing was happening, but the new props were being assigned to this.props again. Since there's no meaningful "this" anymore, you can't use this pattern. Besides, depending on this behavior is not idiomatically the React way of doing things.
Update:
I think this is like a great number of cases I've also encountered where the React team seemed to overcorrect for a lot of use cases of people handling derived state poorly (see You Probably Don't Need Derived State). I've seen plenty of cases, like yours, where the now-deprecated componentWillReceiveProps lifecycle method solved this problem for class-based components very nicely.
Thankfully, useEffect now gives you something like a replacement. Think about it this way: when props.operatingSystems changes, you want to perform the effect of changing the state of your form. It's an unfortunate double update issue, but you had that before. Here's how you could go about writing that:
const [productsForm, setProductsForm] = useState(...);
useEffect(() => {
// Handle the case where props.operatingSystems isn't initialized?
if (!props.operatingSystems || !props.selectedElement.operatingSystem)
return;
setProductsForm({
...productsForm,
operatingSystem: {
...productsForm.operatingSystem,
selectValue: {
value: props.selectedElement.operatingSystem.id,
label: props.selectedElement.operatingSystem.name
},
elementConfig: {
...productsForm.operatingSystem.elementConfig,
options: props.operatingSystems
}
}
});
}, [props.operatingSystems]);
The way this works is that your effect code is only kicked off whenever your props.operatingSystems value changes since the last render. You can do a similar sort of effect for product types.
Another option which is maybe less elegant is for your async function that kicked off the redux actions to also resolve to a value which you can then use in your state setting code:
const operatingSystems = await props.onFetchOperatingSystems();
// ...now set your state
i usually implements thunks in a functional component like:
`export default connect(mapStateToProps, {fetchOperatingSystems})(productsForm)`
can you try this and comment back.

Using the same data dependently in the same useEffect

I need to fetch my data in two different ways and render it according to this. At the first load, I need to fetch all the items one by one and increment the count. After that, I need to fetch all the data at once and update the display. So, I wrote something like this (not the actual code but almost the same thing):
import React, { useEffect } from "react";
import axios from "axios";
import { useGlobalState } from "./state";
const arr = Array.from(Array(100), (x, i) => i + 1);
function App() {
const [{ posts }, dispatch] = useGlobalState();
useEffect(() => {
const getInc = () => {
arr.forEach(async id => {
const res = await axios(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
dispatch({
type: "INC",
payload: res.data
});
});
};
const getAll = async () => {
const promises = arr.map(id =>
axios(`https://jsonplaceholder.typicode.com/posts/${id}`)
);
const res = await Promise.all(promises);
dispatch({
type: "ALL",
payload: res.map(el => el.data)
});
};
if (!posts.length) {
getInc();
} else {
getAll();
}
}, [dispatch]);
return (
<>
<div>{posts.length}</div>
</>
);
}
export default App;
I'm simply using Context and useReducer to create a simple store. The above code works as it is but I skip adding posts.length dependency and this makes me think that my logic is wrong.
I tried to use refs to keep track the initialization state but I need to track the data at every route change. Then, I tried to keep it by adding an init state to my store but I couldn't make it work without problems. For example, I can't find a suitable place to dispatch the init. If I try it after a single fetch it triggers the initialization immediately and my other function (getAll) is invoked.
If anyone wants to play with it here is a working sandbox: https://codesandbox.io/s/great-monad-402lb
I added init to your store:
// #dataReducer.js
export const initialDataState = {
init: true,
posts: []
};
const dataReducer = (state, action) => {
switch (action.type) {
case 'ALL':
// init false
return { ...state, posts: action.payload };
case 'INC':
return { ...state, init: false, posts: [...state.posts, action.payload] };
...
}
// #App.js
function App() {
const [{ init, posts }, dispatch] = useGlobalState();
useEffect(() => {
init ? getInc(dispatch) : getAll(dispatch);
}, [init, dispatch]);
...
}

Vuex spams Error for no obvious reason on Commit

In my Plugin I have the following Code:
import firebase from 'firebase'
export default context => {
firebase.auth().onAuthStateChanged(userObject => {
// eslint-disable-next-line no-console
console.log({ userObject })
context.store.commit('auth/setUser', { userObject })
})
}
Here all is fine and the userObject is the correct thing.
Now in the store:
import Vue from 'vue'
export const state = () => ({
user: null
})
export const mutations = {
setUser(state, val) {
// eslint-disable-next-line no-console
console.log(val)
Vue.set(state, 'user', val)
}
}
Things get weird, once this triggers my Console gets spammed with Do not mutate vuex store state outisde mutation handlers but I do not see any place in where I do that? Searching for hours now placing stuff around but can´t solve the error, thanks in advance.
It is because the firebase user can be changed by firebase itself so you need to set the store with a clone of your userObject. Because it is probably a nested object you can make a deep clone like this:
export default context => {
firebase.auth().onAuthStateChanged(userObject => {
// eslint-disable-next-line no-console
let userOb = JSON.parse(JSON.stringify(userObject))
console.log({ userOb })
context.store.commit('auth/setUser', { userOb })
})
}
Also you aren't using Vue.set correctly. To set an object it should be like this:
Vue.set(state.user, key, value)
but I don't think you need vue set, you should be able to just assign it:
export const mutations = {
setUser(state, val) {
state.user = val
}
}
but if this messes up reactivity I'd initialise user as an array and set it like this:
export const state = () => ({
user: []
})
export const mutations = {
setUser(state, val) {
// eslint-disable-next-line no-console
console.log(val)
Vue.set(state.user, 0, val)
}
}

React native + redux-persist: how to ignore keys (blacklist)?

I'm storing my settings with redux-persist and would like to ignore some of them to have them reset on every restart, e.g. after a crashing.
It's possible to add an array of reducer-names as blacklist or whitelist, but I'd like to ignore specific keys, e.g. settings.isLoggedIn instead of settings.
// ...
function configureStore(initialState) {
const store = createStore(
RootReducer,
initialState,
enhancer
);
persistStore(store, {
storage: AsyncStorage,
blacklist: ['router', 'settings'] // works, 'settings.isLoggedIn' doesn't.
}, () => {
// restored
});
return store;
}
// ...
Do I have to create another reducer or does anyone a solution to this problem?
Thanks in advance!
As per the documentation, the blacklist parameter contains: 'keys (read: reducers) to ignore', so I am afraid it is not possible to implement the behaviour that you want. You can try and implement that functionality yourself, but I think the codebase of the package is really focused on blacklisting reducers instead of properties (see this). I am afraid that the only solution is to create a separate reducer for your non-persistent keys (in my experience it is not much of a hassle).
Use transforms for save separate fields, for example for username in redux-form MyForm inside state.form.MyForm:
const formName = `MyForm`
const formTransform = createTransform(
(inboundState, key) => {
return {
...inboundState,
[formName]: {
values: {
username: _.get(inboundState, `${ MyForm }.values.username`)
}
}
}
},
(outboundState, key) => {
return outboundState
},
{ whitelist: [`form`] }
)
persistStore(store, {
whitelist: [
`form`
],
transforms: [
formTransform
]
})
You can use Nested Persists for this.
import { persistStore, persistReducer } from 'redux-persist';
const rootPersistConfig = {
key: 'root',
storage: storage,
blacklist: ['auth']
}
// here you can tell redux persist to ignore loginFormData from auth reducer
const authPersistConfig = {
key: 'auth',
storage: storage,
blacklist: ['loginFormData']
}
// this is your global config
const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
other: otherReducer,
})
// note: for this to work, your authReducer must be inside blacklist of
// rootPersistConfig
const myReducerConfig = {
key: "cp",
storage: storage,
blacklist: ["authReducer"],
debug: true
};
you have to create reducer for every prop you want to save.
A simple solution is to save the whole reducer in the whitelist and after in the reducer using 'persist/REHYDRATE' action to filter only the keys that you want to keep.
Example:
// configureStore.js
const persistConfig = {
keyPrefix: 'webapp',
whitelist: ['filters'],
}
// filtersReducer.js
const projectsBase = {
[KEYS.SORT]: PROJECTS_SORT_TYPE.NAME,
[KEYS.TEXT]: '',
}
const itemsBase = {
[KEYS.SORT]: ITEMS_SORT_TYPE.INDEX,
[KEYS.TEXT]: '',
}
const base = {
[KEYS.PROJECTS]: projectsBase,
[KEYS.ITEMS]: itemsBase
}
export const filters = (state = base, action) => {
const { type } = action
switch (type) {
case PERSIST_REHYDRATE_ACTION_TYPE: {
if (action.payload.filters) {
const filters = action.payload.filters
const projectsSort = _.get(filters, [KEYS.PROJECTS, KEYS.SORT])
const itemsSort = _.get(filters, [KEYS.ITEMS, KEYS.SORT])
const newBase = { ...base,
[KEYS.PROJECTS]: {
[KEYS.SORT]: projectsSort
},
[KEYS.ITEMS]: {
[KEYS.SORT]: itemsSort
}}
state = newBase
}
}
break
default:
break
}
return state
}
As #martinarroyo mentioned to create a separate reducer which is a good option and if we follow it and create a seperate reducer for errors, we can simply return an empty state as the default;
const initialState = {
error: null
}
export default errorReducer = (state = initialState, action) => {
switch (action.type) {
...
default:
return {
...state,
error: null
}
}
}
This will clear the state everytime we visit the site as defualt is setting the errors to null.

Categories