Is there a a way to retain the order so that when update happens does n't move to end of the list.
Here in the code product update the updated item moves to end of the list.
this.updateProductListener = API.graphql(
graphqlOperation(onUpdateProducts)
).subscribe({
next: productData => {
console.log(productData);
const newProduct = productData.value.data.onUpdateProducts;
const prevProducts = this.state.products.filter(
product => product.id !== newProduct.id
);
const updatedProducts = [...prevProducts, newProduct];
this.setState({ products: updatedProducts });
this.updateProductListener.unsubscribe()
}
});
Related
I am using API of a fake store, so when I add a product to my cart I need to update the quantity of this product in the cart and update the quantity on UI. The problem is that product quantity is updating in global state, but not updating on UI. Could you please give any advice on that? Thank you in advance !
const reducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TO_CART":
const newItem = action.payload;
const exist = state.products.find((item) => item.id === newItem.id);
if (exist) {
exist.qty += 1;
return {
...state,
quantity: state.quantity + 1,
};
} else {
newItem.qty = 1;
return {
...state,
products: state.products.concat(newItem),
};
}
}
};
A reducer function should always return the new state, not mutate the existing one. When you do exist.qty += 1, you are mutating the state. Also, in the if block, you are adding a new property in your state when you do quantity: state.quantity + 1,.
Assuming each product at least has these properties: id and quantity, this should work:
const reducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TO_CART":
const newItem = action.payload;
// Clone existing products array
let updatedProducts = [...state.products];
// Find the index of the product you want to add
const existingIndex = updatedProducts.find((item) => item.id === newItem.id);
// If the product exists (it will have an index of > -1)
if (existingIndex > -1) {
// Increase its quantity
const updatedProduct = { ...updatedProducts[index], quantity: ++updatedProducts[index].quantity };
// Update the item in the array
updatedProducts.splice(existingIndex, 1, updatedProduct);
} else {
// Product was not found in products - so add it at the end, with quantity set to 1
updatedProducts = [...updatedProducts, { ...action.payload, quantity: 1 }];
}
// Return the state
return { products: updatedProducts };
}
};
I often use this code to fetch and update data for my like button. It works but I wonder if there is a more effective or cleaner way to do this function.
const isPressed = useRef(false); // check the need to change the like count
const [like, setLike] = useState();
const [count, setCount] = useState(count_like); // already fetch data
const [haveFetch, setHaveFetch] = useState(false); // button block
useEffect(() => {
fetchIsLike(...).then((rs)=>{
setLike(rs);
setHaveFetch(true);
})
return () => {}
}, [])
useEffect(()=>{
if(like) {
// animation
if(isPressed.current) {
setCount(prev => (prev+1));
// add row to database
}
}
else {
// animation
if(isPressed.current) {
setCount(prev => (prev-1));
// delete row from database
}
}
}, [like])
const updateHeart = () => {
isPressed.current = true;
setLike(prev => !prev);
}
I am new to Redux RTK so the problem might not exactly be on calling getSelectors(). However, when I'm using the state that comes from getSelectors() it reloads the entire state.
Problem
The baseline is that I have different Setup objects that I'm calling based on the documentId. These Setup objects are quite large so in the getSetups I am only fetching some basic properties. Then, when the user selects a specific Setup from the dropdown I want to save it in the setupSlice. But when I trigger the dispatch(setSetup(data)) the RTK reloads all the Setups.
I encounter an infinite loop when after fetching all the Setup objects I want to automatically assign the default Setup to the setupSlice.
Extra
Ideally when I assign a Setup to the setupSlice I would like to call the getSetup from RTK to fetch the entire Setup object of that specific Setup and store it in the setupSlice.
I am not sure if this is suppose to be happening but is there anyway to stop it? Otherwise is there any recommendation so I can move forward?
This is the component I'm trying to generate:
const SetupDropdown = () => {
const dispatch = useDispatch()
const { documentId } = useParams()
const { data, isFetching } = useGetSetupsQuery({ documentId })
let setupsMenu;
const { selectAll: selectAllSetups } = getSelectors({documentId})
const allSetups = useSelector(selectAllSetups)
if (!isFetching) {
const defaultSetup = allSetups.find((setup) => setup.default)
setupsMenu = allSetups.map(setup => {
return (<MenuItem value={setup.id}>{setup.name}</MenuItem>)
})
dispatch(setSetup(defaultSetup))
}
const setupId = useSelector(selectSetupId)
const handleChange = async (event) => {
// Here I ideally call the getSetup RTK Query to fetch the entire information of the single setup
const data = {
id: event.target.value,
name: 'Random name'
}
dispatch(setSetup(data))
};
return (
<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel>Setup</InputLabel>
<Select
value={setupId}
onChange={handleChange}
label="Setup"
>
{setupsMenu}
</Select>
</FormControl>
)
}
export default SetupDropdown;
This is the setupApiSlice:
const setupsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
const initialState = setupsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: ({ documentId }) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
transformResponse: responseData => {
return setupsAdapter.setAll(initialState, responseData)
},
providesTags: (result, error, arg) => [
{ type: 'Setup', id: "LIST" },
...result.ids.map(id => ({ type: 'Setup', id }))
]
}),
getSetup: builder.query({
query: ({ documentId, setupId }) => ({
url: `/documents/${documentId}/setups/${setupId}`,
method: 'GET'
})
})
})
})
export const {
useGetSetupsQuery,
useGetSetupQuery
} = setupsApiSlice
// Define function to get selectors based on arguments (query) of getSetups
export const getSelectors = (
query,
) => {
const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select(query)
const adapterSelectors = createSelector(
selectSetupsResult,
(result) => setupsAdapter.getSelectors(() => result?.data ?? initialState)
)
return {
selectAll: createSelector(adapterSelectors, (s) =>
s.selectAll(undefined)
),
selectEntities: createSelector(adapterSelectors, (s) =>
s.selectEntities(undefined)
),
selectIds: createSelector(adapterSelectors, (s) =>
s.selectIds(undefined)
),
selectTotal: createSelector(adapterSelectors, (s) =>
s.selectTotal(undefined)
),
selectById: (id) => createSelector(adapterSelectors, (s) =>
s.selectById(s, id)
),
}
}
This is the setupSplice:
const initialState = {
name: null,
filters: [],
data: {},
status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
const setupSlice = createSlice({
name: 'setup',
initialState,
reducers: {
setSetup: (state, action) => {
console.log('Dispatch')
const setup = action.payload;
console.log(setup)
state.id = setup.id;
state.name = setup.name;
state.filters = setup.filters;
state.data = setup.state;
state.status = 'succeeded';
}
}
})
export const { setSetup } = setupSlice.actions;
export const selectSetupId = (state) => state.setup.id;
export const selectSetupName = (state) => state.setup.name;
export const selectSetupFilters = (state) => state.setup.filters;
export const selectSetupData = (state) => state.setup.data;
export default setupSlice.reducer;
Tbh., you probably should be using selectFromResult in your useGetSetupsQuery instead of adding another useSelector hook. That would also reduce your code complexity by a lot.
Your problem as hand is that you are creating those selectors within your component on each render - so they don't have a chance to actually memoize and give you a stable result. If you do that in your component, wrap it in a useMemo call to keep your selector instances as stable as possible.
I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.
Item List Container
onDragEnd = (result) => {
if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
return;
}
this.props.itemStore.reorderItem(result);
}
droppedOnSamePosition = ({ destination, source }) => destination.droppableId
=== source.droppableId && destination.index === source.index;
droppedOutsideList = result => !result.destination;
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<div>
{this.props.categories.map((category, index) => (
<ListCategory
key={index}
category={category}
droppableId={category._id}
/>
))}
</div>
</DragDropContext>
);
}
Item Category
const ListCategory = ({
category, droppableId,
}) => (
<Droppable droppableId={String(droppableId)}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<ListTitle
title={category.name}
/>
<ListItems category={category} show={category.items && showIndexes} />
{provided.placeholder}
</div>
)}
</Droppable>
);
List items
<Fragment>
{category.items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
/>
))}
</Fragment>
Items
render() {
const {
item, index, categoryIndex, itemStore,
} = this.props;
return (
<Draggable key={index} draggableId={item._id} index={index}>
{(provided, snapshot) => (
<div
role="presentation"
className={cx({
'list-item-container': true,
'selected-list-item': this.isSelectedListItem(item._id),
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
onClick={this.handleItemClick}
>
<div className={cx('select-title')}>
<p className={cx('list-item-name')}>{item.title}</p>
</div>
{capitalize(item.importance)}
</div>
</div>
)}
</Draggable>
);
}
Method to reorder Items (I'm using Mobx-State_Tree)
reorderItem: flow(function* reorderItem(result) {
const { source, destination } = result;
const categorySnapshot = getSnapshot(self.itemCategories);
const sourceCatIndex = self.itemCategories
.findIndex(category => category._id === source.droppableId);
const destinationCatIndex = self.itemCategories
.findIndex(category => category._id === destination.droppableId);
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
if (sourceCatIndex === destinationCatIndex) {
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
try {
yield itemService.bulkEditPriorities(prioritizedItems);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
} else {
const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
destinationCatItems.splice(destination.index, 0, draggedItem);
const prioritizedSourceItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);
const prioritizedDestItems = setItemPriorities(destinationCatItems);
applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
try {
const sourceCatId = categorySnapshot[sourceCatIndex]._id;
const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
}
}),
Sample data
const itemData = [
{
_id: 'category-1',
title: 'Backlog',
items: [
{ _id: 'item-1', title: 'Here and back again' },
},
{
_id: 'category-2',
title: 'In progress',
items: []
},
{
_id: 'category-3',
title: 'Done',
items: []
}
}
}
Summary
When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing.
If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.
If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call.
Am I missing something?
I am using both mst and the react-beautiful-dnd library
I will just paste my onDragEnd action method
onDragEnd(result: DropResult) {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
(self as any).reorder(source.index, destination.index);
}
},
reorder(source: number, destination: number) {
const tempLayout = [...self.layout];
const toMove = tempLayout.splice(source, 1);
const item = toMove.pop();
tempLayout.splice(destination + lockedCount, 0, item);
self.layout = cast(tempLayout);
},
I think in order to avoid the flicker you need to avoid using applySnapshot
You can replace this logic
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
just splice the items tree
const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)
this way you don't need to applySnapshot on the source items after
I believe this issue is caused by multiple dispatches happening at the same time.
There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.
So especially you need to put a timeout to onDragStart.
const invoke = (fn: any) => { setTimeout(fn, 0) }
Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.
const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
const [dragging, setDragging] = useState(false)
const [dropping, setDropping] = useState(false)
const dragRef = useRef(null)
const lastEnteredEl = useRef(null)
const onDragStart = useCallback((e: DragEvent) => {
const el: HTMLElement = dragRef.current
if (!el || (
document.elementFromPoint(e.clientX, e.clientY) !== el
)) {
e.preventDefault()
return
}
e.dataTransfer.setData("index", `${index}`)
invoke(() => { setDragging(true) })
}, [setDragging])
const onDragEnd = useCallback(() => {
invoke(() => { setDragging(false) })
}, [setDragging])
const onDrop = useCallback((e: any) => {
invoke(() => { setDropping(false) })
const from = parseInt(e.dataTransfer.getData("index"))
onItemDrop && onItemDrop(from, index)
}, [setDropping, onItemDrop])
const onDragEnter = useCallback((e: DragEvent) => {
lastEnteredEl.current = e.target
e.preventDefault()
e.stopPropagation()
setDropping(true)
}, [setDropping])
const onDragLeave = useCallback((e: DragEvent) => {
if (lastEnteredEl.current !== e.target) {
return
}
e.preventDefault()
e.stopPropagation()
setDropping(false)
}, [setDropping])
return (
<DndItemStyle
draggable="true"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
dragging={dragging}
dropping={dropping}
>
{(index < 100) && (
cloneElement(children as ReactElement<any>, { dragRef })
)}
</DndItemStyle>
)
})
I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few
onDragEnd, to sugar code the indicator
onDrop, to re-order
I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.
In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.
I try to retrieve datas in a subcollection based on the key received on the first call.
Basically, I want a list of all my user with the total of one subcollection for each of them.
I'm able to retrieve the data from the first Payload, but not from pointRef below
What is the correct way to achieve that?
getCurrentLeaderboard() {
return this.afs.collection('users').snapshotChanges().map(actions => {
return actions.map(a => {
const data = a.payload.doc.data()
const id = a.payload.doc.id;
const pointRef: Observable<any> = this.afs.collection('users').doc(`${id}`).collection('game').valueChanges()
const points = pointRef.map(arr => {
const sumPoint = arr.map(v => v.value)
return sumPoint.length ? sumPoint.reduce((total, val) => total + val) : ''
})
return { id, first_name: data.first_name, point:points };
})
})
}
I tried to put my code in a comment, but I think it's better formated as a answer.
First you need subscribe your pointRef and you can change your code like this.
getCurrentLeaderboard() {
return this.afs.collection('users').snapshotChanges().map(actions => {
return actions.map(a => {
const data = a.payload.doc.data()
const id = a.payload.doc.id;
const pointRef: Observable<any> = this.afs.object(`users/${id}/game`).valueChanges() // <--- Here
const pointsObserver = pointRef.subscribe(points => { //<--- And Here
return { id, first_name: data.first_name, point:points };
})
})
}
....
//Usage:
getCurrentLeaderboard.subscribe(points => this.points = points);
And if you going to use this function alot, you should start to denormalize your data.