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

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.

Related

More than one getItem localStorage in a state

Is it possible to have more than one localStorage.getItem in state?
Right now I have this:
const [list, useList] = useState(
JSON.parse(localStorage.getItem("dictionary")) || [] //tasks in my to-do
);
and I should also keep in this state my subtasks, contained in a task, with this structure:
- task {
- id
- body
- subtasks
[{
- id
- body
}]
}
Can I save also the subtasks in local storage and access them with getItem?
These are what I want to use to get my subtasks:
JSON.parse(localStorage.getItem("domain")) || []
JSON.parse(localStorage.getItem("range")) || []
Yes, you can have more than one array of values in local storage. You need to set the item before you can access it though, you should also serialize the object or array to a string when saving it.
localStorage.setItem("dictionary", JSON.stringify([]));
localStorage.setItem("domain", JSON.stringify([]));
localStorage.setItem("range", JSON.stringify([]));
alert(JSON.parse(localStorage.getItem("dictionary")));
alert(JSON.parse(localStorage.getItem("domain")));
alert(JSON.parse(localStorage.getItem("range")));
Lucky me, I saw your other question which contains a running code snippet, you should add it here too!
From what I saw you're trying to create a tree of tasks, dictionary is a task and it can have subtasks such as domain and range, right? Then you should have a data structure like this:
singleTask = {
id: 0,
body: "task",
domain: [
{
id: 00,
body: "subtask domain 1"
},
{
id: 01,
body: "subtask domain 2"
}
],
range: [
{
id: 10,
body: "subtask range 1"
},
{
id: 11,
body: "subtask range 2"
}
]
}
When you're rendering a task as TaskListItem, you render the task.body. Then pass task.domain to a SubtaskDomain component, task.range to a SubtaskRange component.
When you submit a subtask, update the main list in App, after you do that, update local storage, you already do that, but you actually only need one set item, and it's
localStorage.setItem("dictionary", JSON.stringify(listState));
because you have everything in it!

Tell apollo-client what gets returned from X query with Y argiments?

I have a list of Items of whatever type. I can query all of them with query items or one with query item(id).
I realize apollo can't know what will be returned. It knows the type, but it doesn't know the exact data. Maybe there is a way not to make additional request? Map one query onto another?
Pseudo-code:
// somewhere in Menu.tsx (renders first)
let items = useQuery(GET_ITEMS);
return items.map(item => <MenuItemRepresenation item={item} />);
// meanwhile in apollo cache (de-normalized for readability):
{ ROOT_QUERY: {
items: [ // query name per schema
{ id: 1, data: {...}, __typename: "Item" },
{ id: 2, data: {...}, __typename: "Item" },
{ id: 3, data: {...}, __typename: "Item" },
]
}
}
// somewhere in MainView.tsx (renders afterwards)
let neededId = getNeededId(); // 2
let item = useQuery(GET_ITEM, { variables: { id: neededId } } );
return <MainViewRepresentation item={item} />;
Code like this will do two fetches. Even though the data is already in the cache. But it seems apollo thinks on query level. I would like a way to explain to it: "If I make item query, you need to look over here at items query you did before. If it has no item with that id go ahead and make the request."
Something akin to this can be done by querying items in MainView.tsx and combing through the results. It might work for pseudo-code, but in a real app it's not that simple: cache might be empty in some cases. Or not sufficient to satisfy required fields. Which means we have to load all items when we need just one.
Upon further research Apollo Link looks promising. It might be possible to intercept outgoing queries. Will investigate tomorrow.
Never mind apollo link. What I was looking for is called cacheRedirects.
It's an option for ApolloClient or Cache constructor.
cacheRedirects: {
Query: {
node: (_, args, { getCacheKey }) => {
const cacheKey = getCacheKey({
__typename: "Item",
id: args.id,
});
return cacheKey;
},
},
},
I'd link to documentation but it's never stable. I've seen too many dead links from questions such as this.

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

What is the Flux Standard Action convention for structuring a payload?

I want to use the Flux Standard Action standard to write the actions for my Redux app, and I'm unsure how the payload itself should be structured. The example given on the Flux Standard Action github repo is:
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
Now what if I'm passing multiple pieces of information in my payload? In a simple todo app for example, say my payload passes a todo object (rather than just the todo's text in the above). I'm unsure whether it should be structured like this:
{
type: 'ADD_TODO',
payload: {
title: 'Do something.',
priority: 'HIGH',
completed: false
}
}
Or whether a todo object should be nested inside the payload, like this:
{
type: 'ADD_TODO',
payload: {
todo: {
title: 'Do something.',
priority: 'HIGH',
completed: false
}
}
}
It looks like the difference is whether the payload is meant to BE or to CONTAIN the data consumed by the reducers. Put another way, whether my reducers should expect a certain type of data as the payload (the payload IS a todo object), or whether they should specify what they're getting out of the payload (the payload CONTAINS a todo object).
In computing and telecommunications, the payload is the part of
transmitted data that is the actual intended message. The payload
excludes any headers or metadata sent solely to facilitate payload
delivery.
As per above quote, payload should be just the data your reducer looks for. something like,
{
type: 'ADD_TODO',
payload: {
title: 'Do something.',
priority: 'HIGH',
completed: false
}
}
Your reducer would generate the new state based on the action which you pass and our action is responsible to tell the reducer that what needs to be done.
When your reducer gets an FS Action which has type: 'ADD_TODO' , it knows that it has to add a todo and the todo is with payload property.
So it is explicit that your actions payload would be a todo. It is not necessary to tell that what is there in the payload. Because the FSA itself tells that this action contains a payload which is of type todo.

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