What is the recommended way to handle this sort of store in redux? I can see a couple different options but am looking to evaluate the tradeoffs of each and see what is the more idiomatic approach.
These are the base models, think of a blog:
Post
Author
Comment
Let's say I have these pages:
View a paginated list of posts: /posts
View one post: /posts/:id
View author profile: /authors/:id
When I go to Page 1 I will fetch a list of posts from the server, ultimately, when I get this data from the server, and the reducers handle it, the store will look like this:
{
posts: {
byId: {
'post-id-1': {
title: 'Post 1', authorId: 'author-id-1'
},
'post-id-2': {
title: 'Post 2', authorId: 'author-id-2'
},
allIds: ['post-id-1', 'post-id-2']
},
authors: {
byId: {
'author-id-1': {
name: 'Author 1'
},
'author-id-2': {
name: 'Author 2'
},
allIds: ['author-id-1', 'author-id-2']
}
}
Now I can render the post title with the author's name. Now when a user clicks on a specific post they are going to go to Page 2 (/posts/:id). When they do this, how should this be handled:
Option 1 Reset the store posts when they leave the page, so the store "posts" and "authors" will be empty, and when Page 2 componentDidMount() fires then make another network request to download the post content + the author details for only that one post. If so, now the store will look like:
// when the user navigates away from `/posts`
{
posts: { byId: {}, allIds: []}
authors: { byId: {}, allIds: []}
}
// when the user loads `/posts/:id`
{
posts: {
byId: {
'post-id-1': {
title: 'Post 1', authorId: 'author-id-1'
},
allIds: ['post-id-1']
},
authors: {
byId: {
'author-id-1': {
name: 'Author 1'
}
allIds: ['author-id-1']
}
}
Option 2 Alternatively, should I keep the data in the store that I already have and do something like this:
// when componentDidLoad() for `/posts/1`
componentDidMount () {
// check the `store` to see if we already have this post loaded
// if we do, then do nothing
// if we do not, then fetch it from the server
if (this.props.getPostForId(1)) {
} else {
this.props.fetchPost({postId: 1})
}
}
Next, if the user clicks into an author's profile page /authors/1, and I want to fetch the posts that author has written. How does this make my store look? Following Option 1, I can clear out the posts store again before the user navigates, and then on the author's page I can fetch all the posts that author wrote so I can render them on the author's page. But if I do not clear out the full list of posts, and now I fetch the posts that only this author wrote (post 3, post 4, post 5, post 6, etc.), so I keep the posts I already have in the store and merge the results?
{
posts: {
'post-id-1': {
title: 'Post 1', authorId: 'author-id-1'
},
'post-id-2': {
title: 'Post 2', authorId: 'author-id-2'
},
'post-id-3': {
title: 'Post 3', authorId: 'author-id-1'
},
'post-id-4': {
title: 'Post 4', authorId: 'author-id-1'
},
},
authors: {
byId: {
'author-id-1': {
name: 'Author 1'
},
'author-id-2': {
name: 'Author 2'
},
allIds: ['author-id-1', 'author-id-2']
}
}
My main confusion is around how data in the store is shared between pages (components).
When loading one page, how do I know if I already have the data in the store that I need, or if I need to make a request to get it?
When do I clear out old data from the store? Do I ever do this? Should every component be responsible for both dispatching an action to load the data that it needs on mount? And also dispatching an action to remove that data on un-mount?
Related
So, I have a weird problem with Redux in my React app.
I am trying to create a notes app (how original) with Notion-style editor. In this editor, lines are like contenteditables with different stylings depending on line's tag (h1, h2, p and so on). Here is how notes are stored in Redux store:
notes: {
'uiwefniu1231': {
name: "Note 1",
parentId: 'root',
content: [
{tag: 'h1', text: "Header", id: '12312'},
{tag: 'h2', text: "Header 2", id: '121321'},
{tag: 'p', text: "Paragraph 1", id: '123s2'},
{tag: 'p', text: "Paragraph 2", id: '123s1'},
{tag: 'p', text: "Paragraph 3", id: '243143'},
{tag: 'p', text: "Paragraph 4", id: '52334'},
{tag: 'p', text: "Paragraph 5", id: '423223'},
]
},
}
I update note data with updateNote action dispatcher (I use Redux Toolkit):
updateNote: (state, action) => {
const {id, newData} = action.payload;
state.notes[id] = newData;
}
So, my Main component gets note id from URL and uses it to get opened note data with useSelector. That data is passed to Editor:
const Main = (props) => {
const {noteId} = useParams();
const noteData = useSelector(selectNote(noteId));
console.log(noteData.content)
return (
<main className={styles.main}>
<Header title={noteData.name}/>
<Editor data={noteData} id={noteId}/>
</main>
)
}
Editor gets note data, and maps through note's content array, rendering input component for each item. If you press Enter inside one of those blocks, you will get a new block underneath. This is where my problem starts.
My EditableBlock component (the one that is rendered for each block in the note content) calls addNewBlock function on Enter, this function gets current blocks array and adds new empty block to given index. But the blocks variable this function uses is not updated for some reason. Like, even if data is changed in Redux store, it always gets the initial state of blocks array, updates that copy of array and updates note content with that array, which causes bugs with editing.
I've spent a lot of time googling and just can't find the solution.
Here is the code for addNewBlock function btw:
const addNewBlock = (index, data) => {
debugger;
let newBlocks = [...blocks];
newBlocks.splice(index, 0, data);
dispatch(updateNote({
id: props.id,
newData: {
name: noteData.name,
parentId: noteData.parentId,
content: newBlocks,
}
}))
}
Here is the the demo where you can see this weird bug - https://notes-app-ecru-one.vercel.app/
Just choose some note from the sidebar and try to edit blocks.
And here is the repo: https://github.com/AzamatUmirzakov/notes-app
I need quick help with React and Express. I have an array of data at my express server and fetched with react hooks useEffect. Currently, the response shows all the data on the same page, I want to display only 4 items per page? My React component has useState, useEffect and the JSX as normal. See my code below from Express. Any kind of help will be nice, thanks.
An array of data on Express server:
app.get('/api/surveyoptions', (req, res) => {
const surveyOptions = [
// { id: 5, title: "How often do you eat meat and dairy?" },
{ id: 1, name: "diet", label: "daily1", value: "daily1"},
{ id: 2, name: "diet", label: "daily2", value: "daily2" },
{ id: 3, name: "diet", label: "daily3", value: "daily3" },
{ id: 4, name: "diet", label: "daily4", value: "daily4" },
{ id: 5, name: "diet", label: "daily5", value: "daily5"},
{ id: 6, name: "diet", label: "daily6", value: "daily6" },
{ id: 7, name: "diet", label: "daily7", value: "daily7" },
{ id: 8, name: "diet", label: "daily8", value: "daily8" }
]
res.json(surveyOptions)
})
Not sure what your backend setup is and what database you're using, but in general you do something like this:
On frontend you make a request to the api, including the number of the page you need as a query parameter (e.g. you request '/api/surveyoptions?page=3').
*There are different styles of pagination: some prefer page number, some limit/offset. It really depends on the backend/database/etc. For short explanation of each style you can check this django-rest-framefork doc for example.
Server reads query params, finds pagination-related ones (e.g. we sent page=3, now server knows that we want the 3rd page of results), paginates results and send it back to the client. Server also should know how many items per page you want, or it could be a constant (e.g. always 10 items per page). Applying to your example, server would split surveyOptions array into chunks (10 items per chunk), and send you the 3rd one.
In react you store the page number in state, update it when user selects another page and sent along with the request as described in the first step.
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.
I'm using urigo:angular package. In my project I have two collections: posts and users. Is it possible to publish from server an object, which looks like this:
[{createdAt: 20-12-2015,
text: 'something',
user: { name: 'some name'}
},
{createdAt: 22-12-2015,
text: 'something2',
user: { name: 'some other name'}
}]
I mean insert user object to every post.
Update:
I have two collectionsL posts and users. Every post has user id. When I do something like this with publishComposite: https://github.com/Nitrooos/Forum-Steganum/blob/posts/server/posts.methods.coffee then I have on the client side (https://github.com/Nitrooos/Forum-Steganum/blob/posts/client/posts/posts.controller.coffee) only an array with posts, without the user in it.
I have this:
[{createdAt: 20-12-2015,
text: 'something',
userId: 123
},
{createdAt: 22-12-2015,
text: 'something2',
userId: 123
}]
So, when I'll want display a username, I'll have to do a request to every post about user?
I am trying to extend the routing in the Sencha Touch Kitchensink app as follows:
My data (in List store) are as follows:
{ category: 'fruit', str: 'tomato'},
{ category: 'fruit', str: 'green bean'},
{ category: 'vegetable', str: 'celery'},
{ category: 'vegetable', str: 'sprouts'},
{ category: 'notAVegetable', str: 'ketchup'},
{ category: 'notAVegetable', str: 'prune'}
I would like to show only those data selected by a particular category, such as "fruit"
In the Main.js controller, I am trying to do this by grabbing another parameter from the "List" node in the Demos TreeStore
routes: {
'demo/:id/:category': 'showViewById',
'menu/:id': 'showMenuById'
},
Where the showViewById action adds the extra parameter for use later
showViewById: function (id, category) {
var nav = this.getNav(),
view = nav.getStore().getNodeById(id);
console.log('view ' + id);
this.showView(view);
this.setCurrentDemo(view);
this.hideSheets();
// do stuff with category
},
I am trying to add and access 'category' as an extraParameter in my Demos.js store in the "List" tree node as follows:
{
text: 'List',
leaf: true,
id: 'list',
extraParams: {
category: 'fruit'
}
},
A few questions: Can I use an extraParameter to add this attribute to the Store? If so, how can I access it to use for my routing? I thought it would be available as metadata for my Demos store, but have not been able to access it.
Any alternatives short of creating multiple stores (one for "fruit", "vegetable", "notAVegetable," etc.) with filters on them to achieve the same thing?
TIA!