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!
Related
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!
I am currently trying to loop through an array of objects (each object is a task), where each task contains relevant information such as a name and date. Information from each task is then utilized in creating an object containing arrays, where each array contains objects that correspond to the date, or the array.
My current code is as follows:
contextTasks.forEach((taskItem) => {
taskItem["taskSchedule"].forEach((dateItem) => {
setItems((items) => ({
...items,
[dateItem["date"]]: [
{
name: taskItem["taskName"],
time: new Date(dateItem["time"]).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
type: "Task",
},
],
}));
});
});
However, if there are multiple tasks with the same date, they will override each other and I only end up with one task per date. How would I go about pushing further objects to the array if there are other entries for that specific date?
Finished object:
Object {
"2021-04-21": Array [
Object {
"name": "Test Class v1",
"type": "Class",
},
],
"2021-04-24": Array [
Object {
"name": "Test Task v2",
"type": "Task",
},
//I would like to add another object here without overriding existing contents of the array
],
}
Have you tried using reduce ?
the idea will be to have something like this inside your accumulator:
{"date1": [{val}, {val}, ...] , "date2": [{val}, {val}, ...]}
array.reduce((acc, val) => {
// test if your accumulator has the same date as your date from the val
if(acc[val.date]) {
acc[val.date] = [... acc[val.date], ...your val]
} else {
// no date found in the accumulator so make acc.date = ...acc.date, val
acc[val.date] = [ val ]
}
}, {})
Sorry if the code is not perfect but if you want provide your initial array of data and I will fix the response code
The cause of your issue is the fact you're executing an async method inside a synchronous loop. Also, modifying state forces a re-render, and you're attempting to do it presumably many times at once. It might, and will cause a bottleneck at some point.
The solution: build your new state first, and execute a setState once.
I'm stuck since a while trying to access a nested array inside another array after filtering it by an id. To be clear, this is the mock data I have:
bundleSets: [
{
id: 1,
title: "bundle set 1",
bundles: [
{
bundleTitle: "bundle 1",
content:[]
},
{
bundleTitle: "bundle 2",
content:[]
}
]
},
{ id:2,
title: "bundle set 2",
bundles: [
{bundleTitle: "ciaopao", content:[]}
]
},
{ id:3,
title: "bundle set 3",
bundles: [
{bundleTitle: "ciapo", content:[]}
]
}
]
Now I need to filter each bundleSets by id, and then access the bundles array inside and mapping those elements. This is what I tried to do:
const [key,setKey]=useState(1)
const [bundles,setBundles]=useState([])
useEffect(()=>{
const filteredBundles = bundleSets && bundleSets.filter(bundleSet=>bundleSet.id==key).map(filteredBundles=>{
return filteredBundles.bundles
})
setBundles(filteredBundles)
},[key])
Now If I try mapping the new state I can see on the console.log a weird [Array(1)] instead of the usual [{}] and I can't render it to the page. Where am I getting wrong with this?
Array.prototype.map returns an array and the callback you're passing to the map method returns filteredBundles.bundles which is an array. So, you get an array of arrays. filteredBundles is a confusing name, btw.
Since you're looking up the bundle by id and the ids are unique in the bundleSets array, you can use Array.prototype.find to find the bundle set by id and then get the bundle array. You can return an empty array if find returns undefined (if the key is not found).
const bundles = bundleSets?.find(set => set.id === key)?.bundles || []
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.
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