I want to hide the selected option in the Dropdown, the option should not appear in the next dropdown. For an example, there are 2 dropdowns, in the first dropdown - i have selected "Hockey" then "hockey" should not be shown in the second dropdown, It should show only "Baseball and badminton".
My JSON data will be appearing in this way:
"details": [
{ "id": "12wer1", "name": "ABC", "age": 15, "game": "badminton" },
{ "id": "78hbg5", "name": "FRE", "age": 21, "game": "Hockey" }
]
Here is the sample Code:
let games = [{ game: "Baseball"}, { game: "Hockey"}, { game: "badminton" }];
class Field extends React.Component {
constructor(props) {
super(props);
this.state = {
details: [{id: '', name: '', age: '', game: ''}]
}
}
...
...
render() {
return (
...
...
{this.state.details.map((y) => (
<Autocomplete
style={{ witdth: 200 }}
options={games}
getOptionLabel={(option) => option.game}
onChange={(value) =>this.onGameChange(value, y.id)}
value={games.filter(i=> i.game=== y.game)}
renderInput={(params) =>
<TextField {...params} label="Games" margin="normal" />
}
/>))}
...
...
)
}
}
onGameChange = (e, id)=> {
let games = this.state.details;
let data = games.find(i => i.id === id);
if (data) {
data.game = value.game;
}
this.setState({ details: games });
}
I have no idea, how to hide the selected option, can anyone help me in this query?
Thanks! in advance.
A possible solution would be to
create an array and store the values in an array when the user selects autocomplete
while passing options, filter the values that have been passed to other autocompletes.
const ary = [111,222,333];
let obj = [{id: 111},{id: 222}];
const i = 1; // this is your index in loop
const ary2 = ary.slice()
ary2.splice(i,1);
console.log(obj.filter((o) => !ary.includes(o.name))); // this should be given to our options list in autocomplete
you can hide this is in CSS easily no need to do anything in ReactJS
autocomplete renders as an unordered list so something like this
.panel > ul > li:first-child {
display:none;
}
Related
I am new to Vue and although I could find way around most problems I've encountered, this one has been bugging me for last two days and just cannot find solution. Any help much appreciated, thanks in advance!
I've got following code:
<template>
<header><h1>HomeShop</h1></header>
<main>
<section>
<Item-Card
v-for="(item, index) in items"
:key="index"
:item="item.item"
:check="item.check"
#item-checked="checkClicked(index, item.id, items)"
#item-deleted="deleteClicked(item.id, items)"
/>
<Add-New-Item #item-submited="newItemCard" />
</section>
</main>
<button #click="$log(JSON.stringify(items, null, 1))">Log</button>
</template>
<script setup>
import { reactive, toRefs } from "vue";
import axios from "axios";
import _ from "lodash";
//data + explicit expression
const state = reactive({
items: [],
});
const { items } = toRefs(state);
//fce
const checkClicked = (index, id, items) => {
if (items[index].check === false) {
items[index].check = true;
items[index].checkTime = _.now();
items = _.sortBy(items, ["check", "checkTime"])
console.log(JSON.stringify(items, null, 1));
axios.patch("http://localhost:3000/items/" + id, { check: true });
} else {
items[index].check = false;
delete items[index].checkTime;
axios.patch("http://localhost:3000/items/" + id, { check: false });
}
};
I pass my data object array (items) as argument to checkClicked method. This function changes check status and adds checkTime timestamp based on which I sort the array (using lodash method sortBy). From within the checkClicked method I log the items array with expected correct result:
[
{
"item": "2",
"check": false,
"id": 2
},
{
"item": "3",
"check": false,
"id": 3
},
{
"item": "4",
"check": false,
"id": 4
},
{
"item": "5",
"check": true,
"id": 5,
"checkTime": 1671719753796
},
{
"item": "1",
"check": true,
"id": 1,
"checkTime": 1671719755363
}
]
However, when I log items from outside the method (the custom function $log at template) I get following result:
[
{
"item": "1",
"check": true,
"id": 1,
"checkTime": 1671719755363
},
{
"item": "2",
"check": false,
"id": 2
},
{
"item": "3",
"check": false,
"id": 3
},
{
"item": "4",
"check": false,
"id": 4
},
{
"item": "5",
"check": true,
"id": 5,
"checkTime": 1671719753796
}
]
How do I manipulate the reactive data object array items from inside the function checkClicked? My intention is to sort the data array each time timestamp checkTime is added (that is what the checkClicked function does).
My understanding is that passing the data object array items as argument into function creates separate instance of the array, that is why I am getting two different results while loging the array. However I cannot find solution how to manipulate the real items from inside the function checkClicked.
You are right. You are only changing the parameter variable items in the function. See my other answer for the clarification.
Pay also attention to the following:
When you assign a new value to your reactive proxy object, you can lose the reactivity.
Here it is:
items = _.sortBy(items, ["check", "checkTime"])
I guess, this line could also remove reactivity from the array.
There are ways to fix it, by passing the array through vue reactivity system again.
But, this causes too much work for vue to full recalculate the array items and is not efficient.
My way to provide sorted results is to use a Computed Property.
Like this:
<Item-Card v-for="(item, index) in sortedItems">
and then
const sortedItems = computed(() => {
return _.sortBy(items, ["check", "checkTime"])
})
The advantage of using a computed property is that, it will be recalculated by Vue Reactivity System only when your items array changes.
It looks like you only change the local variable items in the checkClicked() function. I have build a test playground to check it.
You have to change the line items = []; to this.items = []; if you want to change the property, not the parameter variable.
See my second answer for the right sorting solution.
const { ref, reactive, createApp, toRefs } = Vue;
const data = [
{
"item": "1",
"check": true,
"id": 1,
"checkTime": 1671719755363
},
{
"item": "2",
"check": false,
"id": 2
},
{
"item": "3",
"check": false,
"id": 3
}
]
const App = {
methods: {
clear(items) {
items = [];
}
},
setup() {
//data + explicit expression
const state = reactive({
items: data,
});
const { items } = toRefs(state);
return {
items
}
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<div v-for="(item, index) in items"
:key="index"
:item="item.item"
:check="item.check"> #{{index}}: {{item.item}}
</div>
<button #click="clear(items)">clear</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js">
</script>
sending update with solution I got at another forum. I feel dumbed I would not figured that out in two days on my own, whatever. Although advice from Tolbxela can be right track it brings as well more problems so in the end I prefer the more cleaner way:
"I can reference the state declared within . i.e. don’t pass it as a value to the function."
Thats it. Simply not passing the data object array into function, just reference it right away from within the function. So the correct code goes like this:
<template>
<header><h1>HomeShop</h1></header>
<main>
<section>
<Item-Card
v-for="(item, index) in items"
:key="item.id"
:item="item.item"
:check="item.check"
#item-checked="checkClicked(index, item.id)"
#item-deleted="deleteClicked(item.id, items)"
/>
<Add-New-Item #item-submited="newItemCard" />
</section>
</main>
<button #click="$log(JSON.stringify(sortedItems, null, 1))">Log</button>
</template>
<script setup>
import { reactive, toRefs, computed } from "vue";
import axios from "axios";
import _ from "lodash";
//data + explicit expression
const state = reactive({
items: [],
});
const { items } = toRefs(state);
//fce
const checkClicked = (index, id) => {
if (state.items[index].check === false) {
state.items[index].check = true;
state.items[index].checkTime = _.now();
state.items = _.sortBy(state.items, ["check", "checkTime"])
I have implemented Global Filer using setGlobalFilter
But I want to Set filter column wise like:
Outside the table, I need a dropdown with some values which should filter the react-table based on the opted value from the dropdown.
this dropdown filter should filter out the whole table based on the opted value from the dropdown.
Can anyone give me link of demo or any help regarding this will be appreciated.
Ok, let's do it on react-table v8, yes, it is the new version of react-table.
First, make sure you import the required items from #tanstack/react-table
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
} from "#tanstack/react-table";
Here, we use simple data.
const defaultData = [
{
firstName: "tanner",
lastName: "linsley",
age: 24,
visits: 100,
status: "In Relationship",
progress: 50,
},
{
firstName: "tandy",
lastName: "miller",
age: 40,
visits: 40,
status: "Single",
progress: 80,
},
{
firstName: "joe",
lastName: "dirte",
age: 45,
visits: 20,
status: "Complicated",
progress: 10,
},
];
In the new version of react-table, we do not have to memoize the columns. We can define it with the help of columnHelper. You can read the details here.
const columnHelper = createColumnHelper();
const columns = [
columnHelper.accessor("firstName", {
header: "First Name",
}),
columnHelper.accessor("lastName", {
header: "Last Name",
}),
columnHelper.accessor("age", {
header: "Age",
}),
columnHelper.accessor("visits", {
header: "Visits",
}),
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("progress", {
header: "Profile Progress",
}),
];
Next, we define required states at our component.
const [data] = useState(() => [...defaultData]);
// to keep the selected column field
const [field, setField] = useState();
// to keep the input search value
const [searchValue, setSearchValue] = useState("");
// required by react-table for filtering purposes
const [columnFilters, setColumnFilters] = useState();
In the previous version, we use useTable hooks to create our table instance, here, in the new version, we use useReactTable instead. We pass these configurations to make our filter run correctly.
const table = useReactTable({
data,
columns,
enableFilters: true,
enableColumnFilters: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: {
columnFilters,
},
onColumnFiltersChange: setColumnFilters,
});
Next, we create our select option tag where we bind the select option value to field state and the onChange event handler to handleSelectChange. For the input tag, we bind the value to searchValue and and the onChange event handler to handleInputChange method.
Inside the select change handler, we need to reset both columnFilters and searchValue states.
...
const handleSelectChange = (e) => {
setColumnFilters([]);
setSearchValue("");
setField(e.target.value);
};
const handleInputChange = (e) => {
setSearchValue(e.target.value);
};
return (
...
<select value={field} onChange={handleSelectChange}>
<option value="">Select Field</option>
{table.getAllLeafColumns().map((column, index) => {
return (
<option value={column.id} key={index}>
{column.columnDef.header}
</option>
);
})}
</select>
<input
value={searchValue}
onChange={handleInputChange}
className="p-2 font-lg shadow border border-block"
placeholder={
field ? `Search ${field} column...` : "Please select a field"
}
/>
...
)
Here, we got the select options list from table.getAllLeafColumns().
Since columns age, visits, and progress value is number, we need to modify our columns configurations by using custom filterFn options.
const columns = [
...
columnHelper.accessor("age", {
header: "Age",
filterFn: (row, _columnId, value) => {
return row.original.age === parseInt(value);
},
}),
columnHelper.accessor("visits", {
header: "Visits",
filterFn: (row, _columnId, value) => {
return row.original.visits === parseInt(value);
},
}),
...
columnHelper.accessor("progress", {
header: "Profile Progress",
filterFn: (row, _columnId, value) => {
return row.original.progress === parseInt(value);
},
}),
];
And as the documentation said that we need to remember this:
Every filter function receives:
The row to filter
The columnId to use to retrieve the row's value
The filter value
and should return true if the row should be included in the filtered rows, and false if it should be removed.
Finally, it is time to render the table:
return (
<div className="p-2">
...
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
And here is the working code:
I hope it helps.
I'm trying to do a pagination where the user can see each button's page number in the UI. I'm using Firestore and Buefy for this project.
My problem is that Firestore is returning wrong queries for this case. Sometimes (depending the page that the users clicks on) It works but sometimes don't (It returns the same data of the before page button).
It's really messy I don't understand what's going on. I'll show you the code:
Vue component: (pay attention on the onPageChange method)
<template>
<div>
<b-table
:data="displayData"
:columns="table.columns"
hoverable
scrollable
:loading="isLoading"
paginated
backend-pagination
:total="table.total"
:per-page="table.perPage"
#page-change="onPageChange">
</b-table>
</div>
</template>
<script>
import { fetchBarriosWithLimit, getTotalDocumentBarrios, nextBarrios } from '../../../../firebase/firestore/Barrios/index.js'
import moment from 'moment'
const BARRIOS_PER_PAGE = 5
export default {
data() {
return {
table: {
data: [],
columns: [
{
field: 'name',
label: 'Nombre'
},
{
field: 'dateAddedFormatted',
label: 'Fecha añadido'
},
{
field: 'totalStreets',
label: 'Total de calles'
}
],
perPage: BARRIOS_PER_PAGE,
total: 0
},
isLoading: false,
lastPageChange: 1
}
},
methods: {
onPageChange(pageNumber) {
// This is important. this method gets fired each time a user clicks a new page. I page number that the user clicks.
this.isLoading = true
if(pageNumber === 1) {
console.log('show first 5...')
return;
}
const totalPages = Math.ceil(this.table.total / this.table.perPage)
if(pageNumber === totalPages) {
console.log('show last 5...')
return;
}
/* Here a calculate the next starting point */
const startAfter = (pageNumber - 1) * this.table.perPage
nextBarrios(this.table.perPage, startAfter)
.then((querySnap) => {
this.table.data = []
this.buildBarrios(querySnap)
console.log('Start after: ', startAfter)
})
.catch((err) => {
console.err(err)
})
.finally(() => {
this.isLoading = false
})
},
buildBarrios(querySnap) {
querySnap.docs.forEach((docSnap) => {
this.table.data.push({
id: docSnap.id,
...docSnap.data(),
docSnapshot: docSnap
})
});
}
},
computed: {
displayData() {
let data = []
this.table.data.map((barrioBuieldedObj) => {
barrioBuieldedObj.dateAddedFormatted = moment(Number(barrioBuieldedObj.dateAdded)).format("DD/MM/YYYY")
barrioBuieldedObj.totalStreets ? true : barrioBuieldedObj.totalStreets = 0;
data.push(barrioBuieldedObj)
});
return data;
}
},
mounted() {
// obtener primer paginacion y total de documentos.
this.isLoading = true
getTotalDocumentBarrios()
.then((docSnap) => {
if(!docSnap.exists || !docSnap.data().totalBarrios) {
// mostrar mensaje que no hay barrios...
console.log('No hay barrios agregados...')
this.table.total = 0
return;
}
const totalBarrios = docSnap.data().totalBarrios
this.table.total = totalBarrios
if(totalBarrios <= BARRIOS_PER_PAGE) {
return fetchBarriosWithLimit(totalBarrios)
} else {
return fetchBarriosWithLimit(BARRIOS_PER_PAGE)
}
})
.then((querySnap) => {
if(querySnap.empty) {
// ningun doc. mostrar mensaje q no hay barrios agregados...
return;
}
this.buildBarrios(querySnap)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
this.isLoading = false
})
}
}
</script>
<style lang="scss" scoped>
</style>
The nextBarrios function:
function nextBarrios(limitNum, startAtNum) {
const query = db.collection('Barrios')
.orderBy('dateAdded')
.startAfter(startAtNum)
.limit(limitNum)
return query.get()
}
db is the result object of calling firebase.firestore(). Can I tell a query to start at a certain number where number is the index position of the document within a collection? If not, How could I approach this problem?
Thank you!
Firestore doesn't support offset or index based pagination. It's also not possible to tell how many documents the entire query would return without actually reading them all. So, unfortunately, what you're trying to do isn't possible with Firestore.
It seems also that you're misunderstanding how the pagination APIs actually work. startAfter doesn't take an index - it takes either a DocumentSnapshot of the last document in the prior page, or a value of the ordered field that you used to sort the query, again, the last value you saw in the prior page. You are basically going to use the API to tell it where to start in the next page of results based on what you found in the last page. That's what the documentation means when it says you are working with a "query cursor".
I am using react-table in my application.
I am stuck in doing one thing i.e. changing the CSS of columns while a column is being resized.
Currently when you resize a column only cursor changes. What I want is to add border to the selected column.
I searched for this on SO and google as well. But couldn't find anything useful. And In the documentation as well nothing is mentioned about this topic as well.
Update
Now I am able to add border while dragging the column while resizing. I am able to do so by adding and removing the class.
What I did to do so:
Created a var in the state for className:
this.state = {
addBorder: null
}
Passed this class name in my column:
const columns = [{
Header: 'Name',
accessor: 'name', // String-based value accessors!,
headerClassName: this.state.addBorder,
className: this.state.addBorder
}, {
Header: 'Age',
accessor: 'age',
Cell: props => <span className='number'>{2}</span> // Custom cell components!
}, {
id: 'friendName', // Required because our accessor is not a string
Header: 'Friend Name',
accessor: d => d.friend.name // Custom value accessors!
}, {
Header: props => <span>Friend Age</span>, // Custom header components!
accessor: 'friend.age'
}];
return (
<div onMouseUp={this.handleMouseUp}>
<ReactTable
data={data}
columns={columns}
resizable={true}
onResizedChange={(col, e) => {
const column = col[col.length-1];
this.setState({addBorder: column.id})
}} />
</div>
)
}
To remove the class when dragging ends:
handleMouseUp (e) {
this.setState({addBorder: null});
}
But I am still not able to add border on hover.
Now, I am sending my custom HTML in header props. And in my HTML I have made an extra div. And I have moved this div to right. And on hover of this div, I am emitting mouse events and changing CSS accordingly.
But Existing div in the header that is responsible for resizing column is overlapping with my Div.
Header: props => <div className='header-div'> Name <div onMouseOver = {() => {
console.log('mose');
this.setState({className: 'addBorder'});
}} className='hover-div' onMouseOut = {() => {console.log('sdasd');this.setState({className: null});}}> </div></div> ,
From what I understand, you want to add some border when you hover over a column header. If my understanding is correct, you can use :hover pseudo selector over the header class
.hdrCls:hover {
border: 2px solid rgba(0,0,0,0.6) !important;
}
Update :
You can manipulate state in onResizedChange handler exposed by react-table
onResizedChange={(newResized, event) => {
let resizedCol = newResized.slice(-1)[0].id;
if(this.state.activeCol !== resizedCol) {
this.setState({
activeCol: resizedCol,
resizing: true
})
}
}}
Also, make sure you have to make the resizing state to false on mouseup event. For that I have come up with the below solution.
componentDidUpdate(props, state) {
if (this.state.resizing && !state.resizing) {
document.addEventListener('mouseup', this.onMouseUp);
} else if (!this.state.resizing && state.resizing) {
document.removeEventListener('mouseup', this.onMouseUp);
}
}
onMouseUp = (evt) => {
this.setState({
activeCol: '',
resizing: false
});
evt.stopPropagation();
evt.preventDefault();
}
For reference:
const ReactTable = window.ReactTable.default
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
activeCol: '',
resizing: false
}
}
componentDidUpdate(props, state) {
if (this.state.resizing && !state.resizing) {
document.addEventListener('mouseup', this.onMouseUp);
} else if (!this.state.resizing && state.resizing) {
document.removeEventListener('mouseup', this.onMouseUp);
}
}
onMouseUp = (evt) => {
this.setState({
activeCol: '',
resizing: false
});
evt.stopPropagation();
evt.preventDefault();
}
render() {
const data = [{
name:"Mark",
age:24
},
{
name:"Derek",
age:26
}]
const columns = [{
Header: 'Name',
accessor: 'name', // String-based value accessors!,
headerClassName: 'hdrCls',
className: (this.state.activeCol === 'name') && this.state.resizing ? 'borderCellCls' : 'defaultCellCls'
}, {
Header: 'Age',
accessor: 'age',
headerClassName: 'hdrCls',
className: (this.state.activeCol === 'age') && this.state.resizing ? 'borderCellCls' : 'defaultCellCls'
}];
return <ReactTable
data = { data }
columns = { columns }
showPagination= {false}
onResizedChange={(newResized, event) => {
let resizedCol = newResized.slice(-1)[0].id;
if(this.state.activeCol !== resizedCol) {
this.setState({
activeCol: resizedCol,
resizing: true
})
}
}}
/>
}
}
ReactDOM.render( < App / > , document.getElementById("app"))
.hdrCls:hover {
border: 2px solid rgba(0,0,0,0.6) !important;
}
.borderCellCls {
border-right: 2px solid rgba(0,0,0,0.6) !important;
border-left: 2px solid rgba(0,0,0,0.6) !important;
}
.defaultCellCls {
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-table/6.7.6/react-table.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/react-table/6.7.6/react-table.css"></link>
<div id="app"></div>
You can play around with CSS. Hope this is what you want and hope this helps.
Update:
I think you have to play with CSS to achieve what you desire.
.borderCellCls {
border-right: 2px solid rgba(0,0,0,0.6) !important;
border-left: 2px solid rgba(0,0,0,0.6) !important;
}
If you are here to find out how to set className to a column cell (with the react-table), here is the solution:
1)
<tr
{...row.getRowProps()}
>
{row.cells.map((cell) => (
<td
{...cell.getCellProps([
{
className: cell.column.className, // pay attention to this
style: cell.column.style,
// set here your other custom props
},
])}
>
{cell.render('Cell')}
</td>
))}
</tr>
2)
const columns = React.useMemo(
() => [
{
Header: 'Date',
accessor: 'date',
minWidth: 70,
className: 'text-dark fw-bolder fs-6 min-w-70px', // pass className props here
headerClassName: 'text-muted', // or another props like this one
}]
<Table columns={columns} ... />
And finally, those props will be passed to your cells
For TypeScript support follow the instructions in DefinitelyTyped, ie. create the file /src/types/react-table-config.d.ts with the content from the instructions, then add the following to it to support custom properties on your column (add more properties in the last line as required):
// Added to support classes to template from:
// https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-table
export interface ColumnInstance<
D extends Record<string, unknown> = Record<string, unknown>
> extends Omit<ColumnInterface<D>, 'id'>,
ColumnInterfaceBasedOnValue<D>,
UseTableColumnProps<D>,
Record<headerClassName | className, string> {}
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
};
}