Firebase: Multi Location Update using Firebase Object Observable - arrays

I'm trying to work out how to do a multi-location update using the FirebaseObjectObservable.
This is what my data looks like.
recipes: {
-R1: {
name: 'Omelette',
ingredients: ['-I1']
}
}
ingredients: {
-I1: {
name: 'Eggs',
recipes: ['-R1']
},
-I2: {
name: 'Cheese',
recipes: []
}
}
I want to then update that recipe and add an extra ingredient.
const recipe = this.af.database.object(`${this.path}/${key}`);
recipe.update({
name: 'Cheesy Omelette',
ingredients: ['-I1', '-I2']
});
And to do multi-location updates accordingly:
recipes: {
-R1: {
name: 'Cheesy Omelette',
ingredients: ['-I1', '-I2'] // UPDATED
}
}
ingredients: {
-I1: {
name: 'Eggs',
recipes: ['-R1']
},
-I2: {
name: 'Cheese',
recipes: ['-R1'] // UPDATED
}
}
Is this possible in Firebase? And what about the scenario where an update causes 1000 writes.

Storing your ingredients in an array makes it pretty hard to add an ingredient. This is because arrays are index-based: in order to add an item to an array, you must know how many items are already in that array.
Since that number requires a read from the database, the code becomes pretty tricky. The most optimal code I can think of is:
recipe.child("ingredients").orderByKey().limitToLast(1).once("child_added", function(snapshot) {
var updates = {};
updates[parseNum(snapshot.key)+1] = "-I2";
recipe.child("ingredients").update(updates);
});
And while this is plenty tricky to read, it's still not very good. If multiple users are trying to change the ingredients of a recipe at almost the same time, this code will fail. So you really should be using a transaction, which reads more data and hurts scalability of your app.
This is one of the reasons why Firebase has always recommended against using arrays.
A better structure to store the ingredients for a recipe is with a set. With such a structure your recipes would look like this:
recipes: {
-R1: {
name: 'Omelette',
ingredients: {
"-I1": true
}
}
}
And you can easily add a new ingredient to the recipe with:
recipe.update({ "ingredients/-I2": true });

Related

Instead of replacing the object it adds a new one in a nested array with React

Sup,
So I have this object:
data: {
OtherFields: {},
Skills: [
{
id: Math.random(),
name: 'Default Category',
skills: [],
},
],
{
So the Skills Array is very dynamic, I need to add categories, and each categories have their own array named skills, that will be filled with other objects, and the default category is there.
While the skills inside will have:
{
id: Math.random(),
skillName: 'Default Category',
}
What I want to do is add the skill to the specific category in a dynamic way with the id category as we don't know how much the user will add.
Here what I did until now:
const handleAdd = (id, content) => {
// id is the cateogry of that specific cateogry that im receiving from input
// content is the value of the input
// this is the object i need to push into the category
const newItem = {
id: Math.random(),
skillName: content,
};
// and then update it,
const newData = data.Skills.find((i) => i.id === id);
console.log(newData)
newData.skills.push(newItem);
setData({ ...data, Skills: [...data.Skills, newData] });
//this it works but adds another cateogry and doesnt not replace the current one with the new value that is added
};
This appends newData to the array:
Skills: [...data.Skills, newData]
But it doesn't filter that same record from the array when appending it. It basically means "the whole array as-is, plus this new element". Even if that element is conceptually a duplicate, the code doesn't know that. (Heck, even if it's by reference a duplicate, there's still nothing stopping an array from containing two references to the same object.)
It sounds like you want to filter that whole array to remove that element before re-appending it. For example:
Skills: [...data.Skills.filter(s => s.id !== newData.id), newData]
Since you're modifying the original object this should work, rename your variables to make it easier to read.
Also consider not changing the original object.
setData({ ...data, Skills: [...data.Skills] });

How do I select and update an object from a larger group of objects in Recoil?

My situation is the following:
I have an array of game objects stored as an atom, each game in the array is of the same type and structure.
I have another atom which allows me to store the id of a game in the array that has been "targeted".
I have a selector which I can use to get the targeted game object by searching the array for a match between the game ids and the targeted game id I have stored.
Elsewhere in the application the game is rendered as a DOM element and calculations are made which I want to use to update the data in the game object in the global state.
It's this last step that's throwing me off. Should my selector be writable so I can update the game object? How do I do this?
This is a rough outline of the code I have:
export const gamesAtom = atom<GameData[]>({
key: 'games',
default: [
{
id: 1,
name: 'Bingo',
difficulty: 'easy',
},
{
id: 21,
name: 'Yahtzee',
difficulty: 'moderate',
},
{
id: 3,
name: 'Twister',
difficulty: 'hard',
},
],
});
export const targetGameIdAtom = atom<number | null>({
key: 'targetGameId',
default: null,
});
export const targetGameSelector = selector<GameData | undefined>({
key: 'targetGame',
get: ({ get }) => {
return get(gamesAtom).find(
(game: GameData) => game.id === get(selectedGameIdAtom)
);
},
// This is where I'm getting tripped up. Is this the place to do this? What would I write in here?
set: ({ set, get }, newValue) => {},
});
// Elsewhere in the application the data for the targetGame is pulled down and new values are provided for it. For example, perhaps I want to change the difficulty of Twister to "extreme" by sending up the newValue of {...targetGame, difficulty: 'extreme'}
Any help or being pointed in the right direction will be appreciated. Thanks!

React Redux Store Layout: How to handle "pending" state of "add item" request?

Example store:
{
todos: {
byId: {
"1": { id: "1", title: "foo" },
"2": { id: "2", title: "bar" }
},
allIds: ["2", "1"] // ordered by `title` property
}
}
Now the user wants to add a new Todo Entry:
dispatch({
type: 'ADD_TODO_REQUEST',
payload: { title: "baz" }
})
This triggers some API request: POST /todos. The state of the request is pending as long as there's no response (success or error). This also means, that I have no id yet for the newly created Todo Entry.
Now I already want to add it to the store (and display it). But of course I can't add it to byId and allIds, because it has no id yet.
Question 1: How should I change the layout of my store to make this possible?
After the response arrives, there are two possibilities:
success: Update the store and set the id property of the new Todo Entry. Using dispatch({type:'ADD_TODO_SUCCESS', payload: response.id}).
error: Remove the new Todo Entry from the store. Using dispatch({type:'ADD_TODO_ERROR', payload: ???})
Now the reducer for those two actions has to somehow find the corresponding element in the store. But it has no identifier.
Question 2: How do I find the item in the store if it has no id?
Additional information:
I'm using react with redux-saga
It should be possible to have multiple concurrent ADD_TODO_REQUEST running at the same time. Though it must be possible to have multiple pending Todo Entries within the store. (For example if the network connection is really slow and the user just enters "title1" and hits the "add" button, then "title2" and "add", "title3" and "add".) Though it's not possible to disable the AddTodo component while a request is pending.
How do you solve these kind of problems within your applications?
EDIT: There's even more:
The same functionality should be available for "updating" and "deleting" Todo Entries:
When the user edits a Todo Entry and then hits the "save" button, the item should be in the pending state, too, until the response arrives. If it's an error, the old version of the data must be put back into the store (without requesting it from the server).
When the user clicks "delete", then the item will disappear immediately. But if the server response is an error, then the item should be put back into the list.
Both actions should restore the previous data, if there's an error respsonse.
I found a simple solution. But I'm sure that there are other possibilities and even better solutions.
Keep the Todo Entries in 2 separate collections:
{
todos: {
byId: {
"1": { id: "1", title: "foo" },
"2": { id: "2", title: "bar" }
},
allIds: ["2", "1"],
pendingItems: [
{ title: "baz" },
{ title: "42" }
]
}
}
Now I can find them in the store "by reference".
// handle 'ADD_TODO_REQUEST':
const newTodoEntry = { title: action.payload.title };
yield put({ type: 'ADD_TODO_PENDING', payload: newTodoEntry });
try {
const response = yield api.addTodoEntry(newTodoEntry);
yield put({ type: 'ADD_TODO_SUCCESS', payload: { id: response.id, ref: newTodoEntry } });
} catch(error) {
yield put({ type: 'ADD_TODO_ERROR', payload: newTodoEntry });
}
The reducer will look like this:
case 'ADD_TODO_PENDING':
return {
..state,
pendingItems: // add action.payload to this array
}
case 'ADD_TODO_SUCCESS':
const newTodoEntry = { ...action.payload.ref, id: action.payload.id };
return {
..state,
byId: // add newTodoEntry
allByIds: // add newTodoEntry.id
pendingItems: // remove action.payload.ref from this array
}
case 'ADD_TODO_ERROR':
return {
..state,
pendingItems: // remove action.payload.ref from this array
}
There are 2 problems:
The reducer must use the object reference. The reducer is not allowed to create an own object from the action payload of ADD_TODO_PENDING.
The Todo Entries cannot be sorted easily within the store, because there are two distinct collections.
There are 2 workarounds:
Use client side generated uuids which only exist while the items are within the pending state. This way, the client can easily keep track of everything.
2.
a) Add some kind of insertAtIndex property to the pending items. Then the React component code can merge those two collections and display the mixed data with a custom order.
b) Just keep the items separate. For example the list of pending items on top and below that the list of already persisted items from the server database.

Redux updating nested immutable data

I have an issue with updating the immutable redux and quite nested data. Here's an example of my data structure and what I want to change. If anyone could show me the pattern of accessing this update using ES6 and spread operator I would be thankful.
My whole state is an object with projects (key/value pairs - here as an example only one project) that are objects with its own key (and the keys are ids as well), arrays of procedures and inside the tasks:
{ 1503658959473:
{ projectName: "Golden Gate",
projectLocation": "San Francisco",
start:"22/09/1937",
id:1503658959473,
procedures:[
{ title: "Procedure No. 1",
tasks:[
{name: "task1", isDone: false},
{name: "task2", isDone: false},
{name: "task3", isDone: false}
]
}
]
}
}
What I'm willing to do is to update one single task 'isDone' property to 'true'. It's some kind of toggling the tasks. How can I return this state with that information updated?
The action creator pass this information to reducer:
export function toggleTask(activeProject, task, taskIndex) {
return {
type: TOGGLE_TASK,
payload: {
activeProject,
task,
taskIndex
}
};
}
You've run into a common issue with Redux. The docs recommend that you flatten your data structure to make it easier to work with, but if that's not what you want to do, I'd refer to this part of their docs.
Because both Object.assign() and the ...spread operator create shallow copies, you must go through each level of nest in your object and re-copy it.
Your code might look something like this...
function updateVeryNestedField(state, action) {
return {
...state,
procedures : {
...state.procedures,
tasks : {
return tasks.map((task, index) => {
if (index !== action.taskIndex) {
return task
}
return {
...task,
task.isDone: !task.isDone
}
}
}
}
}
}
I myself would create a new class called ProjectModel, which has a public method toggleTask that is able to update its task's status. The reducer state would be an object whose keys are project IDs and values are ProjectModel instances.

How can I get an item in the redux store by a key?

Suppose I have a reducer defined which returns an array of objects which contain keys like an id or something. What is the a redux way of getting /finding a certain object with a certain id in the array. The array itself can contain several arrays:
{ items:[id:1,...],cases:{...}}
What is the redux way to go to find a record/ node by id?
The perfect redux way to store such a data would be to store them byId and allIds in an object in reducer.
In your case it would be:
{
items: {
byId : {
item1: {
id : 'item1',
details: {}
},
item2: {
id : 'item2',
details: {}
}
},
allIds: [ 'item1', 'item2' ],
},
cases: {
byId : {
case1: {
id : 'case1',
details: {}
},
case2: {
id : 'case2',
details: {}
}
},
allIds: [ 'case1', 'case2' ],
},
}
Ref: http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html
This helps in keeping state normalized for both maintaining as well as using data.
This way makes it easier for iterating through all the array and render it or if we need to get any object just by it's id, then it'll be an O(1) operation, instead of iterating every time in complete array.
I'd use a library like lodash:
var fred = _.find(users, function(user) { return user.id === 1001; });
fiddle
It might be worth noting that it is seen as good practice to 'prefer objects over arrays' in the store (especially for large state trees); in this case you'd store your items in an object with (say) id as the key:
{
'1000': { name: 'apple', price: 10 },
'1001': { name: 'banana', price: 40 },
'1002': { name: 'pear', price: 50 },
}
This makes selection easier, however you have to arrange the shape of the state when loading.
there is no special way of doing this with redux. This is a plain JS task. I suppose you use react as well:
function mapStoreToProps(store) {
function findMyInterestingThingy(result, key) {
// assign anything you want to result
return result;
}
return {
myInterestingThingy: Object.keys(store).reduce(findMyInterestingThingy, {})
// you dont really need to use reduce. you can have any logic you want
};
}
export default connect(mapStoreToProps)(MyComponent)
regards

Resources