Redux-Saga using Immutable.js and state is always empty - reactjs

I am trying to wrap my head around redux and sagas and I think I have set something up wrong and i'm hoping someone can lend some insight.
I have created my store with my inital state and I dispatch an action, as seen here:
const initialState = fromJS({
product: {},
basket: {},
global: {}
});
const reducers = combineReducers({ product, basket, global });
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers,
initialState,
applyMiddleware(sagaMiddleware))
initSagas(sagaMiddleware);
store.dispatch(retrieveSiteKeyValues())
return store;
};
Combine Reducers is from redux-immutable.
My saga function:
export function* fetchSiteKeyValuesSaga() {
yield take(RETRIEVE_SITE_KEY_VALUES)
const siteKeyValues = yield call(retrieveSiteKeyValues)
yield put(storeSiteKeyValues(siteKeyValues));
}
My reducer function:
const storeSiteKeyValues = (state, payload) => {
payload.anotherObject = {};
payload.anotherMap = new Map();
const newState = fromJS({ payload })
return newState
// OR return state.merge({ global: { siteKey: action.siteKey } }); ?
}
When I interrogate the state object the size is zero. I expected the size to be at least 3 due to my initalState. When the newState is 'created' the size is 4. But when it drops back into the state switch statement, the state size is zero again:
export default (state, action) => {
switch (action.type) {
case STORE_SITE_KEY_VALUES : {
return storeSiteKeyValues (state, action.payload);
}
default:
return state;
}
}
Im 90% sure just dumping over the state as I am doing in the reducer function is wrong and i should be using set() or setIn() i thought update() would make more sense, but when I use those methods the state is always empty or 'undefined' if I try to enact .get(x) in the console.
When I inspect the state in the browser it looks like this:
storeState:{
[2],
[2]
[2]
}
The array expanded looks like this:
0:"product"
1:{anotherObject :{}, anotherMap: map()
size:1
I would expect the values that were part of of the payload to be here not just the new object and map.
Am I initaiting my state incorrectly at the store creation? Am I approaching redux and state management in the wrong way?

I want to be sure you aren't missing a fundamental part: where is the sagaMiddleware.run(YOUR_SAGA); call? Is it hidden inside initSagas?

It was setting my inital state twice, once when I was initialsing my store and again when the reducer inital state was read. My state in my reducer was an empty object as it would be if on the time of reducer 'activation'. In the end I realised i'm not reading some 'remembered' state from anywhere, I just needed some inital values. Which I moved into the reducer and remvoed the immutable js from my app as it was confusing matters.
Some lessons for all you newbies to react/redux-saga! Don't try and over complicate matters. Learn what immutable mean! Figure out for yourself if you need it, in my case just having one source of truth and access to state was enough.
Further reading:
Initalising State ,
Immutable and State considerations

Related

Why can't I update the state using Redux?

I'm trying to integrate Redux in a project that works already. I've configured the Redux store using multiple slices, here's the one causing troubles:
const initialCategoriesState = [];
const categoriesSlice = createSlice({
name: "categories",
initialState: initialCategoriesState,
reducers: {
setCategories(state, action) {
state = action.payload;
},
},
});
In my component I'm using useSelector to access the state:
const categories = useSelector(state => state.categories);
And to update it I dispatch an action, accordingly with the one declared in the slice:
const fetchedCategories = await fetchData(urlCategories, CATEGORIES);
dispatch(categoriesActions.setCategories(fetchedCategories));
But once I run the code, the categories constant gets never updated. Since I wasn't sure the action was getting the data, I tried to console.log the state inside the reducer it as it follows:
reducers: {
setCategories(state, action) {
console.log("state before", state);
state = action.payload;
console.log("state after", state);
},
},
The state is indeed changing, but not the extracted state in the categories constant.
Is there something I'm missing here?
I've got the same problem a few months ago and solved it this way. But, I may be missing something too.
Please try this for your initial state:
const initialCategoriesState = { categories: [] };
And then in your reducer:
state.categories = action.payload;
I've found one of the cool things about Redux Toolkit to be it's Entity Adapters, which can safely initialize your state while providing helper functions for updating your state. They (reasonably) assume that your slice will have collections, and probably a main collection of things with the same name as the slice. createEntityAdapter() allows you to do like:
const categoriesAdapter = createEntityAdapter()
// name the below "initialState" exactly
const initialState = categoriesAdapter.getInitialState({
// this will, by default, get you an `entities{}` and `ids[]` representing your categories
// you can add any additional properties you want in state here as well
})
Then, when you are wanting to update state, in your reducer you can do like:
reducers: {
setCategories(state, action) {
categoriesReducer.setAll(state, action.payload)
// this will update both entities{} and ids[] appropriately
},
},

Do we always have to treat states as immutable in Reducer?

I'm a beginner in React & Redux and I am confused with manipulating the state in Reducers.
In most of the articles, documentations, I keep seeing that the states are immutable and we should never update the state. We should always use ...state or object.assign in the reducers
However, in famous tutorials (Cory House or other places (Eg. Here on GitHub) , they update the state the directly like the following:
var initialState = {
numberOfAjaxCall: 0
}
const ajaxStatusReducer = (state = initialState.numberOfAjaxCall, action) => {
if (action.type === AJAX_BEGIN_CALL) {
return state + 1;
}
return state;
}
Why these codes are not written like the following?
var initialState = {
numberOfAjaxCall: 0
}
const ajaxStatusReducer = (state = initialState, action) => {
if (action.type === AJAX_BEGIN_CALL) {
return {
...state,
numberOfAjaxCall: state.numberOfAjaxCall + 1
};
}
return state;
}
I would like to know whether my code is wrong or not. Or I misunderstood about Redux & Reducers or don't understand the way these codes are implemented?
Could you please help me to enlighten about these coding styles?
The first example doesn't mutate state - it returns a new number. In that case the reducer is responsible for only the one number.
If your state is shaped like in the example you gave:
var initialState = {
numberOfAjaxCall: 0
}
The reducer ajaxStatusReducer is responsible for numberOfAjaxCall only. You will still need another reducer for the overall state object, which could look something like this (simplest option, lots of other ways you could write this):
function reducer(state, action) {
return {
numberOfAjaxCall: ajaxStatusReducer(state.numberOfAjaxCall, action)
};
}
In the second example, you combine both of these reducers into one. Both are valid options and it depends on how you like to compose your code/reducers in general.

React-redux - state overwrites itself

I am using react-redux (for the first time). I have a component into which users put a 'startDate' and an 'endDate'. These should then be stored in the redux store, so that they persist.
I have the following action creator:
export const setDates = dates => ({
type: "SET_DATES",
payload: dates
});
The following reducer:
const dates = (state = {}, action) => {
switch (action.type) {
case "SET_DATES":
return action.payload;
default:
return state;
}
};
export default dates;
The state is set conditionally (i.e. only if the start and end dates actually make sense) like this:
handleSubmit = () => {
if (this.state.startDate <= this.state.endDate) {
store.dispatch(setDates([this.state.startDate, this.state.endDate]));
window.location = `/search/${
this.state.location
}&${this.state.startDate.format("DDMMYYYY")}&${this.state.endDate.format(
"DDMMYYYY"
)}&${this.state.guestCount}&${this.state.offset}&${this.state.count}`;
} else {
console.log("HANDLE ERROR");
}
};
The problem, according to the chrome redux dev-tools, is that when the submit is triggered, the store does indeed change to the new dates, but it then seems to be immediately overwritten to the empty state. By modifying the reducer to take state = {dates: 'foo'} as its first argument, I can get the store to persist 'dates:foo'. This suggests to me that, for some reason, the reducer is being called twice - once with an action of type "SET_DATES", which works, and then again, immediately, with an action of unknown type (confirmed by console.log-ging action.type), which causes it to return the default state.
So I'm pretty sure I know what the problem is, but I have no idea why it would do this.
I Already commented, but anyways. The problem is that you reload the page. It reloads redux, and it boots up from initial state, which is probably an empty array. Here is a great video from one of the brains behind redux.
https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage
It all boils down to subscribing to the store state changes, and saving it / loading the state back from storage of your choise.
Try changing you reducer like this
const dates = (state = {}, action) => {
switch (action.type) {
case "SET_DATES":
return Object.assign({}, state, {
action.payload
});
default:
return state;
}
};
export default dates;

nextProps and this.props have always the same value

componentWillReceiveProps(nextProps) {
if (nextProps.group.data !== this.props.group.data) {
console.log(true);
}
}
the group.data object is comming from
const mapStateToProps = (state, props) => {
return {
group: {
data: state.group.data,
isFetching: state.group.isFetching,
error: state.group.error
}
}
}
I'm using redux module to collect the data...
I know the the nextPros references to this.props and i know a solution will be to use Immutable.js but I couldn't find a good example of using it and I found many complications integrating it, as I have already so many modules without Immutable.
is there any other solution to compare newProps to this.props or a good example how to use Immutable with Redux.
Thanks,
Lucas Franco
This means that the data object from the store isn't changing, you need to make sure that you are returning a new object in the reducer, and never mutate it. For example:
// reducer for group
reducer (state = initialState, action) {
...
// when your code is going to update data return a completely new object
return {...state,
data: { ...state.data, ...action.newData}
// never something like state.data.someProp = action.someProp
}
...
}
I'd need to see more of your code to be more sure, mainly the reducer.
But if the expression (nextPros.group.data !== this.props.group.data) is resulting in true, it's certain to say that the reference to the data object hasn't changed.

how to set initial state in redux

I'm trying to figure out how to set an initial state for a store in redux. I'm using https://github.com/reactjs/redux/blob/master/examples/todos-with-undo/reducers/index.js as an example. I tried to modify the code such that the todos had a value initialized.
const todoApp = combineReducers({
todos,
visibilityFilter
}, {
todos: [{id:123, text:'hello', completed: false}]
})
following the doc: http://redux.js.org/docs/api/createStore.html
but it's not working, and I'm not quite sure why.
It needs to be the second argument to createStore:
const rootReducer = combineReducers({
todos: todos,
visibilityFilter: visibilityFilter
});
const initialState = {
todos: [{id:123, text:'hello', completed: false}]
};
const store = createStore(
rootReducer,
initialState
);
You can set the initial state in the reducer(s).
const initialTodos = [{id:123, text:'hello', completed: false}]
// this is the ES2015 syntax for setting a default value for state in the function parameters
function todoReducer(state = initialTodos, action) {
switch(action.type) {
...
}
return state
}
const todoApp = combineReducers({
// todos now defaults to the array of todos that you wanted and will be updated when you pass a new set of todos to the todoReducer
todos: todoReducer,
visibilityFilter
})
There have been great answers so far but let me ice the cake; perhaps to give you an in-depth analysis, so that you don't just copy StackOverflow codes``` that works but don't know why your program is working.
There are two main ways to accomplish this viz:
1. using the createStore method. It takes an optional second argument (the preloadedState value)
const store = createStore(counter) // createStore without preloadedState
const initialState = {} // or in your case:
const initialState = {
initialTodos = [{id:123, text:'hello', completed: false}]
}
const store = createStore(counter, initialState) // create store with preloadedState
if you call createStore without the preloadedState it would initialize the state to {}
Hence the reducers will receive undefined as their state values.
That brings us to the second method.
You can set it at the reducers. Reducers can also set initialState by looking at the incoming state argument (which would be undefined if createStore is not called with initialState) and returning the values they would like to use as default.
const initialState = {} // a common pattern but in your case:
const initialState = {
initialTodos = [{id:123, text:'hello', completed: false}]
}
function todoReducer(state = initialState, action) {
switch (action.type) {
case // your type:
return ...
default:
return state
}
}
The drawback of method 2 is evident in a case where there is huge data; like a huge todo list for instance that you want to pass as initialState "app-wide". Method 2 would bring in a lot of repetition as you would have to do the same thing across all your reducers. This is the main drawback. But it is quite popular when you just want to set initialState as {} it is a common pattern.
Here is a 4min read for better understanding: https://dev.to/lawrence_eagles/how-to-properly-set-initial-state-in-redux-78m
per #ctrlplusb answer, the reason this works is because
const rootReducer = combineReducers({
todos: todos,
visibilityFilter: visibilityFilter
});
the first todos is a key which sets the second todos as a return value from the reducer. Reducers always run one time upon store creation. This initializes your global store.
There's an action dispatched when the store is created. That's how the initial state supplied in each combined reducer gets initialized in the store. If you check redux dev tools you'll see the first action dispatched is "##redux/INIT{something}"
In redux's documentation, near the end of the file, there is a dispatch({ type: ActionTypes.INIT })
See here https://github.com/reduxjs/redux/blob/master/src/createStore.js#L281-L284
See this question/answer I made on stackoverflow clarifying the response:
Different ways of initializing a react redux store's initial global state?

Resources