React Redux Reducers and Immutable Updates - reactjs

In a previous scenario to Update Profile values, i created a new store slice (updatedProfileDetails) and stored a new object there, but i think this isn't the best practice to solve my problem (since i have now two slices profileDetails & updateProfileDetails),
Now I found in redux documentation that we can make immutable update reducers to change data in the same slice without mutate states but in this scenario (set conversation unseen to false) , the API don't send an object as a response but just a success message,
SO, I'm trying to passe my conversation ID from Action To reducer to check it and change a single value of this conversation[id]
but i'm unable to make it correctly
My code:
1- Action: where I'm supposed to send id after action success
export const updateSeenConversation = (id) => async (dispatch, getState) => {
try {
dispatch({
type: SEEN_CONVERSATIONS_REQUEST,
})
const {
userLogin: { userInfo },
} = getState()
const config = {
headers: {
// headers
},
}
const { data } = await axios.put(
`${process.env.REACT_APP_API_KEY}/conversation/unseen/${id}`,
"",
config
)
// if success data : {success:true , message:"..."}
dispatch({
type: SEEN_CONVERSATIONS_SUCCESS,
payload: id, // usually i send data sent from API as dispatch payload,, to check for the exact conversation to update i tried to send id
})
} catch (error) { //error...})
}
}
2- my reducer:
export const conversationsListReducer = (
state = { conversations: {} },
action
) => {
// get conversations works Fine and return a slice conversations{..} that i need to update
switch (action.type) {
case GET_CONVERSATIONS_REQUEST:
return { ...state, loading: true }
case GET_CONVERSATIONS_SUCCESS:
return { loading: false, conversations: action.payload }
case GET_CONVERSATIONS_FAIL:
return { loading: false, error: action.payload }
// here ERROR
// i cant access the exact value i want "unseen" **NB:CHECK store in IMAGE BELLOW**
case SEEN_CONVERSATIONS_REQUEST:
return { ...state, loading: true }
case SEEN_CONVERSATIONS_SUCCESS:
return {state.conversations.conversation.map((conversation) => {
if (conversation._id !== action.payload) {
return conversation // return without change if !== myid
}
return {
...conversation, // make a copy
unseen: false, // change this value to false/true
}
})}
case SEEN_CONVERSATIONS_FAIL:
return { loading: false, error: action.payload }
case USER_LOGOUT:
return { conversations: {} }
default:
return state
}
}
Redux store Slice Image Link
Thank You.

Ok, I still don't see where in your post you say what isn't working but I'm gathering it is something in the SEEN_CONVERSATIONS_SUCCESS reducer case since that's where you focused some comments.
From what I can tell in this reducer case, you are mutating the state invariant from { conversations: {} } to { [] } since mapping returns an array, and isn't a valid object. When updating state you need to shallow copy the state object and also any nested state being updated.
case SEEN_CONVERSATIONS_SUCCESS:
return {
...state, // <-- shallow copy state
conversations: {
...state.conversations // <-- shallow copy state.conversations
conversation: state.conversations.conversation.map( // <-- shallow copy conversation array
conversation => conversation._id === action.payload
? {
...conversation, // <-- shallow copy conversation
unseen: false
}
: conversation
),
}
};
Since this reducer case is accessing nested state properties that are possibly undefined, you should also fully declare your state.
const initialState = {
conversations: {
conversation: [], // <--- now access this deep won't throw error
},
loading: false,
error: null,
};
export const conversationsListReducer = (
state = initialState,
action
) => {
...
case USER_LOGOUT:
return initialState; // <-- reset to initial state

Related

Redux state showing previous or default value while submitting function called

Async await is not functioning properly on my React project. It is not awaiting a response. Redux state displaying the previous or default value while calling the function but outside the function, it's working fine. As you can see, I print the employee from the selector in the handleSubmit function, however this prints the prior or default state, and I need the updated value to proceed. Data is not being awaited.
// handle submit function
const handleSubmit = async(values) => {
const personalData = new FormData();
Object.keys(personalValue).forEach((key) => personalData.append(key, personalValue[key]));
await dispatch(addEmployeePersonal(personalData));
console.log(employee) // Inside the function Employee prints default or previous state value
};
console.log(employee) // Here it's working fine, Outside the function employee prints updated value
// Selector
const {
employee,
employeeLoading,
employeeError
} = useSelector((state) => state.employee);
// Redux Reducer
export const employeeReducer = (state = {
employee: 0
}, action) => {
switch (action.type) {
case UPDATE_EMPLOYEE_PERSONAL_REQUEST:
case EMPLOYEE_ID_REQUEST:
case NEW_EMPLOYEE_PERSONAL_REQUEST:
case NEW_EMPLOYEE_OFFICIAL_REQUEST:
case DELETE_EMPLOYEE_REQUEST:
return {
...state,
employeeLoading: true,
employee: 0,
};
case UPDATE_EMPLOYEE_PERSONAL_SUCCESS:
case UPDATE_EMPLOYEE_OFFICIAL_SUCCESS:
case EMPLOYEE_ID_SUCCESS:
case NEW_EMPLOYEE_PERSONAL_SUCCESS:
case NEW_EMPLOYEE_OFFICIAL_SUCCESS:
case DELETE_EMPLOYEE_SUCCESS:
return {
...state,
employeeLoading: false,
employee: action.payload,
};
case UPDATE_EMPLOYEE_PERSONAL_FAILED:
case UPDATE_EMPLOYEE_OFFICIAL_FAILED:
case EMPLOYEE_ID_FAILED:
case NEW_EMPLOYEE_PERSONAL_FAILED:
case NEW_EMPLOYEE_OFFICIAL_FAILED:
case DELETE_EMPLOYEE_FAILED:
return {
...state,
employeeLoading: false,
employeeError: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
employeeError: null,
};
default:
return state;
}
};
// Redux Action
export const addEmployeePersonal = (info) => async(dispatch) => {
try {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_REQUEST
});
const {
data
} = await coreAxios.post("/api/Employee/PersonalInfo", info);
dispatch({
type: NEW_EMPLOYEE_PERSONAL_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_FAILED,
payload: error.response,
});
}
};
Reason: At the first render of the component, the value employee is 0. You trigger the form submit handler, it uses the employee, which value is 0. Even though you dispatch an action and try to change it, the handleSubmit still uses the employee evaluated in the first render(execution of function component) of the component before finishing executing. await dispatch(thunk()) will NOT wait for next render of the component. That's why you get the previous value of employee.
After handleSubmit finishes its execution, the state in the redux store has been changed, the redux store context provider will subscribe to that change and rerender the children. Your component will re-render(the second execution of the function component), useSelector will execute again and return the new employee. A new handleSubmit function will be declared and it reference the new employee defined in the function component scope.
There are two solutions:
Option 1: useEffect
useEffect(() => {
// get the latest employee to do something.
}, [employee])
Option 2: return action payload in thunk so that you can get it after dispatching action
export const addEmployeePersonal = (info) => async(dispatch) => {
try {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_REQUEST
});
const {
data
} = await coreAxios.post("/api/Employee/PersonalInfo", info);
dispatch({
type: NEW_EMPLOYEE_PERSONAL_SUCCESS,
payload: data,
});
return data; // here
} catch (error) {
dispatch({
type: NEW_EMPLOYEE_PERSONAL_FAILED,
payload: error.response,
});
}
};
const handleSubmit = async(values) => {
const personalData = new FormData();
Object.keys(personalValue).forEach((key) => personalData.append(key, personalValue[key]));
const newEmployee = await dispatch(addEmployeePersonal(personalData));
// Do something with newEmployee.
};

Struggling to Get Array Data From Redux Store

I have not been able to access items in an array that I am retrieving with redux. When I do console logs in the action itself, I am able to access array elements individually. But once that data makes its way to the component for display following the dispatch of actions, I have been unable to parse the data structure without error.
ListingActions.js:
If I do a console log here, I can parse through the different indices of the variable data without issue
export const getListings = () => async (dispatch) => {
try {
dispatch({ type: LISTING_REQUEST })
const { data } = await axios.get('/gmk')
// I can access the elements of the array here without a problem
dispatch({ type: LISTING_SUCCESS, payload: data })
} catch (error) {
dispatch({
type: LISTING_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
})
}
ListingReducers.js:
export const listingReducer = (state = { itemListings: [] }, action) => {
switch (action.type) {
case LISTING_REQUEST:
return { loading: true, itemListings: [] }
case LISTING_SUCCESS:
return { loading: false, itemListings: action.payload }
case LISTING_FAIL:
return { loading: false, error: action.payload }
default:
return state
}
Snippet from store.js:
const initialState = {
itemListings: [],
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
HomeScreen.js:
function HomeScreen() {
const dispatch = useDispatch()
const freshItemListings = useSelector((state) => state.itemListings)
const { loading, error, itemListings } = freshItemListings
useEffect(() => {
dispatch(getListings())
}, [dispatch])
return <div>{loading ? <p>Loading</p> : error ? <p>{error}</p> : itemListings.length}</div>
You'll see that I am trying to just access the length of itemListings. When I do so, I get an error: TypeError: Cannot read properties of undefined (reading 'length'). I have done other things like itemListings[0], and other methods just to see what my options are, but each has resulted in an error.
I believe this has to do with how you are updating itemListings in ListingReducers.js
case LISTING_SUCCESS:
return { loading: false, itemListings: action.payload }
You must update itemListing using the spread (...) operator to preserve the current state described here.
case LISTING_SUCCESS:
return {
...state,
loading: false,
itemListings: action.payload
}
In your code, the initial state is never actually getting updated in your reducer.
See Redux Documentation
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
WARNING
In Redux, our reducers are never allowed to mutate the original / current state values!
// ❌ Illegal - by default, this will mutate the state!
state.value = 123
TIP
Reducers can only make copies of the original values, and then they can mutate the copies.
// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}
Currently you have two itemListings in your app, which i think is causing some confusion.
In here :
const initialState = {
itemListings: [],
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
the default itemListings state from the store is an array.
Then in here:
const freshItemListings = useSelector((state) => state.itemListings)
freshItemListings is the same array and you are receiving it in the component
then in this line:
const { loading, error, itemListings } = freshItemListings
you are extracting a itemListings property from an array, which results to an error.
You have to define itemListings in the initial state as an object:
const initialState = {
itemListings: {itemListings:[]},
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
Another solution is as you are already defining a initial state for itemListings in its reducer, omit the itemListings property in initialState object,
const initialState = {
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
I discovered that the issue was with properly setting up the initial state in my store. I did change some of the variable names as suggested to make readability easier as well.
Changes to store.js:
const initialState = {
listingData: { itemListings: [], loading: true },
nope: { nopeItems: nopeItemsFromStorage },
keep: { keepItems: keepItemsFromStorage },
Setting both the loading and itemListing portions to the proper initial state was ultimately the answer.

How to prevent non-deterministic state updation in Redux?

When working with Redux, maintaining the shape of the initial state is crucial. The results/data of side effects like API call will change the shape of the state since we have no control over the properties. For example, consider this initial state:
const book = {
id: 0,
name: 'something'
};
And updation is made to it by the book sub-reducer as follows based on the API data:
//receives `book` part of the state
const bookReducer = (state=book, action) => {
switch(action.type) {
case 'SET_BOOK': {
return { ...action.payload };
} default:
return state;
}
}
Two scenarios that could happen:
If the data sent from the API is null, then newly produced state is now {} as a result of the spread operator. If some parts of UI were to listen to the book part of the state, then it will break. Possibly access individual properties from the API data? In that case, null/undefined checks needs to be performed for properties. Is there a more elegant solution?
There could also be additional properties in the data which we may not be interested in. Possibly use an object mapper to filter unused properties?
What is the best practice to handle these kind of scenarios and prevent state becoming non-deterministic? Please share your experience on how you approached these scenarios.
Only the reducer has to be pure/deterministic, not the stuff outside of it.
To prevent your reducer from overwriting data incorrectly, write some logic between the API response and the dispatch-call to ensure the reducer always gets valid data.
For example a thunk might look like:
const createBook = (name) => {
return async dispatch => {
// suppose the api call gives back "uid" plus extra data
const { uid, ...unneededData } = await myApi.setBook(name);
// dispatch the data in the way the reducer expects it
dispatch({ type: 'SET_BOOK', id: uid, name });
}
}
In the above example, the api call gives me uid, but no name, and a bunch of extra data. Just prepare the data before dispatching it.
The best practice is the one where you prevent your app from breaking from every aspect, which means you need to check and format your data before returning from the reducer.
In your case, I would check for both data validity and map it to a necessary format.
only dispatch 'SET_BOOK' if API response has both id and book.
in order to avoid unnecessary additional properties, you can always map your data const book = {id: apiData.id, book: apiData.book} before dispatching.
In your reducer you can do like below. This way only id and name will get updated even though there are other key/values in the response. Also this will make sure that if null values are received then those values will not be updated in the state. Hope this will help in resolving the issue.
//receives `book` part of the state
const bookReducer = (state=book, action) => {
const { type, payload } = action;
switch(type) {
case 'SET_BOOK': {
return {
...state,
...(payload.id && {id: payload.id}),
...(payload.name && {name: payload.name})
};
} default:
return state;
}
}
Your redux reducer logic should not worry about that due to its deterministic nature. You handle your api call and response handling elsewhere (redux thunk or component), and then dispatch the action to set your redux. Building off of your example:
book.reducer.js
const book = {
id: 0,
name: ''
};
const bookReducer = (state=book, action) => {
switch(action.type) {
case 'SET_BOOK': {
return { ...action.payload };
} default:
return state;
}
book.actions.js
const setBook = (book) => ({
type: SET_HEROES,
payload: book
});
// thunk
const findBook = name => async dispatch => {
const book = await bookService.findBook(name);
if (book) {
dispatch(setBook(book));
}
};
book.service.js
const findBook = async (name) => {
// Do your api call
const bookResponse = axios.get(`${apiUrl}/book/search/${name}`);
// Handle the response
if (!bookResponse) {
// Logic you do if book not found
return null;
}
return {id: bookResponse.id, name: bookResponse.name};
}
Now in a component you can just dispatch the findBook call
Component.js
const Component = () => {
const [search, setSearch] = useState('');
const dispatch = useDispatch();
const handleOnSearch = () => {
dispatch(findBook(search));
}
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)}/>
<button onClick={handleOnSearch}>Search</button>
</div>
);
}
If field value from API is undefined then convert it into null and store so that the code doesn't break and operatable. If API gives other params as well then de-structure the API returned object and extract the required fields. So that storing unnecessary data can be avoided.
const bookReducer = (state=book, action) => {
switch(action.type) {
case 'SET_BOOK': {
const {id, name, otherParam1, otherParam2} = action.payload;
return {
id: id || null,
name: name || null,
otherParam1,
otherParam2
}
} default:
return state;
}
}
Having the value null won't break the code instead, it renders nothing
which is better than undefined which breaks the code
Hope this helps you
What I do is to have all of my logic in my action method and create reducers for when an action is correctly fulfilled and another one for when is rejected. In the fulfilled reducer, I would do the regular instructions and in the rejected reducer I would add the data to a variable called error which I always have in my state and use in the frontend to show an error message if needed.
Example
This is an action that creates a house by sending a post request to my api which returns the created object or an error if something went wrong.
export const createHouse = houseData => {
const URL = HTTP://EXAMPLE.URL
return async dispatch => {
try {
const response = await axios.post(`${URL}`, houseData);
const data = await response.data;
dispatch({ type: "CREATE_HOUSE_DRAFT_FULFILLED", data });
} catch (err) {
dispatch({ type: "CREATE_HOUSE_DRAFT_REJECTED", data: err });
}
};
};
Then I would have 2 reducer methos to recieve the fulfilled or the rejected response, like this.
case 'CREATE_HOUSE_DRAFT_FULFILLED': {
return {
houses: [action.data, ...state.houses],
house: action.data,
houseCount: state.houseCount + 1,
fetched: true,
error: null
};
}
case 'CREATE_HOUSE_DRAFT_REJECTED': {
return {
...state,
error: action.data.response.data,
fetched: false,
success: null
};
}
Hope this works for you!

Why doesnt add layers on redux

I'm trying to add an array of layers using redux.
First, I create an array of promises.
Secnod, I use Promise.all with promises array and send to database all the info and returns all layers created on database.
Third, totalLayers contains current layers with new layers from database.
My problem is that launch dispatch and draw layers on my map, but dont update the array of redux with totalLayers.
SET_MAP_LAYERS update layers stored in Store as you can see in mapGlLayers variable.
What I'm doing wrong??
static addMultipleLayersFromDataSet(layers, source) {
return (dispatch) => {
let mapGlLayers = store.getStore().getState().maplayers.maplayers.slice();
let position = mapGlLayers.filter(l => l.name).length;
let promises = layers.map( layer => uploadMultipleLayers(layer, source, position++));
Promise.all(promises)
.then(downloadedlayers => {
let totalLayers = [...mapGlLayers, ...downloadedlayers];
dispatch({
type: LayerTypeConstants.SET_MAP_LAYERS,
payload: totalLayers
});
})
.catch(error => {
dispatch({
type: LayerTypeConstants.MAPLAYER_ERROR,
payload: error
});
});
};
}
REDUCER:
import { LayerTypeConstants } from '../utils/ReduxConstants';
const initialStateApp = {
maplayers: [],
};
export default function LayerReducer(state = initialStateApp, action) {
switch (action.type) {
case LayerTypeConstants.SET_MAP_LAYERS: {
return Object.assign({}, state, {
maplayers: action.payload
});
}
case LayerTypeConstants.MAPLAYER_ERROR: {
return Object.assign({}, state, {
messageMapLayer: action.payload
});
}
case LayerTypeConstants.INIT_LAYERS:
return Object.assign({}, initialStateApp);
default:
return state;
}
};
Here is an image of my redux state:
Here is console message with layers:
Promise is a state machine under the hood. State machines perform state transitions. Redux alone is not prepared to handle such transitions so it heeds a helper. I'd suggest reading about Async Redux Actions, link. The two widely used helpers are redux-thunks and redux-saga.

Redux - why loading everything in state at root

I am trying to understand Redux and having some difficulty.
I understand the concept of combineReducer, ie ....
var reducer = combineReducers({
user: userReducer,
products: productsReducer
})
But what if I have thousands of products, only available on the products page. I do not understand why I need to load them at root; to me this will slow the initial start up of the app for something that will not be needed unless the user goes to the products page.
Is this just the way it is with redux?
In Redux apps, you always build your entire state at the start. With Redux you have one store and one state - everything should trickle down from that one state to props on your components. However, that does not mean you actually need to load all the data into the state at launch, only that the structure needs to be there. This is why you should set up an initial state object for each reducer.
Let's say you have thousands of product records that you load from the database. In your products reducer you could do something like this:
const initialState = {
data: []
};
//use ES6 default parameters
function productsReducer (state = initialState, action) {
switch (action.type) {
case 'GET_PRODUCTS':
//return data from action
return {
data: action.result
};
default:
return state;
}
}
This means that when you start your app, if you use the full reducer you declared in your post, your application state will look like this:
{
user: {},
products: {
data: []
}
}
products.data will be an empty array until you fire an action that actually requires you to load the products data (i.e. you go to the Products page in your app or something). It's true that the products data will remain in your state if you then go elsewhere in your app, but this is a great thing - the next time you render the Products page you will already have the data at your disposal without having to do a database lookup.
In our app, we made an API for the products and it has limit of 15 per page. So our reducer goes like this.
collection: {
"total": 0,
"per_page": 0,
"current_page": 0,
"last_page": 0,
"from": 0,
"to": 0,
data: []
},
isFetching: false,
isFetchingError: false
on the first load we fetched limited amount of products, then we made a pagination out of it.. using selectors in redux https://github.com/rackt/reselect
Loading a thousands of data will get your app very slow.
const paginated = (state = initialState, action) => {
switch (action.type) {
case FETCH_PAGINATED_PRODUCTS:
return {
...state,
isFetching: true,
isFetchingError: false
};
case FETCH_PAGINATED_PRODUCTS_SUCCESS:
return {
...state,
collection: action.payload,
isFetching: false
};
case FETCH_PAGINATED_PRODUCTS_ERROR:
return {
...state,
isFetching: false,
isFetchingError: true
};
default:
return state
we have used axios for request:
https://github.com/mzabriskie/axios
Here's how we implement axios in redux-async
export function getAll(page = 1) {
return (dispatch, getState) => {
const state = getState();
const { filters } = state.products.paginated;
if ( state.products.paginated.isFetching ) {
return;
}
dispatch({ type: FETCH_PAGINATED_PRODUCTS });
return axios
.get(`products?page=${page}&limit=16&filters=${JSON.stringify(filters)}`)
.then((res) => dispatch({
type: FETCH_PAGINATED_PRODUCTS_SUCCESS,
payload: res.data
}))
.catch((res) => dispatch({
type: FETCH_PAGINATED_PRODUCTS_ERROR,
/*payload: res.data.error,*/
error: true
}));
}
}
export function get(id) {
return (dispatch, getState) => {
const state = getState();
if ( state.products.resource.isFetching ) {
return;
}
dispatch({ type: FETCH_PRODUCT });
return axios
.get(`products/${id}`)
.then((res) => dispatch({
type: FETCH_PRODUCT_SUCCESS,
payload: res.data.data
}))
.catch((res) => dispatch({
type: FETCH_PRODUCT_ERROR,
/*payload: new Error(res.data.error),*/
error: true
}));
}

Resources