React Data in props does not update? - reactjs

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

Related

Dynamically use React Component based on data in an object

I'm creating this menu as a fun project in React, and I've finished the code to display/style the components, so now I'm setting it up to be dynamic and generate the menu based on a passed set of data. My React project routes like this:App /→Tab /→Various components based on data.
The plan is to have a menu (App/) contain potentially multiple Tab / of various inputs, such as date, text, number, range, etc. Let's say I use this dataset as an example:
elements: [
{
type: 'number',
text: 'Some Text Label',
fields: [
{
text: 'Some Text',
min: 0,
max: 50,
value: 25,
},
{
text: 'Some Text',
min: 25,
max: 75,
value: 50,
},
],
},
{
type: 'text',
text: 'Some Text Label',
fields: [
{
text: 'Some Text',
value: 'Some Text Placeholder',
},
],
},
{
type: 'number',
text: 'Some Text Label',
fields: [
{
text: 'Some Text',
min: 0,
max: 100,
value: 50,
},
],
},
]
Within the Tab / component, I'd want to look through the elements to find the type of one element, then use the component that is ready for that element (for example, type:number, I would use Number / and then pass the fields as a prop to that component.
Something like this:Number fields={insertdatahere}/>
I'm a total beginner when it comes to react, but I've tried a few ways myself and I feel like I'm just missing something because nothing is working. I was considering just having all the Components manually placed inside the tab component and having them set as Display: None unless an element is present for that type. Thanks for any advice.
Here's a Code sandbox which illustrates one way you could get started.
The basic idea is to define a component for each type of field (text, number, ...) and make a lookup. A function component is like any other value in Javascript - you can store it in an array or an object.
In this case I've defined TextDisplay and NumberDisplay:
const TextDisplay = ({ text, value }) => (
<li>
<h4>{text}</h4>
<input value={value} />
</li>
);
const NumberDisplay = ({ text, min, max, value }) => (
<div>
<h4>{text}</h4>
<input type="number" min={min} max={max} value={value} />
</div>
);
const ComponentForType = {
text: TextDisplay,
number: NumberDisplay
};
Then there's a bit of machinery for displaying the lists of elements in the definition, and the list of fields in each element.
Hopefully that can give you something to play around with. There's a bit of weird syntax in there like the spread operator, and everything gets complicated once you want to make the values editable, but this could be a starting point.

Merge and sort arrays of objects JS/TS/Svelte - conceptual understanding

The goal is to display a recent activity overview.
As an example: I would like it to display posts, comments, users.
A post, comment and user object live in its corresponding arrays. All of the objects have a timestamp (below createdAt), but also keys that the objects from the different arrays don't have. The recent activites should be sorted by the timestamp.
(Ultimately it should be sortable by different values, but first I would like to get a better general understanding behind merging and sorting arrays / objects and not making it to complicated)
I thought of somehow merging the arrays into something like an activity array, then sorting it and looping over it and conditionally output an object with its keys depending on what kind of object it is?
If someone is willing to deal with this by giving an example, it would make my day. The best thing I could imagine would be a svelte REPL that solves this scenario. Anyway I'm thankful for every hint. There probably already are good examples and resources for this (I think common) use case that I didn't find. If someone could refer to these, this would also be superb.
The example I'm intending to use to get this conceptual understanding:
const users = [
{ id: 'a', name: 'michael', createdAt: 1 },
{ id: 'b', name: 'john', createdAt: 2 },
{ id: 'c', name: 'caren', createdAt: 3 }
]
const posts = [
{ id: 'd', topic: 'food', content: 'nonomnom' createdAt: 4 },
{ id: 'e', name: 'drink', content: 'water is the best' createdAt: 5 },
{ id: 'f', name: 'sleep', content: 'i miss it' createdAt: 6 }
]
const comments = [
{ id: 'g', parent: 'd', content: 'sounds yummy' createdAt: 7 },
{ id: 'h', parent: 'e', content: 'pure life' createdAt: 8 },
{ id: 'i', parent: 'f', content: 'me too' createdAt: 9 }
]
Edit: it would have been a bit better example with more descriptive id keys, like userId and when a post and comment object contains the userId. However, the answers below make it very understandable and applicable for "real world" use cases.
This is fun to think about and it's great that you're putting thought into the architecture of the activity feed.
I'd say you're on the right track with how you're thinking of approaching it.
Think about:
How you want to model the data for use in your application
How you process that model
Then think about how you display it
You have 3 different types of data and you have an overall activity feed you want to create. Each type has createdAt in common.
There's a couple of ways you could do this:
Simply merge them all into one array and then sort by createdAt
const activities = [...users, ...posts, ...comments];
activities.sort((a,b) => b.createdAt - a.createdAt); // Sort whichever way you want
The tricky part here is when you're outputting it, you'll need a way of telling what type of object each element in the array is. For users, you can look for a name key, for posts you could look for the topic key, for comments you could look for the parent/content keys to confirm object type but this is a bit of a brittle approach.
Let's try to see if we can do better.
Give each activity object an explicit type variable.
const activities = [
...users.map((u) => ({...u, type: 'user'})),
...posts.map((u) => ({...u, type: 'post'})),
...comments.map((u) => ({...u, type: 'comment'}))
];
Now you can easily tell what any given element in the whole activities array is based on its type field.
As a bonus, this type field can also let you easily add a feature to filter the activity feed down to just certain types! And it also makes it much simpler to add new types of activities in the future.
Here's a typescript playground showing it and logging the output.
As a typesafe bonus, you can add types in typescript to reinforce the expected data types:
eg.
type User = {
type: 'user';
name: string;
} & Common;
type Post = {
type: 'post';
topic: string;
content: string;
} & Common;
type UserComment = {
type: 'comment';
parent: string;
content: string;
} & Common;
type Activity = User | Post | UserComment;
To expand on the other answers, eventually you will want to show each element also differently, while you could do this with an if block testing the type that has been added to the object, this is not very scalable as a new type of block would require at least two changes, one to add the type to the activities array and one to add this new type to the if blocks.
Instead if we change our activities array as follows:
const activities = [
...users.map((u) => ({...u, component: UserCompomnent})),
...posts.map((u) => ({...u, component: PostComponent})),
...comments.map((u) => ({...u, component: CommentComponent}))
];
where UserComponent, PostComponent and CommentComponent are the different ways of presenting this data.
Then when you loop over your data to display them, we can use svelte:component and leverage that we already defined which component should be shown:
{#each acitivities as activity}
<svelte:component this={activity.component} {...activity} />
{/each}
Here's an approach using simple 'helper classes' so that the different objects can be distinguished when displayed REPL
<script>
class User {
constructor(obj){
Object.assign(this, obj)
}
}
class Post {
constructor(obj){
Object.assign(this, obj)
}
}
class Comment {
constructor(obj){
Object.assign(this, obj)
}
}
const users = [
{ id: 'a', name: 'michael', createdAt: 1652012110220 },
{ id: 'b', name: 'john', createdAt: 1652006110121 },
{ id: 'c', name: 'caren', createdAt: 1652018110220 }
].map(user => new User(user))
const posts = [
{ id: 'd', topic: 'food', content: 'nonomnom', createdAt: 1652016900220 },
{ id: 'e', topic: 'drink', content: 'water is the best', createdAt: 1652016910220 },
{ id: 'f', topic: 'sleep', content: 'i miss it', createdAt: 1652016960220 }
].map(post => new Post(post))
const comments = [
{ id: 'g', parent: 'd', content: 'sounds yummy', createdAt: 1652116910220 },
{ id: 'h', parent: 'e', content: 'pure life', createdAt: 1652016913220 },
{ id: 'i', parent: 'f', content: 'me too', createdAt: 1652016510220 }
].map(comment => new Comment(comment))
const recentActivities = users.concat(posts).concat(comments).sort((a,b) => b.createdAt - a.createdAt)
</script>
<ul>
{#each recentActivities as activity}
<li>
{new Date(activity.createdAt).toLocaleString()} -
{#if activity instanceof User}
User - {activity.name}
{:else if activity instanceof Post}
Post - {activity.topic}
{:else if activity instanceof Comment}
Comment - {activity.content}
{/if}
</li>
{/each}
</ul>

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!

Move list item to new parent without flickering

Imagine the following list:
Managing Director
Sales Director
IT Director
Technical Lead
Software Developer
Support Technician
HR Department
HR Officer
HR Assistant 1
HR Assistant 2
It's backed by a state in the form of:
[
{
id: 1,
text: "Managing Director",
children: [
{
id: 2,
text: "Sales Director"
}
...
]
}
...
]
Now I want to indent Support Technician. I would modify the state array to remove the item from the Technical Lead parent & add it to the Software Developer parent. The problem is, that React first deletes it, which causes all items below it to jump one line up, and then in the next frame adds it again to the new parent, which pushes those items a line down again. This appears as a flicker. It doesn't happen every time (sometimes react manages to render both in the same frame), but often enough it happens and is very distracting.
The state is modified in a way, that the parent passes its state callback setState down to its children. In this case, the initial state of the Technical Lead node looks like:
{
id: 4,
text: "Technical Lead",
children: [
{
id: 5,
text: "Software Developer"
},
{
id: 6,
text: "Support Technician"
}
]
}
As obvious from the state, every node renders all its children recursively.
After the indention, the state is modified to the following:
{
id: 4,
text: "Technical Lead",
children: [
{
id: 5,
text: "Software Developer",
chiilderen: [
{
id: 6,
text: "Support Technician"
}
]
}
]
}
If I were to this without React and instead with regular DOM APIs, I would move the node to the new parent with something like insertBefore(). React on the other hand unmounts & remounts the node.
Below is a simplified example of my "Node" component, which renders the list:
const Node = ({data, setSiblings}) => {
const [children, setChildren] = useState(data.children)
function indent() {
setSiblings(siblings => {
// const prevSibling = find the item in the state array
// const thisNode = {id, text, children}
const newPrevSibling = {...prevSibling, children: [thisNode]}
const siblingsWithout_ThisNode = deleteFromArray(siblings, thisNodeIndex)
// updateAtIndex() returns a new array with the modification (immutable)
return updateAtIndex(siblingsWithout_ThisNode, prevSiblingIndex, newPrevSibling)
})
}
const childNodes = children?.map(child =>
<Node data={child} setSiblings={setChildren} key={child.id}/>
)
return (
<li>
<div>{data.text}</div>
{childNodes ? <ul>{childNodes}</ul> : null}
</li>
)
}
The indent() function is triggered by a Tab press, but I didn't include the key handler logic here
I didn't find a solution to this problem directly, but I switched to using MST (MobX-State-Tree) for state management now and it worked with it (didn't flicker anymore - seemingly, both the unmounting & remounting of the component happen in the same frame now).
Below is a CodeSandbox with the MST implementation. When clicking e.g. on the Support Technician node, you can press Tab to indent and Shift + Tab to outdent (you have to click again, since it loses focus)
https://codesandbox.io/s/priceless-keldysh-17e9h?file=/src/App.js
While this doesn't answer my question directly, it helped solve the problem semantically (it's better than nothing).

Proper redux store shape

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?

Resources