I'm aware of "sacred" data is and how dangerous it might get if handled incorrectly; that's why in my app, I'm in a position now where I need to handle a nested long JSON object (that repsents my app state) and it's already a headache to me to get in and out of the nodes/values that need to be amended. I was thinking of including Immutable.js. The question now is: how do I adapt my reducers, actions, state, etc?
Here's an extract of my state when it comes from the MongoDB database:
"shops": [
{
"shopId": "5a0c67e9fd3eb67969316cff",
"picture": "http://placehold.it/150x150",
"name": "Zipak",
"email": "leilaware#zipak.com",
"city": "Rabat",
"location": {
"type": "Point",
"coordinates": [
-6.74736,
33.81514
]
}
},
{
"shopId": "5a0c6b55fd3eb67969316d9d",
"picture": "http://placehold.it/150x150",
"name": "Genekom",
"email": "leilaware#genekom.com",
"city": "Rabat",
"location": {
"type": "Point",
"coordinates": [
-6.74695,
33.81594
]
}
},
...
When a certain action (end-user hits a "Like" button) is triggered, I need to add an attribute to the related Shop object, so it'd become like this:
{
"shopId": "5a0c67e9fd3eb67969316cff",
"picture": "http://placehold.it/150x150",
"name": "Zipak",
"email": "leilaware#zipak.com",
"city": "Rabat",
"liked": true,
"location": {
"type": "Point",
"coordinates": [
-6.74736,
33.81514
]
}
},
Now, I manage to add the liked attribute to the individual Shop object, but when I want to change/amend the state (group of all Shop objects), I get the object duplicated (the new one with the liked attribute and the old one). To avoid this mess, I want to handle these operations using Immutable.js to make sure everything is clean and proper.
Where do I start from? Do I convert the state to an Immutable.js Map? List?
Rather than using Immutable.js you can use immutability-helper which has an update function returns the updated value without mutating the original value.
immutability-helper has a syntax inspired from MongoDB so getting used to it shouldn't be very hard for you.
For your case, you can modify your state with the below sample,
import update from 'immutability-helper';
const id = '5a0c67e9fd3eb67969316cff'; // id of the item you need to update
const newState = update(state, {
shops: {
$apply: (item) => {
if (item.shopId !== id) return item
return {
...item,
liked: true
}
}
}
});
Simply reduce your array of identified shops to an object on fetch success. It's a good practice to do so for any list of unique items to avoid the O(n) complexity of find functions and to mutate your state in a simpler way.
Reduce shops on fetch success in your shops reducer :
shops.reduce((obj, item) => {
obj[item.id] = item
return obj;
}, {})
It will then be easy to manipulate your object inside your shops reducer on any action such as a like success :
return {
...state,
[action.shopId]: {
...state[action.shopId],
liked: true,
},
}
Related
Let's say you are given an array of objects in your React state, like this:
[
{
"id": "…",
"title": "Brief history of Space Shuttle program",
"date": "2016-10-29 19:00:00+01:00",
"venue": "NASA History Museum"
},
{
"id": "…",
"title": "Why did the Challenger explode?",
"date": "2016-11-31 18:30:00+01:00",
"venue": "Albert II Library Building"
}
]
Now you want to sort this by date, let's say you have a forum or something similar, where the first post should be the newest according to date.
How would you sort it? This was my attempt:
function sortExample (dataToSort) {
let test = [...dataToSort];
test.sort((a,b) => new Date(a.date) - new Date(b.date)
}
Unfortunately, that didn't work out quite well. And most importantly, where would you place this function? I put it in my useEffect, that didn't seem to work for me either.
You have two options:
Sort it before you setState, this is recommended if you only need the data in this order everywhere.
Sort it where you use the data but keep the original data as is in the state.
Say we have:
const [dataInState, setDataInState] = useState([]);
First one:
useEffect(() => {
fetch("/data")
.then(res => res.json())
.then(json => setDataInState(json.sort(myCompareFunc))
}, [])
Second one:
Set state as usual, then in component body, before return:
const sortedData = [...dataInState].sort(myCompareFunc)
Now you can use sortedData in JSX or elsewhere.
I recommend the first one due to performance reasons.
I'm building a little web-app to practice and learn Vue.js and working with APIs.
For a particular problem I want to solve, I would like to return the object that has the matching uuid that I request.
With my current knowledge, I understand I can do this by implementing some sorts and loops logic.
However I'm still new with JS, Vue.js, so I'm not sure if there is a better way to approach this.
Is there a built in function, or some form of "best practice" to approach this?
methods: {
fetchItem(row) {
// row.target_uuid -- this is the UUID I want
// this.$props.todoItems; -- this contains the json objects
// return this.$props.todoItems[i] where this.$props.todoItems[i]['uuid'] == row.target_uuid
},
This is a snippet of my $props.todoItems for context
[
{
"title": "Install Maris",
"uuid": "9ec9ea6b-0efc-4f6a-be2e-143be5748d3a",
"field_completed": "False"
},
{
"title": "Figure out why VS Code sucks",
"uuid": "85120da5-ee59-4947-a40f-648699365c73",
"field_completed": "False"
},
{
"title": "Start designing portfolio",
"uuid": "243c1960-7ade-4a68-9a74-0ccc4afa3e36",
"field_completed": "False"
},
{
"title": "Meal Prep",
"uuid": "85b64b18-9110-44d8-bd2d-8f818b0a810f",
"field_completed": "False"
},
{
"title": "Sharpen knives",
"uuid": "8a7ac5f6-8180-4f20-b886-628fd3bcfc85",
"field_completed": "False"
},
{
"title": "Set up SSH keys",
"uuid": "f879c441-8c05-4f24-9226-125c62576297",
"field_completed": "False"
}
]
If you know you're looking for exactly one item (or the first item that matches) you should take a closer look at the Array.find() method provided by JS. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find)
Also take a look at all the other methods the Array prototype provides, most of them are fairly descriptive and solve most of the basic problems you'll encounter.
To use this in your Vue app you can either have a method that returns your todo based on a provided uid like this
todoByUid(uidToFind) {
return this.todos.find(todo => todo.uid == uidToFind)
}
If you only care about a currently selected item a computed value as Jacob mentioned is the way to go:
computed() {
selectedTodo() {
return this.todos.find(todo => todo.uid == this.selectedUid)
}
}
I want to add a new object for each nested array. I'm calling this function any time I add a product to my orderintake:
add2order(productID, productName, productRatePlans) {
this.orderIntake.push({ productID, productName, productRatePlans });
let i = 0;
this.orderIntake[0].productRatePlans[0].productRatePlanCharges.forEach(element => {
i++;
this.orderIntake[0].productRatePlans[0].productRatePlanCharges[
i
].quantity = this.orderIntake[0].productRatePlans[0].productRatePlanCharges[
i
].defaultQuantity;
});
}
this is an example response from the server:
{
"id": "8adc8f996928b9a4016929c59b943a8f",
"sku": "SKU-00006778",
"Partner_Account_ID__c": null,
"productRatePlans": [
{
"id": "8adce4216928c28d016929c59bff3372",
"status": "Active",
"name": "Enterprise",
"description": null,
"effectiveStartDate": "2016-02-26",
"effectiveEndDate": "2029-02-26",
"productRatePlanCharges": [
{
"id": "8adc8f996928b9a4016929c59d183a92",
"name": "USAGE_COUNTER_2",
"type": "Usage",
"model": "Volume",
"uom": "Each",
"pricingSummary": [
"Up to 5000 Each: USD0 flat fee"
],
"pricing": [
{
...
}
],
"defaultQuantity": null,
"applyDiscountTo": null,
"discountLevel": null,
"discountClass": null,
...
"financeInformation": {
..,
}
}
]
}
],
"productFeatures": [
{
...
}
]
}
The data is being retrived this way from an external REST backend so unfortunately I can't initialize the data including the new property...
so in every productRatePlanCharges there should be 1 new object 'quantity'.
How can I add this field to every productRatePlanCharges?
Right now I'm getting: ERROR
TypeError: Cannot read property 'productRatePlanCharges' of undefined
And how can I make sure I'm always adding this to the last orderIntake element? Don't mind productRatePlans there is only 1 in each orderintake...
thanks for your support!
Here you have to create productDetails object with inititalised array like below so that you won't get the error.
add2order(productID, productName, productRatePlans) {
// Create object like below
let productDetails = { productID : productID, productName : productName, productRatePlans : productRatePlans
}
this.orderIntake.push(productDetails);
let i = 0;
this.orderIntake[0].productRatePlans[0].productRatePlanCharges.forEach(element => {
i++;
this.orderIntake[0].productRatePlans[0].productRatePlanCharges[
i
].quantity = this.orderIntake[0].productRatePlans[0].productRatePlanCharges[
i
].defaultQuantity;
});
}
Hope this will help!
as you used Angular you probably use Typescript too. I recommend that you create a model like your incoming model and there define your quantity: number inside productRatePlanCharges object. then map the incoming data to your own model. therefore you will have a quantity=0 in your model that you can change it later in a loop.
If you want to continue with your own way take a look at this:
Add new attribute (element) to JSON object using JavaScript
there is no problem to add an element to current model almost like you did, and the problem might be somewhere else as your error refers to existence of productRatePlanCharges!
as you used forEach I prefer to use that 'element' and double iterating with i++; is not a good idea to me.
this might be better:
element.quantity = element.defaultQuantity;
I am trying to get filter Firebase using multiple fields. This is more or less my object in Firebase:
{
"id": "-id",
"category": "History",
"level": "High School",
"pointAmount": 128,
"pointBoost": 0,
"photoURL": "link"
},
{
"id": "-id",
"category": "Physics",
"level": "Primary School",
"pointAmount": 128,
"pointBoost": 0,
"photoURL": "link"
}
What I'm doing now, is using an array of checkboxes in React to grab the level and category to filter by. This part is done. My question is, how can I filter the elements coming in from the database? This is how I'm doing it right now:
componentDidMount() {
const assignmentsRef = firebase
.database()
.ref('Works')
.orderByChild('available')
.equalTo(true)
.limitToFirst(9);
assignmentsRef.on('value', snapshot => {
let assignments = snapshot.val();
let newState = [];
for (let assignment in assignments) {
newState.push({
id: assignment,
category: assignments[assignment].category,
level: assignments[assignment].level,
pointAmount: assignments[assignment].pointAmount,
pointBoost: assignments[assignment].pointBoost,
photoURL: assignments[assignment].photoURL,
workText: assignments[assignment].workText,
});
}
this.setState({
assignments: newState
});
});
}
So as you can see, I'm already doing orderByChild. Also there will be multiple variables which to filter by. For example: If I select History, and Physics I will get both objects. Same if I select History and Primary School, but if I select Physics I should only get the second object. How can I filter it? There will be over 10 filters.
It looks like you're trying to do an OR of both conditions. There isn't any built-in support for returning items that match one of a number of conditions. You will have to fire a separate query for each condition, and then merge the results from all queries client-side. This is not as slow as you may expect, since Firebase will pipeline the queries over a single connection.
I am currently starting with Redux and it is unclear to me what is the proper way of binding reducer to sub, dynamically set, parts of the state.
For instance let's say my state looks like this (after asynchronously fetching some data from backend APIs)
{
"categories": {
"collection": {
"42": {
"id": "42",
"title": "whatever",
"owner_id": "10"
"posts": {
"collection": {
"36": {
"id": "36",
"title": "hello",
"content": "hello world"
},
"37": { // more posts ... }
},
"ids": ["36", "37", ...]
},
"otherChildren": { // more sub-entities }
},
"43": { // more categories ... }
},
"ids": ["42", "43", ...]
},
"users": {
"collection": {
"10": {
"id": "10"
"email": "what#ever.com"
},
"11": { // more users ... }
},
"ids": [10, 11]
}
}
My root reducer would look like this :
export default combineReducers({
categories: categoriesReducer,
users: usersReducer
})
and the categoriesReducer :
function categoriesReducer(state = initialState, action) {
switch (action.type) {
case FETCH_ALL_CATEGORIES_SUCCESS:
return Object.assign({}, state, {
categories: {
collection: action.payload
}
})
default:
return state
}
}
Now what I'd like to do is to seamlessly delegate handle the post subset part of the state with postsReducer function, basically adding a case like :
case FETCH_CATEGORY_ALL_POSTS_SUCCESS:
let categoryId = action.categoryId
return Object.assign({}, state, {
categories: {
[categoryId]: combineReducers({
"posts": postsReducer,
"otherChildren": otherChildrenReducer
})
}
}
Of course, this isn't working. What I don't get is how to get redux to automatically update a subset of the state using combineReducer for nested reducers, while automatically passing the proper subset as state argument to the reducer function, and without overriding the existing data (i.e. the category in my example).
I somehow managed to make that work writing my own "delegate" fonction, but it feels pretty wrong - especially looking at https://github.com/reactjs/redux/blob/master/src/combineReducers.js which looks like doing exactly that.
How am I, conventionally, suppose to do that? Is that even possible to use combineReducers that way with Redux, am I misunderstanding the point of combineReducer or am I expecting to much magic from it ?
Thanks !
EDIT/UPDATE:
I do really need those to be nested (right, maybe the category/post example isn't the right one) and I'd like the reducer (i.e. here, postsReducer, but it could be a Collection reducer) to re-usable in multiple places.
(Why do I want it to be nested ? Actually in that example, let's say that one post can only belong to one category, since data of post are actually encrypted with a private key from category. That why it makes so much sens to me to represent this chain, this relation in the state)
Isn't there a way with redux to delegate to other reducer while passing the proper subset of the state - that is, for instance, passing state categories.collection.42.posts.collection.36 to a postReducer ?
You where doing it so nice with the store, why not keep on separating all the diferent collections.
I mean your posts should/could have another reducer, the store would en up like this:
{
categories: [],
posts: [],
users: []
}
Your categories would then contain only the ID's of the posts
So you could "capture" the "FETCH_ALL_CATEGORIES_SUCCESS" action in both reducers (categories and the new posts reducer), on categoryReducer you safe the category data, and the ID's, and the postsReducer will just save the posts
You are using combineReducers in the wrong way in the last code snippet. (It is correct in the first snippet where you're using it to combine categoriesReducer and usersReducer together.)
In the case of FETCH_CATEGORY_ALL_POSTS_SUCCESS, instead of calling combineReducers, just call a plain function inside of categoriesReducer.
posts: posts(action.payload.posts)
That plain function is basically a sub-reducer. It would be sth you write yourself, and you'd probably put it in the same file.
But two other main issues with your code above:
1) As another user has already said above, it probably wouldn't be best to store posts as a sub-property of categories. Rather store the posts as their own item within the state tree, and just have a category_id field for each post to show which category it belongs to. So your state tree in that case would look like:
{
categories: {},
posts: {},
users: {}
}
and you would use combineReducers initially as:
export default combineReducers({
categories: categoriesReducer,
posts: postsReducer,
users: usersReducer
})
2) Within the categoriesReducer (or any for that matter), you don't have to have the categories property, because you've already created that property when calling combineReducers. In other words in categoriesReducer you ought to just have it as this:
function categoriesReducer(state = initialState, action) {
switch (action.type) {
case FETCH_ALL_CATEGORIES_SUCCESS:
return Object.assign({}, state, {
collection: action.payload,
ids: // function to extract ids goes here
});
default:
return state;
}
}