Given the collection:
const peeps = [
{name: 'John', id: 1},
{name: 'Paul', id: 2},
{name: 'George', id: 3},
{name: 'Pete', id: 4}
];
What is the recommended way to find the item with id: 4 and update the name Pete to Ringo in a Redux store, with no mutations?
Edit: I know that changing the shape of the object (using an object with an ID key) would be better than an array, but I have no control over the shape of data and have to use the above shape.
You'd want to use the .map() method inside your reducer. Your action will include a payload of id which is the id of the peep to be updated, and a newName to use to update.
Your peeps state:
const state = [
{name: 'John', id: 1},
{name: 'Paul', id: 2},
{name: 'George', id: 3},
{name: 'Pete', id: 4}
];
Your action creator will take an id and a newName argument and create an action with them:
const updateName = (id, newName) => {
return {
type: 'UPDATE_NAME',
payload: {
id
newName
}
};
};
You'd dispatch the action like:
dispatch(updateName(4, 'Ringo'));
Your reducer will receive your action and use the id and newName provided to map through all the peeps, find the one that matches the given id and update its name to be equal to the newName passed in.
const peeps = (state = [], action) => {
switch (action.type) {
case 'UPDATE_NAME':
const { id, newName } = action.payload;
// This returns a new array instead of mutating the old one
return state.map(peep => {
if (peep.id === id) {
peep.name = newName;
}
return peep;
});
default:
return state;
}
};
Related
I have a problem.
I fetch data with 2 parameters.
user_id and movie_channel
so a user has multiple movie channels like 1,2 or 3.
I fetch now a query with this params:
user_id: 1, movie_channel: 1
obj:
return {
user: {
user_id: 1,
username: 'assa',
is_follow: false
},
movie_channel: 1,
movies: []
}
then I get a list of movies from this channel and you get users information.
So anyone select now movie_channel 2, then I fetch again and get the obj with different movies.
in the header he can follow a person. (he is current now in movie channel 2)
he can now change the movie_channel to 1 and then I get the cached data. But now user is not followed because he followed in the channel 2. the cache shows the old obj.
how can I change all cached data where only the param is user_id ?
useGetProfileData: builder.query<IProfilePageData, { user_id: number; movie_channel?: number; }>({
query: (data) => ({
url: '/profile_data',
method: 'POST',
body: data
}),
}),
followUser: builder.mutation<void, { user_id: number; follower_id: number; movie_channel?: number; }>({
query: (data) => ({
url: '/follow_user',
method: 'POST',
body: data
}),
async onQueryStarted({ user_id, follower_id, movie_channel }, { dispatch, queryFulfilled }){
const patchResult = dispatch(
ProfileApi.util.updateQueryData('useGetProfileData', { user_id, movie_channel }, (draft) => {
return {
...draft,
user: {
...draft.user,
is_follow: !draft.user.is_follow
}
}
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
}
}),
I have a deeply nested data object that comes back from my API which looks like the JSON below.
I am using Redux toolkit's createSlice to create a slice of a trip
So currently in my createSlice, I want to store an array of trips.
I also want the ability to update a single trip or part of the trip
for example, let's say I want to update a trip item's start date
or, let's say I want to update a trip item's member's name
My questions and concerns:
I currently have all of these entities coming back into the trip createSlice but I am not sure if, once the entities are normalized, should they be separated into separate createSlices? if so, how is this done or is this an anti pattern?
how should nested entities be defined in initialState?
should I define all of my normalized entities in my initalState?
if I do that, how would my reducers look like when I want to update a trip_item or trip_item_member ?
does my normalized data even look "correct"? I have omitted using mergeStrategy between trips_items_members and trip_members which I know I should do but haven't figured out how that works yet or if it's necessary here?
Note:
There is an example in the RTK docs here which shows createSlice being used with 3 separate entities, which originally came from 1 API call. It looks like 3 separate files however it is unclear how data is shared amongst them.
This is how my trip createSlice looks like
/**
* Get trip by ID action
*/
export const getTripByID = createAsyncThunk(
'trips/getTripByID',
async ({ uid }) => {
const response = await findOne(uid)
const normalized = normalize(response, trip)
return normalized.entities
},
)
const tripsAdapter = createEntityAdapter({
selectId: entity => entity.trip_id,
sortComparer: (a, b) => b.start_date.localeCompare(a.start_date),
loading: '',
error: '',
data: [],
})
export const {
selectById: selectTripById,
selectIds: selectTripIds,
selectEntities: selectTripEntities,
selectAll: selectAllTrips,
selectTotal: selectTotalTrips,
} = tripsAdapter.getSelectors(state => state.trip)
const initialState = tripsAdapter.getInitialState()
const tripSlice = createSlice({
name: 'trips',
initialState,
extraReducers: builder => {
builder.addCase(getAllTrips.fulfilled, (state, { payload }) => {
tripsAdapter.upsertMany(state, payload)
state.loading = false
})
builder.addCase(getTripByID.fulfilled, (state, { payload }) => {
console.log('payload', payload)
tripsAdapter.upsertMany(state, payload)
state.loading = false
})
},
})
export default tripSlice.reducer
API response that comes back from await findOne(uid)
{
created_by: "6040c2d1-ea57-43b6-b5f2-58e84b220f4e",
deleted_by: null,
destination: "Valencia",
end_date: "2020-10-04",
start_date: "2020-09-27",
trip_id: "34a620e8-51ff-4572-b466-a950a8ce1c8a",
uid: "14047a5b-2fe5-46c9-b7f2-e9b5d14db05b",
updated_by: null,
trip_items: [
{
destination: "Mezzanine Level Shivaji Stadium Metro Station, Baba Kharak Singh Rd, Hanuman Road Area, Connaught Place, New Delhi, Delhi 110001, India",
end_date: "2020-09-28",
end_time: "2020-09-28T01:20:15.906Z",
note: null,
start_date: "2020-09-28",
start_time: "2020-09-28T01:20:15.906Z",
trip_item_id: "bd775be7-2129-42c0-a231-5a568b0f565d",
trips_items_members: [
{
trip_item_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc",
uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02",
}
],
uid: "e5f81a6d-1a0d-4456-9d4e-579e80bc27d8",
}
],
trips_members: [
{
trip_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc",
uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02",
role: "ADMIN"
}
]
}
This is my normalizr schema
const tripItemMember = new schema.Entity(
'trips_items_members',
{},
{ idAttribute: 'trip_item_member_id' },
)
const tripItem = new schema.Entity(
'trips_items',
{
trips_items_members: [tripItemMember],
},
{
idAttribute: 'trip_item_id',
},
)
const tripMember = new schema.Entity(
'trips_members',
{},
{
idAttribute: 'trip_member_id',
},
)
export const trip = new schema.Entity(
'trip',
{
trips_items: [tripItem],
trips_members: [tripMember],
},
{
idAttribute: 'trip_id',
},
)
This is the output from normalizr
trip: {
"34a620e8-51ff-4572-b466-a950a8ce1c8a": {
created_by: "6040c2d1-ea57-43b6-b5f2-58e84b220f4e"
deleted_by: null
destination: "Valencia"
end_date: "2020-10-04"
start_date: "2020-09-27"
trip_id: "34a620e8-51ff-4572-b466-a950a8ce1c8a"
trips_items: ["bd775be7-2129-42c0-a231-5a568b0f565d"]
trips_members: ["76b54a80-4d09-4768-bc5a-4d7e153e66dc"]
uid: "14047a5b-2fe5-46c9-b7f2-e9b5d14db05b"
updated_by: null
}
}
trips_items:{
"0a56da0f-f13b-4c3d-896d-30bccbe48a5a": {
destination: "Mezzanine Level Shivaji Stadium Metro Station"
end_date: "2020-09-28"
end_time: "2020-09-28T01:20:15.906Z"
note: null
start_date: "2020-09-28"
start_time: "2020-09-28T01:20:15.906Z"
trip_item_id: "0a56da0f-f13b-4c3d-896d-30bccbe48a5a"
trips_items_members: []
uid: "25d20a9d-1eb9-4226-926d-4d743aa9d5dc"
}
}
trips_members: {
"76b54a80-4d09-4768-bc5a-4d7e153e66dc": {
role: "ADMIN"
trip_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc"
uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02"
}
}
Your setup is very much like this detailed example from the redux-toolkit docs. They are fetching articles, but each article comes with embedded users and comments. They define separate slices for each of the three entities.
The comments slice has no actions or reducers of its own, but it uses the extraReducers property to respond to the article received action and store the embedded comments.
const commentsAdapter = createEntityAdapter();
export const slice = createSlice({
name: "comments",
initialState: commentsAdapter.getInitialState(),
reducers: {},
extraReducers: {
[fetchArticle.fulfilled]: (state, action) => {
commentsAdapter.upsertMany(state, action.payload.comments);
}
}
});
The fetchArticle action is "owned" by the article slice, but the action payload contains entities from all three types. All slices receive all actions, so the comments and users are able to respond to this action with their own logic. Each slice doesn't have any effect on what the others can or can't do.
In your case you want to create slices for items and members. Instead of calling upsertMany(state, payload), you want the payload to be keyed by entity type so that you can call upsertMany(state, payload.members).
How i can write generic function, which take Array of Objects (any type of Object, possible even null and undefined), and filter it to return just valid items of array? If i write it lite this, i will lose genericity :/
// #flow
// Types
type Person = {
id: string,
name: string,
};
type Car = {
id: string,
color: string,
};
// Function definition
const isNotUndefinedOrNull = item => !(item === null || item === undefined);
export const trimList = (list: Array<any> | $ReadOnlyArray<any>): Array<any> => {
return list.filter(isNotUndefinedOrNull);
};
// Constants
const persons = [{ id: 'p1', name: 'Johny' }, null, undefined];
const cars = [{ id: 'c1', color: 'red' }, null, undefined];
// Calls
const trimmedPersons = trimList(persons);
const trimmedCars = trimList(cars);
PROBLEM is, there i have trimmed cars and persons, but flow doesnt know, there is Cars in the trimmedCars list and neither know there is Persons in trimmedPersons list. Flow see just Array and i dont know, how to write is right, to not lose this info.
Flow try
As flow has a bug with Refine array types using filter we use explicit type casting ((res): any): T[]).
function filterNullable<T>(items: (?T)[]): T[] {
const res = items.filter(item => !(item === null || item === undefined);
return ((res): any): T[]);
}
// Example
const a: number[] = filterNullable([1, 2, null, undefined]);
i found it :)
export function trimList<V>(list: Array<?V> | $ReadOnlyArray<?V>): Array<V> {
return R.filter(isNotUndefinedOrNull, list);
}
I've just normalised the state of an app I'm working on (based on this article) and I'm stuck trying to add/remove items from part of my state tree based on quantity.
Part of my state tree cart is solely responsible for housing the quantity of tickets that are in the cart, organised by ID. When the user changes the quantity, an action is dispatched UPDATE_QTY which has the qty and the id.
The state starts off correct as the incoming data has the qty but I can't seem to figure out the syntax to remove the item from the cart reducer if qty is 0, also how to add it back in if the qty is 1 or more.
Could someone offer advice on the correct syntax to achieve this please?
EDIT: I'm wondering if I'm trying to do too much inside the UPDATE_QTY action and that I should have separate actions for deleting and adding items.
byId reducer
export function byId(state = initialState, action) {
switch (action.type) {
case SET_INITIAL_CART_DATA:
return Object.assign({}, state, action.tickets);
case UPDATE_QTY: // Here, I need to check if action.qty is 0 and if it is I need to remove the item but also add it back in if action.qty > 0
return {
...state,
[action.id]: { ...state[action.id], qty: action.qty }, // Updating the qty here works fine
};
default:
return state;
}
}
Simplfied state tree
const state = {
cart: {
byId: {
'40': { // How can I remove these items when qty is 0 or add back in if > 0?
qty: 0,
id: '40'
},
'90': {
qty: 0,
id: '90'
}
},
allIds: [
[
'40',
'90',
]
]
},
}
I also need the IDs to be reflected in my allIds reducer.
allIds reducer
export function allIds(state = [], action) {
switch (action.type) {
case SET_INITIAL_CART_DATA:
return [...state, ...action.allIds];
case UPDATE_QTY:
return [ONLY IDS WITH QTY]
default:
return state;
}
}
For this I'm not sure if the allIds reducer needs to be connected to the byIds reducer and take information from there. I would love to hear what best practice for something like this would be.
Why have separate reducers for byIds and allIds? I would combine these into one cart reducer and maintain the allIds state with byIds:
case SET_INITIAL_CART_DATA:
// just guessing here...
const { tickets } = action;
const allIds = tickets
.reduce((arr, ticket) => arr.concat(ticket.id), []);
return {
byIds: { ...tickets },
allIds
}
case UPDATE_QTY: {
const { byIds, allIds } = state;
const { id, qty } = action;
const idx = allIds.indexOf(id);
const next = { };
if (qty > 0) {
next.byIds = {
...byIds,
[id]: { id, qty }
};
next.allIds = idx === -1 ? allIds.concat(id) : [ ...allIds ];
return next;
}
next.byIds = { ...byIds };
delete next.byIds[id];
next.allIds = idx === -1 ? [ ...allIds ] : [
...allIds.slice(0, idx),
...allIds.slice(idx + 1)
];
return next;
}
However, what state do you want normalized? If this represents a shopping cart of tickets, the tickets are what would be normalized, and the cart would just represent the quantity of tickets to be purchased. Then your state would look something like this:
{
tickets: {
byIds: {
'1': { id, name, price, ... },
'2': { ... },
'3': { ... },
...
}
allIds: [ '1', '2', '3', ... ]
},
cart: [
{ id: 2, qty: 2 },
{ id: 1, qty: 1 }
]
}
The use of an array for the cart state maintains insertion order.
Sometimes (when you only iterate through ids and get by id) it's enough to remove id from allIds and skip all unnecessary computations.
case actionTypes.DELETE_ITEM: {
const filteredIds = state.allIds.filter(id => id !== action.itemId);
return {
...state,
allIds: filteredIds
};
}
I'm trying to learn what is the correct way to operate with Redux. I see around a lot of examples using the spread operator but I'm getting confused how to use it.
For instance, in the code below I have a very simple example where I fire in sequence the same action twice to simulate some products added to the cart.
the first action adds an array with two products while the second adds one product.
If I use concat to save the product in the state, I get the expected result of a cart array of three products:
"use strict"
import {createStore} from 'redux';
// REDUCERS
const reducer = function(state={cart:[]}, action) {
switch (action.type) {
case "ADD_TO_CART":
//HERE I ADD MY NEW ARRAY TO THE EXISTING ARRAY IN TEH STATE
let cart = [...state.cart].concat(action.payload);
return {...state, cart}
break;
}
return state
}
// CREATE STORE
const store = createStore(reducer);
// SUBSCRIBE
store.subscribe(function() {
console.log("current state is", store.getState());
})
// ACTIONS
store.dispatch({
type: "ADD_TO_CART",
payload:[ {
productId: 1,
name: "Product A",
price: 33.33
},
{
productId: 2,
name: "Product B",
price: 15.00
}]
})
store.dispatch({
type: "ADD_TO_CART",
payload:
[{
productId: 3,
name: "Product C",
price: 40.00
}]
})
Here is the result as I expected:
If instead I use the spread operator as below, I get a strange result where Redux creates two object, one containing two array with the product added from the first fired action and one object containing the third array/products:
"use strict"
import {createStore} from 'redux';
// REDUCERS
const reducer = function(state={cart:[]}, action) {
switch (action.type) {
case "ADD_TO_CART":
//HERE I ADD MY NEW ARRAY TO THE EXISTING ARRAY IN TEH STATE
return {...state, cart:[...state.cart, action.payload]}
break;
}
return state
}
// CREATE STORE
const store = createStore(reducer);
// SUBSCRIBE
store.subscribe(function() {
console.log("current state is", store.getState());
})
// ACTIONS
store.dispatch({
type: "ADD_TO_CART",
payload:[ {
productId: 1,
name: "Product A",
price: 33.33
},
{
productId: 2,
name: "Product B",
price: 15.00
}]
})
store.dispatch({
type: "ADD_TO_CART",
payload:
[{
productId: 3,
name: "Product C",
price: 40.00
}]
})
and this is the result from Chrome console:
The spread example doesn't work because the payload is an array:
payload:[ {
productId: 1,
name: "Product A",
price: 33.33
},
{
productId: 2,
name: "Product B",
price: 15.00
}]
})
The solution is to spread the payload as well:
state = {...state, cart: [...state.cart, ...action.payload] };
Example:
let state = { cart: [{ id: 1 }] };
const action = { payload: [{ id: 2}, { id: 3}] };
state = {...state, cart: [...state.cart, ...action.payload] };
console.log(state);
Note: since state.cart is an array, if you use Array#concat, you can simplify this:
[...state.cart].concat(action.payload);
By removing the spread and the wrapping array:
state.cart.concat(action.payload);