Redux updating nested immutable data - reactjs

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.

Related

Derive locally cached Apollo client state based off query/cached object updates

I have a query that retrieves a Model. Inside this model, there are nested models with fields.
The shape is roughly like this:
{
model: [
{
id: 1,
fields: [...]
},
{
id: 2,
fields: [...]
}
]
}
Additionally, the frontend needs the model normalized into a list, like this:
{
modelFields: [
{...},
{...},
{...},
{...}
]
}
I’m attempting to derive modelFields declaratively when a query or cache update changes model. I’m trying to achieve this in type-policies section on Model: { merge: modelMergeMiddleware }, like so:
export function modelMergeMiddleware(
__: ModelFragment,
incoming: ModelFragment,
{cache, readField}: FieldFunctionOptions
) {
if (incoming) {
cache.writeQuery({
query: ModelFieldsDocument,
data: {
modelFields: incoming.fieldsets.reduce(
(fields: ModelFieldFragment[], fieldset: FieldsetFragment) => {
return fields.concat(newFields)
},
[]
)
}
})
}
return incoming
}
However, this runs into problems:
nested cache references don’t get passed through leaving empty data
readField and lodash’s _.cloneDeep both result in Readonly data that cause errors
My question is two-fold:
Is there a method to work around the problems mentioned above to derive data in a merge function?
Is there a different approach where I can declaratively derive local-only state and keep it synchronized with cached objects?
Per question 2, my backup approach is to use a reactiveVar/Recoil to store this data. This approach has the tradeoff of needing to call a setter function in all the places the Model object gets queried or mutated in the app. Not the end of the world, but it’s easy to miss a spot or forget about the setter function.

How to delete a specific data/object in an object? (ReactJs Reducer)

If my context is like this
const [data, dispatch] = useReducer(dataReducer, {
projects:{},
tasks:{},
});
and the data was
{
projects:{
'first':{
name:'first',
taskIds:['task-1'],
},
'second':{
name:'second',
taskIds:['task-2','task-3'],
},
},
tasks:{
'task-1':{
title:'Eat',
time:'1:30pm',
},
'task-2':{
title:'Work',
time:'2:30pm',
},
'task-3':{
title:'Sleep',
time:'9:00pm',
},
},
}
How can i delete 'task-2' inside the projects.second and tasks objects by using a reducer?
In conclusion, you will need to replicate the object without 'task-2' from projects and tasks and pass it as a new state because objects inside an object requires a special action called 'deep copy'. Therefore, I suggest that you should separate projects and tasks since reducer requires a whole object to update the current state to new state.
Then, you can pass each reducer an object you want to pass as such:
tasks:
// tasks
{
tasks:{
'task-1':{
title:'Eat',
time:'1:30pm',
},
'task-3':{
title:'Sleep',
time:'9:00pm',
},
},
}
projects:
// projects
{
projects:{
'first':{
name:'first',
taskIds:['task-1'],
},
'second':{
name:'second',
taskIds:['task-3'],
},
}
let projects = action.projects
let tasks = action.tasks
delete tasks['task-2']

What is recommended way to determine if a Redux state empty array is the initial state or the result of an API call that returns an empty array?

Let say I have an initial state tree that looks like this:
{
users: [],
items: []
}
In some cases, the result of calling the items API endpoint may result in a state tree like this:
{
users: [],
items: [
{itemId: 100, itemName: "Something100"},
{itemId: 101, itemName: "Something101"}
]
}
In other cases where there are not items to display, the state tree after an API call will be identical to the initial state tree.
Now in my component I'm using useEffect, something like this:
useEffect(() => {
if (items.length === 0) {
actions.loadItems().catch((error) => {
alert("Loading items failed! " + error);
console.log(error);
});
}
}, [items , actions]);
In this particular case, the length of items will be 0 in two cases: initial state or in case there are no results. If the API returns zero items and items.length === 0, then the action to call the API is executed repeatedly.
We really need a way of knowing that the empty array is the initial state or not. Of course I could change the state tree to something like:
{
users: {isLoaded: false, records: []},
items: {isLoaded: false, records: []},
}
That is going to add a bunch of overhead and refactoring and may not be most efficient/effective, so can someone give me a recommendation?
Unfortunately you will need some way of tracking the initialisation. If the issue is having to refactor then you can pull out this initialisation state into a higher order in the object from what you suggested. This will avoid refactoring so much:
{
isUsersLoaded: false,
isItemsLoaded: false,
users: [],
items: []
}
Another alternative is to init like this and check if users !== null etc.:
{
users: null,
items: null
}

Firebase: Multi Location Update using Firebase Object Observable

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 });

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