I am using normalizr to organize my redux-store state.
Let's say that I have normalized todo-list:
{
result: [1, 2],
entities: {
todo: {
1: {
id: 1,
title: 'Do something'
},
2: {
id: 2,
title: 'Second todo'
}
}
}
}
Then I would like to implement addTodo action. I need to have an id in todo object, so I generate a random one:
function todoReducer(state, action) {
if(action.type == ADD_TODO) {
const todoId = generateUUID();
return {
result: [...state.result, todoId],
enitities: {
todos: {
...state.entities.todos,
[todoId]: action.todo
}
}
}
}
//...other handlers...
return state;
}
But the problem is that eventually all data will be saved to server and generated id should be replaced with real server-assigned id. Now I merge them like this:
//somewhere in reducer...
if(action.type === REPLACE_TODO) {
// copy todos map, add new entity, remove old
const todos = {
...state.entities.todos
[action.todo.id]: action.todo
};
delete todos[action.oldId];
// update results array as well
const result = state.result.filter(id => id !== oldId).concat(action.todo.id);
// return new state
return {entities: {todos}, result};
}
It seems to be a working solution, but there also a lot of overhead. Do you know any way to simplify this and don't make REPLACE_TODO operation?
Related
I'm struggling with a recursive loop and nested create/select statements. I'm receiving an object from a post request with the following structure:
11.6042
---11.6042_01
---11.6042_02
---11.6042_02
---14x10-100
------14x10-100_01
---14x10-100
------14x10-100_01
---14x10-100
------14x10-100_01
---M10-DIN929_14020
---M10-DIN929_14020
---11.6042_05
Wanted behaviour: travel through the structure recursive, add record to Part table, self join with parent part, join with PartLib table, if no match present create PartLib record and match created record. Process next part.
The problem: part 14x10-100 occurs three times in the structure. I want to create a record for part 14x10-100 in the part_lib table and refer to that record three times. What actually happens is that for each 14x10-100 part a corresponding record in the part_lib table is created in stead of one create and two matches. If I run it again it will match like excpected. I suspect I'm lost in the promise/async await parts of the code.
Below the relevant code. I've removed some attribute mappings for readability. My thoughts behind it: I'm not returning new promises like normal in a async function since Sequelize already returns a promise. When creating a part I'm awaiting (or at least I think so) the partLibController calls to ensure that all matching/creating/joining is done before proceeding to the next part in the structure.
Thanks a bunch!!
Recursive loop
function parseChild(child, modelId, parentId, userId, level) {
return new Promise((resolve, reject) => {
partController.create({
parent_id: parentId
, name: child.name
}, { id: userId }).then((part) => {
resolve({ child: child, level: level });
if (child.children) {
child.children.forEach(grandChild => {
parseChild(grandChild, modelId, part.part_id, userId, level + '---');
});
}
}).catch(error => { console.log(error); });
}).then((obj) => { console.log(`${obj.level} ${obj.child.name}`); });
}
PartController Create
async function create(partBody, currentUser) {
let { parent_id, name } = partBody;
const match = await partLibController.match(name);
let partLibId = null;
if (match.length == 0) {
const partLib = await partLibController.createFromPart(partBody, currentUser);
partLibId = partLib.part_lib_id;
} else {
partLibId = match[0].dataValues.part_lib_id
}
return ModelAssembly.create({
parent_id: parent_id
, name: name
, part_lib_id: partLibId
});
}
PartLibController Match
function match(name) {
return PartLib.findAll({
where: {
name: name
},
});
}
PartLibController CreateFromPart
function createFromPart(partBody, currentUser) {
let { name } = partBody;
return PartLib.create({
name,
});
}
Thanks to AKX I've solved the problem: hero
The problem was in the recursive call itself I suppose but here's the working code:
async function parseChild(child, modelId, parentId, userId, level) {
const body = {
parent_id: parentId
, name: child.name
};
const ma = await partController.create(body, { id: userId });
if (child.children) {
for (const grandChild of child.children) {
await parseChild(grandChild, modelId, ma.part_id, userId, level + '---');
}
}
return;
}
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 trying to take a single record from firebase to use in vuejs but I cant find out how to convert it to an array, if thats even what i should be doing.
my mutation
GET_CASE(state, caseId) {
state.caseId = caseId;
},
My action
getCase ({ commit, context }, data) {
return axios.get('http' + data + '.json')
.then(res => {
const convertcase = []
convertcase.push({ data: res.data })
//result below of what is returned from the res.data
console.log(convertcase)
// commit('GET_CASE', convertcase)
})
.catch(e => context.error(e));
},
I now get the following returned to {{ myCase }}
[ { "data": { case_name: "Broken laptop", case_status: "live", case_summary: "This is some summary content", contact: "", createdBy: "Paul", createdDate: "2018-06-21T15:20:22.932Z", assessor: "Gould", updates: "" } } ]
when all i want to display is Broken Laptop
Thanks
Example let obj = {a: 1, b: 'a'); let arr = Object.values(obj) // arr = [1, 'a']
async getCase ({ commit, context }, url) {
try {
let { data } = await axios.get(`http${url}.json`)
commit('myMutation', Object.values(data))
} catch (error) {
context.error(error)
}
}
But as I'm reading your post again, I think you don't want array from object. You want array with one object. So, maybe this is what you want:
async getCase ({ commit, context }, url) {
try {
let { data } = await axios.get(`http${url}.json`)
commit('myMutation', [data])
} catch (error) {
context.error(error)
}
}
Put this inside / after your .then
Object.keys(data).forEach(function(k, i) {
console.log(k, i);
});
With a response from Axios, you can get your data as:
res.data.case_name
res.data.case_number
....
Just build JavaScript object holding these properties and pass this object to your mutation. I think it is better than using an array.
const obj = {};
Object.assign(obj, res.data);
commit('GET_CASE', obj)
And in your mutation you do as follows:
mutations: {
GET_CASE (state, payload) {
for (var k in payload) {
if (payload.hasOwnProperty(k)) {
state[k] = payload[k]
}
}
}
}
Alternatively you can code your store as follows:
state: {
case: {},
...
},
getters: {
getCase: state => {
return state.case
},
....
},
mutations: {
GET_CASE (state, payload) {
state.case = payload
}
}
and you call the value of a case field form a component as follows:
const case = this.$store.getters.getCase
..... = case.case_name
I have 3 classes derived from Record. Definitions of first two classes are below.
// Base.js
import {Record} from 'immutable';
import * as uuid from 'uuid';
export const Base = defaultValues => {
return class extends Record({
key: null,
...defaultValues,
}) {
constructor(props) {
super(Object.assign({}, props, {key: (props && props.key) || uuid.v4()}));
}
};
};
// LOBase.js
import {Base} from './BaseModel';
export const LOBase = defaultValues => {
return class extends Base({
created_at: new Date(null),
updated_at: new Date(null),
deleted_at: new Date(null),
isActive: new Boolean(),
isDeleted: new Boolean(),
publishState: new String(),
...defaultValues,
}) {};
};
And this is my last class derived from LOBase and where my problem is.
// Question.js
import {List, Record, fromJS} from 'immutable';
import _ from 'lodash';
import {LOBase} from './base/LOBaseModel';
export class Question extends LOBase({
id: '',
name: 'test',
description: '',
questionType: 1,
title: 'title',
version: new String(),
customData: {},
//...
}) {
insertOption() {
let index = this.customData.options.length;
this.updateIn(['customData', 'options'], options => {
return options.splice(index, 0, {
someGenericStuff: [],
// ...
});
});
return this;
}
static MultipleChoice() {
let defaultCustomData = {
options: [],
//...
};
let question = new Question()
.set('questionType', QUESTION_TYPE_MULTIPLE_CHOICE)
.set('customData', new Record(defaultCustomData)())
//...
.insertOption()
.insertOption()
.insertOption();
return question;
}
// ...
}
I use let question = Question.MultipleChoice() to create a new Question instance. And when i use question.insertOption() it works fine. But when I do this in the reducer on the state I get an error saying "A state mutation was detected inside a dispatch".
How can I achieve to change question object in the state? Should I clone original Record before doing that? What is the Immutablejs way to do that?
Thanks in advance.
insertOption uses this.updateIn but does not return or store the result.
When you return this at the end of the function you actually return the same immutable Record without the changes.
So, unless I'm missing something here, you should probably go with:
insertOption() {
let index = this.customData.options.length;
return this.updateIn(['customData', 'options'], options => {
return options.splice(index, 0, {
someGenericStuff: [],
// ...
});
});
}
The updateIn will return a new instance of the Record with the updated values.
You did not add your state structure and reducer (if you can please do), but you should be sure to return a new state object every time and not just changing the question field.
BTW, you are doing a sequence of mutation methods one after the other (set, set, updateIn). This is not suggestable from a performance perspective. I'd suggest replacing it with withMutations in the following manner:
static insertOption(record) {
let index = record.customData.options.length;
return record.updateIn(['customData', 'options'], options => {
return options.splice(index, 0, {
someGenericStuff: [],
// ...
});
});
}
static MultipleChoice() {
// ...
let question = new Question();
question.withMutations(record => {
record.merge({
questionType: QUESTION_TYPE_MULTIPLE_CHOICE,
customData: new Record(defaultCustomData)()
})
Question.insertOption(record);
})
return question;
}
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
};
}