NGRX 4: How to combine data from multiple sources - firebase

Question on how to combine reducers / data in app state:
Example of Data (Firebase):
{
"artists" : {
"168dgYui7ExaU612eooDF1" : {
"name" : "Brand New",
"id" : "168dgYui7ExaU612eooDF1",
}
},
"genres" : {
"popPunk" : {
"name" : "pop punk"
},
"screamo" : {
"name" : "screamo"
}
},
"genresPerArtist" : {
"168dgYui7ExaU612eooDF1" : {
"popPunk" : true,
"screamo" : true
}
}
}
App State:
export type State = { app: AppState };
export type AppState = { genres: IGenre[], artistGenres: IGenre[], artists: any[], currentArtist: any };
export const initialState: State = {
app: {
artists: [],
currentArtist: null,
genres: [],
artistGenres: []
}
}
Reducer:
export function appReducer(state: AppState, action: GenreActions.Actions): AppState {
// TODO: add reducers for Fetch Artist and Fetch Artists
switch (action.type) {
case GenreActions.FETCH_GENRES: {
return { ...state };
}
case GenreActions.FETCH_GENRES_SUCCESS: {
return { ...state, genres: action.payload };
}
case GenreActions.FETCH_ARTIST_GENRES: {
return { ...state };
}
case GenreActions.FETCH_ARTIST_GENRES_SUCCESS: {
return { ...state, artistGenres: action.payload };
}
default: {
return state;
}
}
}
Effects:
#Effect()
FetchGenres$: Observable<Action> = this.actions$
.ofType(appActions.FETCH_GENRES)
.switchMap(() => {
return this.firebaseProvider.genres
.map(payload => new appActions.FetchGenresSuccess(payload))
.catch(() => of(new appActions.FetchGenresSuccess([])));
});
// TODO: See if I should be using store's genres here, or if that should be combined elsewhere
#Effect()
FetchArtistGenres$: Observable<Action> = this.actions$
.ofType(appActions.FETCH_ARTIST_GENRES)
.map((action: appActions.FetchArtistGenres) => { return action.payload })
.switchMap((artistId: string) => {
return this.firebaseProvider.genresByArtist(artistId)
.withLatestFrom(this.store.select(state => state.app.genres))
.map((results) => {
let artistGenres = results[0];
let genres = results[1];
return genres.filter(genre => {
let result = artistGenres.findIndex(g => g.$key === genre.$key);
return (result ? true : false);
});
})
.map(payload => new appActions.FetchArtistGenresSuccess(payload));
});
Firebase Provider:
#Injectable()
export class FirebaseProvider {
genres: FirebaseListObservable<IGenre[]>;
constructor(
private db: AngularFireDatabase
) {
this.genres = this.db.list('/genres');
}
genresByArtist(artistId: string): Observable<IDictionary[]> {
return this.db.list(`/genresPerArtist/${artistId}`);
}
}
Artist Page:
My question is how are you supposed to combine different slices of the data in NGRX4?
Example: I have an Artist Detail page. In that page, I want to display the Artist info, along with the Artist's genres. Elsewhere, I might have a Artist List page where I display multiple artists and their genres. Where do I fetch and merge all of that relevant information in my app? In the reducer? In the effect (how FetchArtistGenres$ is currently doing it)?
Other concerns: I don't think I want to have the entire "genresPerArtist" or "artists" node in my app state, because it will eventually become very large and I'm not sure that would be performant (if I'm wrong here, and that's the way to go, let me know). Instead, I'd fetch the specific Artists genre node when I need it (/genresPerArtist/${artistId})

Related

wait until first hook is complete before fetching data

I have this custom hook which fetches the query.me data from graphql. The console.log statement shows that this hook is running a number of times on page load, but only 1 of those console.logs() contains actual data.
import { useCustomQuery } from '../api-client';
export const useMe = () => {
const { data, isLoading, error } = useCustomQuery({
query: async (query) => {
return getFields(query.me, 'account_id', 'role', 'profile_id');
},
});
console.log(data ? data.account_id : 'empty');
return { isLoading, error, me: data };
};
I then have this other hook which is supposed to use the id's from the above hook to fetch more data from the server.
export const useActivityList = () => {
const { me, error } = useMe();
const criteria = { assignment: { uuid: { _eq: me.profile_id } } } as appointment_bool_exp;
const query = useQuery({
prepare({ prepass, query }) {
prepass(
query.appointment({ where: criteria }),
'scheduled_at',
'first_name',
'last_name',
);
},
suspense: true,
});
const activityList = query.appointment({ where: criteria });
return {
activityList,
isLoading: query.$state.isLoading,
};
};
The problem I am facing is that the second hook seems to call the first hook when me is still undefined, thus erroring out. How do I configure this, so that I only access the me when the values are populated?
I am bad with async stuff...
In the second hook do an early return if the required data is not available.
export const useActivityList = () => {
const { me, error } = useMe();
if (!me) {
return null;
// or another pattern that you may find useful is to set a flag to indicate that this query is idle e.g.
// idle = true;
}
const criteria = { assignment: { uuid: { _eq: me.profile_id } } } as appointment_bool_exp;
...

Redux: Uncaught Error: Actions may not have an undefined "type" property. Have you misspelled a constant?

I have decided to break up my redux store—to now represent logical buckets i.e. users, ui etc.
These are the files which each contain the initial state, action types and reducers for each category:
ui reducer file:
/*./reducers/ui/index' reducer for ui */
/* initial state */
export const uiStartState = { ui: { modalActive: false } }
/* action types */
export const actionTypes = {
ui: { MODAL_ACTIVE: 'MODAL_ACTIVE' },
ui: { MODAL_INACTIVE: 'MODAL_INACTIVE' },
}
/* reducer(s) */
export default function ui(state = uiStartState, action) {
switch (action.type) {
case actionTypes.MODAL_ACTIVE:
return Object.assign({}, state, { ui: { modalActive: true } });
case actionTypes.MODAL_INACTIVE:
return Object.assign({}, state, { ui: { modalActive: false } });
default:
return state
}
};
/* actions */
export const modalStateOn = () => {
return { type: actionTypes.ui.MODAL_ACTIVE }
}
export const modalStateOff = () => {
return { type: actionTypes.ui.MODAL_INACTIVE }
}
users reducer file:
/*./reducers/users/index' reducer for ui */
/* initial state */
export const usersStartState = { users: { isLoggedIn: false } }
/* action types */
export const actionTypes = {
users: { IS_LOGGED_IN: 'IS_LOGGED_IN' },
users: { IS_LOGGED_OUT: 'IS_LOGGED_OUT' },
}
/* reducer(s) */
export default function users(state = usersStartState, action) {
switch (action.type) {
case actionTypes.users.IS_LOGGED_IN:
return Object.assign({}, state, {
users: { isLoggedIn: true }
});
case actionTypes.users.IS_LOGGED_OUT:
return Object.assign({}, state, {
users: { isLoggedIn: false }
});
default:
return state
}
};
/* actions */
export const logInUser = () => {
return { type: actionTypes.users.IS_LOGGED_IN }
}
export const logOutUser = () => {
return { type: actionTypes.users.IS_LOGGED_OUT }
}
And this is my store:
import { applyMiddleware, combineReducers, createStore } from 'redux'
/* imported reducers */
import ui from './reducers/ui/index'
import users from './reducers/users/index'
import { composeWithDevTools } from 'redux-devtools-extension'
import { persistStore } from 'redux-persist';
import { createLogger } from 'redux-logger'
import thunkMiddleware from 'redux-thunk'
var rootReducer = combineReducers({
ui,
users
})
export default () => {
let store;
const isClient = typeof window !== 'undefined';
if (isClient) {
const { persistReducer } = require('redux-persist');
const storage = require('redux-persist/lib/storage').default;
const persistConfig = {
key: 'primary',
storage,
whitelist: ['isLoggedIn', 'modalActive'], // place to select which state you want to persist
}
store = createStore(
persistReducer(persistConfig, rootReducer), {
ui: { modalActive: false },
users: { isLoggedIn: false }
},
composeWithDevTools(applyMiddleware(
thunkMiddleware,
createLogger({ collapsed: false })
))
);
store.__PERSISTOR = persistStore(store);
} else {
store = createStore(
rootReducer, {
ui: { modalActive: false },
users: { isLoggedIn: false }
},
composeWithDevTools(applyMiddleware(
thunkMiddleware,
createLogger({ collapsed: false })
))
);
}
return store;
};
So taking my actions from users:
export const logInUser = () => {
return { type: actionTypes.users.IS_LOGGED_IN }
}
export const logOutUser = () => {
return { type: actionTypes.users.IS_LOGGED_OUT }
}
Not sure why the error is saying I don't have a key with the name type, I assume it's a matter of restructuring.
Thanks in advance!
UPDATE
I am wondering if the problem is I am merging the new state incorrectly?
From my reducer:
case actionTypes.users.IS_LOGGED_IN:
return Object.assign({}, state, {
users: { isLoggedIn: true }
});
My state feedback from redux tools:
You can see the next state the users object gets another users object nested in the orignal with the correct payload!
Your user action types must be in this format:
export const actionTypes = {
users: { IS_LOGGED_IN: "IS_LOGGED_IN", IS_LOGGED_OUT: "IS_LOGGED_OUT" }
};
With your code, actionTypes.users.IS_LOGGED_IN will be undefined, because you have the same key in the same object, and it will be replaced. This is the reason why redux complains.
Also ui action types must be:
export const actionTypes = {
ui: { MODAL_ACTIVE: "MODAL_ACTIVE", MODAL_INACTIVE: "MODAL_INACTIVE" }
};
May be you can keep all your action types in a single object like this:
export const actionTypes = {
users: { IS_LOGGED_IN: "IS_LOGGED_IN", IS_LOGGED_OUT: "IS_LOGGED_OUT" },
ui: { MODAL_ACTIVE: "MODAL_ACTIVE", MODAL_INACTIVE: "MODAL_INACTIVE" }
};
Update: about your question merging state:
Can you try like this?
export default function users(state = usersStartState, action) {
switch (action.type) {
case actionTypes.users.IS_LOGGED_IN:
return {
...state,
users: {
...state.users,
isLoggedIn: true
}
};
case actionTypes.users.IS_LOGGED_OUT:
return {
...state,
users: {
...state.users,
isLoggedIn: false
}
};
default:
return state;
}
}

Unable to update reducer state in redux

I have a redux app which enables users to post something(like a blog post which contains only a title and body).The user can edit the post whenever they want to.I am not able to update the reducer,i.e. I just want to update the title and the body for that particular "id". I am trying to update the state in my reducer as shown below:
case SUBMIT_POST:
let id = action.payload.id;
return {
...state,
[id]: {
...state[id],
title: action.payload.title,
body: action.payload.body
}
};
My action-creator looks like this:
//Action creator for submitting edited post
export function submitEditedPost(id, values, callback) {
const request = axios.put(`${API}/posts/${id}`, {values}, {headers});
return dispatch => {
return request.then((res) => {
callback();
console.log(res.data.values)
dispatch({
type:SUBMIT_POST,
payload: res.data.values
})
})
}
}
How to update the reducer with the new edited title and body for that particular post id?
EDIT 1:
I am calling the action creator in my form onSubmit() method as shown below:
onSubmit(values) {
const { id } = this.props.match.params;
const formData = {};
for (const field in this.refs) {
formData[field] = this.refs[field].value;
}
formData.id = id;
console.log('-->', formData);
this.props.submitEditedPost(id, formData, () => {
this.props.history.push('/');
});
}
EDIT 2: Screenshot of the action in reducer is shown below:
EDIT 3: My entire reducer:
import _ from 'lodash';
import { FETCH_POSTS, FETCH_POST, CREATE_POST, EDIT_POST, SUBMIT_POST } from '../actions/posts_action';
export default function(state = {posts: {} }, action) {
switch (action.type) {
case FETCH_POST:
// const post = action.payload.data;
// const newState = { ...state, };
// newState[post.id] = post;
// return newState;
return {...state, [action.payload.id]: action.payload};
case FETCH_POSTS:
return {posts: { ...state.posts, ...(_.mapKeys(action.payload,'id'))}};
case CREATE_POST:
return {...state, [ action.payload.id]: action.payload};
case EDIT_POST:
return { ...state, [action.payload.id]: action.payload};
case SUBMIT_POST:
console.log(action.payload);
let id = action.payload.id;
return {
...state,
[id]: {
...state[id],
title: action.payload.title,
body: action.payload.body
}
};
default:
return state;
}
}
EDIT 4 : Screenshot of redux-dev-tools:

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