How do I pass a new key as an array with immer? - redux

Say I have some initial state like
const initialState = {
loading: false,
updating: false,
saving: false,
data: {},
error: null
};
And I want add to data as the result of an action but the data I want to add is going to be an array. How do I go about this?
I've tried
export default produce((draft, action) => {
switch (action.type) {
case UPDATE_STATE.SUCCESS:
draft.data.new_Array.push(action.payload);
draft.loading = false;
break;
default:
}
}, initialState);
but this errors.
If I start the initial state as
const initialState = {
loading: false,
updating: false,
saving: false,
data: {
newArray: []
},
error: null
};
any update to the state before I make the array key overwrites the initial state and removes the key. ie
export default produce((draft, action) => {
switch (action.type) {
case OTHER_UPDATE_STATE.SUCCESS:
draft.data = payload.action;
draft.loading = false;
break;
default:
}
}, initialState);
can anyone help?

There's one thing I noticed you'll run into trouble, draft.data.new_Array.push(action.payload);
Make sure you don't modify the existing data structure. The reason is that redux relies on the memory reference of an object, if the object reference doesn't change, it might fool the redux that nothing actually happened in the past.
In your case, i have a feeling that nothing will ever get triggered.
One way to modify the reference is to create an new object, ex.
return [...data, newElementObject]

Related

Redux Toolkit - assign entire array to state

How can I assign an entire array to my intialState object using RTK?
Doing state = payload or state = [...state, ...payload] doesn't update anything.
Example:
const slice = createSlice({
name: 'usersLikedPosts',
initialState: [],
reducers: {
getUsersLikedPosts: (state, { payload }) => {
if (payload.length > 0) {
state = payload
}
},
},
})
payload looks like this:
[
0: {
uid: '1',
title: 'testpost'
}
]
update
Doing this works but I don't know if this is a correct approach. Can anyone comment?
payload.forEach((item) => state.push(item))
immer can only observe modifications to the object that was initially passed into your function through the state argument. It is not possible to observe from outside the function if that variable was reassigned, as it only exists in the scope within the function.
You can, however, just return a new value instead of modifying the old one, if you like that better. (And in this case, it is probably a bit more performant than doing a bunch of .push calls)
So
return [...state, ...payload]
should do what you want.

Flow errors when dealing with nullable types

I have working on a redux reducer with the following state:
export type WishlistState = {
+deals: ?DealCollection,
+previousWishlist: ?(Deal[]),
+currentWishlist: ?(Deal[]),
+error: ?string
};
export type DealCollection = { [number]: Deal };
export const initialState: WishlistState = {
deals: null,
previousWishlist: null,
currentWishlist: null,
error: null
};
export default function wishlistReducer(
state: WishlistState = initialState,
action: WishlistAction
): WishlistState {
switch (action.type) {
case "GET_DEALS_SUCCESS":
return { ...state, deals: action.deals };
case types.GET_WISHLIST_SUCCESS:
console.log(action);
const currentWishlist: Deal[] = action.wishlistIds.map(
// ATTENTION: THIS LINE HERE
d => state.deals[d]
);
return {
...state,
currentWishlist,
previousWishlist: null,
error: null
};
// ...other cases
default:
return state;
}
}
The line I've flagged with the comment is getting a flow error on the d in the
brackets:
Cannot get `state.deals[d]` because an index signature declaring the expected key/value type is missing in null or undefined.
This is happening because of the type annotation: deals: ?DealCollection, which is made clearer if I change the line to this:
d => state.deals && state.deals[d]
Which moves the error to state.deals; and the idea is that if state.deals is null, then the callback returns null (or undefined), which is not a acceptable return type for a map callback.
I tried this and I really thought it would work:
const currentWishlist: Deal[] = !state.deals
? []
: action.wishlistIds.map(d => state.deals[d]);
It would return something acceptable if there are no deals is null, and never get to the map call. But this puts the error back on the [d] about the index signature.
Is there any way to make Flow happy in this situation?
Flow invalidates type refinements whenever a variable may have been modified. In your case, the thought of checking !state.deals is a good start; however, Flow will invalidate the fact that state.deals must have been a DealCollection because (theoretically) you could be modifying it in your map function. See https://stackoverflow.com/a/43076553/11308639 for more information on Flow type invalidation.
In your case, you can "cache" state.deals when you have refined it as a DealCollection. For example,
type Deal = string; // can be whatever
type DealCollection = { [number]: Deal };
declare var deals: ?DealCollection; // analogous to state.deals
declare var wishlistIds: number[]; // analogous to action.wishlistIds
let currentWishlist: Deal[] = [];
if (deals !== undefined && deals !== null) {
const deals_: DealCollection = deals;
currentWishlist = wishlistIds.map(d => deals_[d]);
}
Try Flow
that way you can access deals_ without Flow invalidating the refinement.

seamless-immutable is not updating state consistently

I'm working with seamless-immutable and redux, and I'm getting a strange error when updating the state. Here's my code, without the bits like the action return or combineReducers. Just the junk that's running/causing the error.
Initial State
{
things: {
fetching: false,
rows: []
}
}
Action Handler
export default {
[DEALERS_REQUEST]: (state, action) => {
return Immutable({ ...state, fetching: true });
},
[DEALERS_RECEIVE]: (state, action) => {
return Immutable({ ...state, rows: action.payload, fetching: false });
},
Middleware with thunk
export const thingsFetch = (data) => {
return (dispatch, getState) => {
dispatch(thingsRequest());
dispatch(thingsReceive(data));
}
}
Now, what's weird is, if I run these two actions together, everything is fine.
If I only dispatch thingsRequest(), I get a "cannot push to immutable object" error.
I've tried using methods like set, update, replace, merge, but they usually return with "this.merge is not a function".
Am I doing something wrong procedurally or should I contact the module dev to report an issue?
This issue on this was that, on the case of an empty array, the component was trying to write back to the Immutable object with it's own error message.
To get around this, I pass the prop as mutable. There's also some redux-immutable modules that replace the traditional connect function to all the app to pass mutable props to components while maintaining immutability in the state.

Does data that will never change also belong into the store?

Some parts of my initial state will never change during the whole lifecycle of my app. Now I wonder if this kind of data also belong into the store?
If yes:
Is there a way to put this data in the initial state when calling createStore(), without having an (empty) corresponding reducer function? Because since the data never changes there's no need for a reducer, but combineReducers() is pushing me to have one, otherwise it throws this error:
Unexpected key "keyName" found in initialState argument passed
to createStore. Expected to find one of the known reducer keys
instead: "otherKey1", "otherKey2". Unexpected keys will be ignored.
Example of what I'm looking for:
var dataThatWillChange = function(state, action) { /* reduce */ };
var myApp = Redux.combineReducers({
dataThatWillChange: dataThatWillChange,
dataThatWillNeverChange: Redux.dummyReducer // <-- something like this?
});
var store = Redux.createStore(myApp, {
dataThatWillChange: [0, 1, 2],
dataThatWillNeverChange: { createdBy: "me" } // <-- no need for a reducer
});
you could just have a reducer that always returns the initial state, there is nothing wrong with that:
var initialState = { createdBy: "me" }
var dataThatWillNeverChange = function(state=initialState, action) {
return state;
};
var store = Redux.createStore(myApp, {
dataThatWillChange: DataThatWillChange,
dataThatWillNeverChange: dataThatWillNeverChange
});
or more compactly:
var initialState = { createdBy: "me" }
var store = Redux.createStore(myApp, {
dataThatWillChange: DataThatWillChange,
dataThatWillNeverChange: () => initialState
});
If your constant data doesn't really belong to the "application state", you could also consider exporting it from some module and importing it whenever you need it, like
export default {
createdBy: "me",
...
}

Can combineReducers work with extra actions?

What I want is the root reducer combine other reducers, and listen to extra actions. I've find the docs, but I can not get any information.
Here is some pseudo code.
const root1 = combineReducers({
reducer1,
reducer2,
reducer3,
reducer4
});
function root2(state = initState, action) {
switch (action.type) {
case LOAD_DATA:
return _.assign({}, initState, action.data);
default:
return state;
}
}
merge(root1, root2);
The only way I figure out is to drop combineReducers:
function root(state = initState, action) {
switch (action.type) {
case LOAD_DATA:
return _.assign({}, initState, action.data);
case ...: return ...;
case ...: return ...;
case ...: return ...;
default: return state;
}
}
Is there another way to implement this?
Yes, you can use combineReducers() with multiple reducers while also having an action that rebuilds your entire application state. Admittedly, that is a bit of a strange design decision and does not scale very well with more complex apps, but you obviously have a use-case. If you want to do something like that you have two choices.
Option 1: Divide up action
It is totally valid to listen for the same action type within multiple reducer functions. This is the most straightforward approach, although it involves more repetition. You would just break out each piece of state returned by your action into the individual reducer functions it applies to.
For instance, if this was your entire application state
{
foo: {},
bar: {}
}
And your action type that rebuilt the entire application state was LOAD_DATA, you could do this
function foo (state = {}, action) {
switch (action.type) {
case 'LOAD_DATA':
return {...state, action.result.foo}
}
}
function bar (state = {}, action) {
switch (action.type) {
case 'LOAD_DATA':
return {...state, action.result.bar}
}
}
const root = combineReducers({
foo,
bar
});
With that, both foo and bar in your state would always get rebuilt with the corresponding data coming from the same action.
Option 2: Build Custom combineReducers()
There is nothing stopping you from building your own version of combineReducers(). If you watch this video on building a combineReducers() function from scratch, you'll see that the logic in place is not that complicated. You would just have to listen for the specific action type and return the entire state from that action if it matched. Here's a version of that I built by looking at the current source for combineReducers() and then working the 2 util functions into that function
function combineReducers(reducers) {
var fn = (val) => typeof val === 'function';
var finalReducers = Object.keys(reducers).reduce((result, key) => {
if (fn(reducers[key])) {
result[key] = reducers[key]
}
return result
}, {});
return function combination(state = {}, action) {
if (action.type === 'LOAD_DATA') {
return completeStateReducer(action)
} else {
var hasChanged = false
var fn = (reducer, key) => {
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
var errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
return nextStateForKey
}
var finalState = Object.keys(finalReducers).reduce((result, key) => {
result[key] = fn(finalReducers[key], key)
return result
}, {})
return hasChanged ? finalState : state
}
}
}
function completeStateReducer(action) {
return action.result;
}
Outside of merging those util functions back in, the only thing I really added was the bit about listening for the LOAD_DATA action type and then calling completeStateReducer() when that happens instead of combining the other reducer functions. Of course, this assumes that your LOAD_DATA action actually returns your entire state, but even if it doesn't, this should point you in the right direction of building out your own solution.
First, combineReducers is merely a utility function that simplifies the common use case of "this reducer function should handle updates to this subset of data". It's not required.
Second, that looks like pretty much the exact use case for https://github.com/acdlite/reduce-reducers. There's an example here: https://github.com/reactjs/redux/issues/749#issuecomment-164327121
export default reduceReducers(
combineReducers({
router: routerReducer,
customers,
stats,
dates,
filters,
ui
}),
// cross-cutting concerns because here `state` is the whole state tree
(state, action) => {
switch (action.type) {
case 'SOME_ACTION':
const customers = state.customers;
const filters = state.filters;
// ... do stuff
}
}
);
Also, I give an example of using reduceReducers in the "Structuring Reducers" section of the Redux docs: http://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html .

Resources