how to convert ES5 to ES6 in redux's reducer - redux

How to convert the below code to ES6: (especially the part after newState.guests = []
case ActionTypes.GetInviteFulfilled: {
const { host, agenda, guests } = action.invite;
const newState = Object.assign({}, state, {
inProgress: false,
success: 'Got invite.',
host,
agenda
});
newState.guests = [];
if (guests) {
newState.guests = Object.keys(guests).map(k => guests[k]);
}
return newState;
}

This might work for you:
case ActionTypes.GetInviteFulfilled: {
const { host, agenda, guests } = action.invite;
return {
...state,
inProgress: false,
success: 'Got invite.',
host,
agenda,
guests: guests ? Object.keys(guests).map(k => guests[k]) : [],
};
}

Related

Call other slice methods from same slice in redux

I am trying to call other slice methods from the same slice in redux. I tried this approach but TS is throwing errors. What am I doing wrong? (Please see function setInitialLocation)
export const appSlice = createSlice({
name: "app",
initialState,
reducers: {
addCompletedAppSetupSteps: (
state,
action: PayloadAction<AppSetupSteps | AppSetupSteps[]>
) => {
if (Array.isArray(action.payload)) {
state.data.completedAppSetupSteps = union(
state.data.completedAppSetupSteps,
action.payload
);
} else {
state.data.completedAppSetupSteps = union(
state.data.completedAppSetupSteps,
[action.payload]
);
}
},
selectLocation(state, action: PayloadAction<LocationSelection | null>) {
if (!action.payload) {
state.data.selectedLocation = null;
return;
}
const payloadWithDefaults: LocationSelection = {
...action.payload,
settings: {
hasPopup: action.payload?.settings?.hasPopup ?? true,
hasFlyToAnimation:
action.payload?.settings?.hasFlyToAnimation ?? true,
},
};
state.data.selectedLocation = payloadWithDefaults;
},
setInitialLocation(state, action: PayloadAction<LocationSelection>) {
appSlice.caseReducers.selectLocation(state, action); // I am trying to call the other methods here.
appSlice.caseReducers.addCompletedAppSetupSteps(state, {
payload: AppSetupSteps.SetInitialLocationSelection,
type: "addCompletedAppSetupSteps",
});
},
},
});

ReduxJS Toolkit action creator

I am currently converting the state management from old redux to reduxjs/toolkit.
This is how it looked previously:
ACTION:
export const RECEIVED_FUNCTION_SUCCESS = 'RECEIVED_FUNCTION_SUCCESS ';
export const getFunctionSuccess = (result, callbackParams) => {
const append = callbackParams.pageNumber > 0;
return {
type: RECEIVED_FUNCTION_SUCCESS,
payload: result,
append,
pageSize: callbackParams.pageSize,
};
};
REDUCER:
{
case ActionTypes.RECEIVED_FUNCTION_SUCCESS:
return {
...state,
canLoadMore: payload.length >= pageSize,
data: append ? [...state.data, ...payload] : payload,
};
This is how it looks now:
ACTION:
export const getFunctionSuccess = createAction('RECEIVED_FUNCTION_SUCCESS ');
REDUCER:
extraReducers: {
[getFunctionSuccess ]: (state, { payload }) => {
const { pageSize, pageNumber } = payload;
const append = pageNumber > 0;
return {
...state,
canLoadMore: payload.length >= pageSize,
data: append ? [...state.data, ...payload] : payload,
};
},
I can't make it work, pageSize and pageNumber is always undefined... I don't know how to include the callbackParams in the toolkit reducer and action.
For more context this is the main part of the fetching:
request({
requestId: 'getFunction',
params: {
data: {
filters: mappedFilters,
sorts: sort,
pageSize,
pageNumber,
},
},
callbackParams: {
pageNumber,
pageSize,
},
}),
I do fetch successfully and all, just the callbackParams, what is additional to the action after payload, I can't manage to make it work.
I'd appreciate some help.

Understanding JSON.stringify() on Redux Action?

I was trying to reset the data, and want to go to initial state ,I know that the immutability playing major role in this part.
Below is my store data (Flow Completed data)
animalSense: {
selectedVision: 'dayLight',
selectedState: 'california',
viewedVisions: ['dayLightcalifornia', 'dayLightsouthAfrica', 'nightVisioncalifornia'],
viewedAnimals: ['dog', 'cat']
},
I want to replace it with the below data
animalSense: {
selectedVision: '',
selectedState: '',
viewedVisions: [''],
viewedAnimals: []
},
I know the below action is the Straight and proper way to add initial data is
export const RESET_ANIMAL_SENSES = 'actions/reset_animal_senses';
export default () => ({
type: RESET_ANIMAL_SENSES,
payload: {
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
}
});
But the above action maintaining the same state
Below action is Working Solution but I don't know is this a Proper way
export const RESET_ANIMAL_SENSES = 'actions/reset_animal_senses';
const data = JSON.stringify({
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
});
export default () => ({
type: RESET_ANIMAL_SENSES,
payload: JSON.parse(data)
});
When we are using stringify the connectivity has been ended and the new state has been added but i don't know why this is not working without JSON.stringify()?
Reducer
import { SELECT_VISION } from '../actions/select_vision_type';
import { CHANGE_ANIMAL_VIDEO_STATE } from '../actions/change_animal_video_state';
import { UPDATE_ANIMALS } from '../actions/update_animals';
import { RESET_ANIMAL_SENSES } from '../actions/reset_animal_senses';
export default (state = {}, action) => {
let newState = state;
switch (action.type) {
case SELECT_VISION:
newState = { ...state, ...action.payload };
break;
case CHANGE_ANIMAL_VIDEO_STATE:
newState = { ...state, ...action.payload };
break;
case UPDATE_ANIMALS:
newState = { ...state, ...action.payload };
break;
case RESET_ANIMAL_SENSES:
newState = { ...state, ...action.payload };
break;
default:
break;
}
return newState;
};
Spread Operator in payload Solved this issue
export const RESET_ANIMAL_SENSES = 'actions/reset_animal_senses';
const data = {
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
};
export default () => ({
type: RESET_ANIMAL_SENSES,
payload: { ...data } // here is the solution
});
Try this out, I'd do good amount of refactors to your reducer.
import { SELECT_VISION } from '../actions/select_vision_type';
import { CHANGE_ANIMAL_VIDEO_STATE } from '../actions/change_animal_video_state';
import { UPDATE_ANIMALS } from '../actions/update_animals';
import { RESET_ANIMAL_SENSES } from '../actions/reset_animal_senses';
const initialState = {
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
}
export default (state = initialState, action) => {
switch (action.type) {
// since all the cases have common code.
case SELECT_VISION:
case CHANGE_ANIMAL_VIDEO_STATE:
case UPDATE_ANIMALS: {
return { ...state, ...action.payload }
}
case RESET_ANIMAL_SENSES: {
return { ...initialState }
}
default: {
return state;
}
}
};
Try this reducer once. However, currently I don't have a clarity on why would it work with stringify in place.

Handling loading state of multiple async calls in an action/reducer based application

I donĀ“t think this issue is bound to a specific framework or library, but applies to all store based application following the action - reducer pattern.
For clarity, I am using Angular and #ngrx.
In the application I am working on we need to track the loading state of individual resources.
The way we handle other async requests is by this, hopefully familiar, pattern:
Actions
GET_RESOURCE
GET_RESOURCE_SUCCESS
GET_RESOURCE_FAILURE
Reducer
switch(action.type)
case GET_RESOURCE:
return {
...state,
isLoading = true
};
case GET_RESOURCE_SUCCESS:
case GET_RESOURCE_FAILURE:
return {
...state,
isLoading = false
};
...
This works well for async calls where we want to indicate the loading state globally in our application.
In our application we fetch some data, say BOOKS, that contains a list of references to other resources, say CHAPTERS.
If the user wants to view a CHAPTER he/she clicks the CHAPTER reference that trigger an async call. To indicate to the user that this specific CHAPTER is loading, we need something more than just a global isLoading flag in our state.
The way we have solved this is by creating a wrapping object like this:
interface AsyncObject<T> {
id: string;
status: AsyncStatus;
payload: T;
}
where AsyncStatus is an enum like this:
enum AsyncStatus {
InFlight,
Success,
Error
}
In our state we store the CHAPTERS like so:
{
chapters: {[id: string]: AsyncObject<Chapter> }
}
However, I feel like this 'clutter' the state in a way and wonder if someone has a better solution / different approach to this problem.
Questions
Are there any best practices for how to handle this scenario?
Is there a better way of handling this?
I have faced several times this kind of situation but the solution differs according to the use case.
One of the solution would be to have nested reducers. It is not an antipattern but not advised because it is hard to maintain but it depends on the usecase.
The other one would be the one I detail below.
Based on what you described, your fetched data should look like this:
[
{
id: 1,
title: 'Robinson Crusoe',
author: 'Daniel Defoe',
references: ['chp1_robincrusoe', 'chp2_robincrusoe'],
},
{
id: 2,
title: 'Gullivers Travels',
author: 'Jonathan Swift',
references: ['chp1_gulliverstravels', 'chp2_gulliverstravels', 'chp3_gulliverstravels'],
},
]
So according to your data, your reducers should look like this:
{
books: {
isFetching: false,
isInvalidated: false,
selectedBook: null,
data: {
1: { id: 1, title: 'Robinson Crusoe', author: 'Daniel Defoe' },
2: { id: 2, title: 'Gullivers Travels', author: 'Jonathan Swift' },
}
},
chapters: {
isFetching: false,
isInvalidated: true,
selectedChapter: null,
data: {
'chp1_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp1_robincrusoe', bookId: 1, data: null },
'chp2_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp2_robincrusoe', bookId: 1, data: null },
'chp1_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp1_gulliverstravels', bookId: 2, data: null },
'chp2_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp2_gulliverstravels', bookId: 2, data: null },
'chp3_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp3_gulliverstravels', bookId: 2, data: null },
},
}
}
With this structure you won't need isFetching and isInvalidated in your chapter reducers as every chapter is a separated logic.
Note: I could give you a bonus details later on on how we can leverage the isFetching and isInvalidated in a different way.
Below the detailed code:
Components
BookList
import React from 'react';
import map from 'lodash/map';
class BookList extends React.Component {
componentDidMount() {
if (this.props.isInvalidated && !this.props.isFetching) {
this.props.actions.readBooks();
}
}
render() {
const {
isFetching,
isInvalidated,
data,
} = this.props;
if (isFetching || (isInvalidated && !isFetching)) return <Loading />;
return <div>{map(data, entry => <Book id={entry.id} />)}</div>;
}
}
Book
import React from 'react';
import filter from 'lodash/filter';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class Book extends React.Component {
render() {
const {
dispatch,
book,
chapters,
} = this.props;
return (
<div>
<h3>{book.title} by {book.author}</h3>
<ChapterList bookId={book.id} />
</div>
);
}
}
const foundBook = createSelector(
state => state.books,
(books, { id }) => find(books, { id }),
);
const mapStateToProps = (reducers, props) => {
return {
book: foundBook(reducers, props),
};
};
export default connect(mapStateToProps)(Book);
ChapterList
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class ChapterList extends React.Component {
render() {
const { dispatch, chapters } = this.props;
return (
<div>
{map(chapters, entry => (
<Chapter
id={entry.id}
onClick={() => dispatch(actions.readChapter(entry.id))} />
))}
</div>
);
}
}
const bookChapters = createSelector(
state => state.chapters,
(chapters, bookId) => find(chapters, { bookId }),
);
const mapStateToProps = (reducers, props) => {
return {
chapters: bookChapters(reducers, props),
};
};
export default connect(mapStateToProps)(ChapterList);
Chapter
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class Chapter extends React.Component {
render() {
const { chapter, onClick } = this.props;
if (chapter.isFetching || (chapter.isInvalidated && !chapter.isFetching)) return <div>{chapter.id}</div>;
return (
<div>
<h4>{chapter.id}<h4>
<div>{chapter.data.details}</div>
</div>
);
}
}
const foundChapter = createSelector(
state => state.chapters,
(chapters, { id }) => find(chapters, { id }),
);
const mapStateToProps = (reducers, props) => {
return {
chapter: foundChapter(reducers, props),
};
};
export default connect(mapStateToProps)(Chapter);
Book Actions
export function readBooks() {
return (dispatch, getState, api) => {
dispatch({ type: 'readBooks' });
return fetch({}) // Your fetch here
.then(result => dispatch(setBooks(result)))
.catch(error => dispatch(addBookError(error)));
};
}
export function setBooks(data) {
return {
type: 'setBooks',
data,
};
}
export function addBookError(error) {
return {
type: 'addBookError',
error,
};
}
Chapter Actions
export function readChapter(id) {
return (dispatch, getState, api) => {
dispatch({ type: 'readChapter' });
return fetch({}) // Your fetch here - place the chapter id
.then(result => dispatch(setChapter(result)))
.catch(error => dispatch(addChapterError(error)));
};
}
export function setChapter(data) {
return {
type: 'setChapter',
data,
};
}
export function addChapterError(error) {
return {
type: 'addChapterError',
error,
};
}
Book Reducers
import reduce from 'lodash/reduce';
import { combineReducers } from 'redux';
export default combineReducers({
isInvalidated,
isFetching,
items,
errors,
});
function isInvalidated(state = true, action) {
switch (action.type) {
case 'invalidateBooks':
return true;
case 'setBooks':
return false;
default:
return state;
}
}
function isFetching(state = false, action) {
switch (action.type) {
case 'readBooks':
return true;
case 'setBooks':
return false;
default:
return state;
}
}
function items(state = {}, action) {
switch (action.type) {
case 'readBook': {
if (action.id && !state[action.id]) {
return {
...state,
[action.id]: book(undefined, action),
};
}
return state;
}
case 'setBooks':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
[key]: books(value, action),
}), {});
},
default:
return state;
}
}
function book(state = {
isFetching: false,
isInvalidated: true,
id: null,
errors: [],
}, action) {
switch (action.type) {
case 'readBooks':
return { ...state, isFetching: true };
case 'setBooks':
return {
...state,
isInvalidated: false,
isFetching: false,
errors: [],
};
default:
return state;
}
}
function errors(state = [], action) {
switch (action.type) {
case 'addBooksError':
return [
...state,
action.error,
];
case 'setBooks':
case 'setBooks':
return state.length > 0 ? [] : state;
default:
return state;
}
}
Chapter Reducers
Pay extra attention on setBooks which will init the chapters in your reducers.
import reduce from 'lodash/reduce';
import { combineReducers } from 'redux';
const defaultState = {
isFetching: false,
isInvalidated: true,
id: null,
errors: [],
};
export default combineReducers({
isInvalidated,
isFetching,
items,
errors,
});
function isInvalidated(state = true, action) {
switch (action.type) {
case 'invalidateChapters':
return true;
case 'setChapters':
return false;
default:
return state;
}
}
function isFetching(state = false, action) {
switch (action.type) {
case 'readChapters':
return true;
case 'setChapters':
return false;
default:
return state;
}
}
function items(state = {}, action) {
switch (action.type) {
case 'setBooks':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
...reduce(value.references, (res, chapterKey) => ({
...res,
[chapterKey]: chapter({ ...defaultState, id: chapterKey, bookId: value.id }, action),
}), {}),
}), {});
};
case 'readChapter': {
if (action.id && !state[action.id]) {
return {
...state,
[action.id]: book(undefined, action),
};
}
return state;
}
case 'setChapters':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
[key]: chapter(value, action),
}), {});
},
default:
return state;
}
}
function chapter(state = { ...defaultState }, action) {
switch (action.type) {
case 'readChapters':
return { ...state, isFetching: true };
case 'setChapters':
return {
...state,
isInvalidated: false,
isFetching: false,
errors: [],
};
default:
return state;
}
}
function errors(state = [], action) {
switch (action.type) {
case 'addChaptersError':
return [
...state,
action.error,
];
case 'setChapters':
case 'setChapters':
return state.length > 0 ? [] : state;
default:
return state;
}
}
Hope it helps.

Correct reducers composition

How to get reducers not to return always new state? Is my solution ok or there are some better way?
My state shape:
userList: {
id: 'xxx', // <-|- Not mutable by reducers fields, so using combineReducers for each field is frustrating.
ba: 'xxx', // <-|
fo: 'xxx', // <-|
users: [
{
id: 'yyy',
be: 'yyy',
fo: 'yyy',
photo: {
id: 'zzz',
url: 'http://image.jpg', <-- The only field, that changes by reducers.
},
},
...
],
}
My current reducers:
// Always returns new object.
const userListReducer = (userList, action) => {
return {
...userList,
users: usersReducer(userList, action),
}
};
// Always returns new array.
const usersReducer = (users, action) => {
return users.map(user => userReducer(user, action));
};
// Always returns new object too.
const userReducer = (user, action) => {
return {
...user,
photo: photoReducer(user.photo, action),
};
};
// The only one true reducer.
const photoReducer = (photo, action) => {
switch (action.type) {
case 'update_photo':
return {
...photo,
url: action.url,
};
default:
return photo;
}
};
Solutions
1). Call usersReducer when it's necessary. Bad part: we need to care about logic other reducers.
const userListReducer = (userList, action) => {
switch (action.type) {
case 'update_photo':
return {
...userList,
users: usersReducer(userList, action),
};
default:
return userList;
}
};
2). Using combineReducers. Bad part: we need to care about all usersList's shape. Also, this still doesn't work, because usersReducer always returns new array.
const userListRecuer = combineReducers({
id: id => id,
ba: ba => ba,
fo: fo => fo,
users: usersReducer,
});
3). My solution. Using mergeOrReturnOld and mapOrReturnOld helpers.
const userListReducer = (userList, action) => {
return mergeOrReturnOld(userList, {
users: usersReducer(userList, action),
});
};
const usersReducer = (users, action) => {
return mapOrReturnOld(users, user => userReducer(user, action));
};
const userReducer = (user, action) => {
return mergeOrReturnOld(user, {
photo: photoReducer(user.photo, action),
});
};
helpers implementation:
const mergeOrReturnOld = (obj, addition) => {
let hasChanged = false;
for (let key in addition) {
if (addition.hasOwnProperty(key)) {
if (obj[key] !== addition[key]) {
hasChanged = true;
}
}
}
if (!hasChanged) {
return obj;
} else {
return {
...obj,
...addition,
};
}
};
const mapOrReturnOld = (array, callback) => {
let hasChanged = false;
const newArray = array.map(item => {
const newItem = callback(item);
if (newItem !== item) {
hasChanged = true;
}
return newItem;
});
if (!hasChanged) {
return array;
} else {
return newArray;
}
};

Resources