React - Creating a recursive comment system that moves the reply - javascript

Let me preface this question with my experience level. I have been trying to get into web development as a hobby, and as a result, I found my way to learning React. It's been a fun experience trying to learn through trial and error. Usually, I have been following tutorials, but today, I have tried to do something a little more outside of my comfort zone.
I wish to create a comment system. All comments will be to the left side of the screen, displayed in an arbitrary manner. The user would select a comment from the left side, from where it will be displayed in full on the right side of the screen. The comment in the right side view will have a list of buttons. Each of those buttons opens up a reply to the parent comment. The child will be placed beside the parent.
Right now, I am only concerned with the logic of moving comments from the left view to the right view. In its current state, all the comments are shown in a single view, where clicking a reply button of a comment removes the reply from the main view, and places it directly beside the parent.
Given the limits of my understanding, I have worked out a half-solution. The major issue here is this bug: If a comment with no parent has two or more replies, opening all of them will cause the main list to not remove the replies from the main list, where they should be, and moved to the side of the parent. I tracked down what I believe to be the issue to the makeHidden function, or more specifically, the use of the backendComments useState. I guess that I am using it in a fundamentally wrong manner.
I apologize in advanced if I am not describing this question clearly. If my code is of no help, I would ask for a explanation on how you might tackle this problem, and please, any resources would be greatly appreciated.
CommentView.jsx
import {getComments} from '../mockDB/api'
import Comment from '../components/Comment'
import React from 'react'
import {useState, useEffect} from 'react'
function Threadview() {
const [backendComments, setBackendComments] = useState([])
const [cachedComments, setCachedComments] = useState([])
var hiddenComments = [];
const getReplies = (commentID) => {
return cachedComments.filter(backendComment => backendComment.parentID ===
commentID)
}
const makeHidden = (comment) => {
hiddenComments.push(comment)
var hiddenListIDs = [];
hiddenComments.forEach((e) => hiddenListIDs.push(e.id))
var filter = cachedComments.filter((backendComment) =>
!hiddenListIDs.includes(backendComment.id))
setBackendComments(filter)
}
useEffect(() => {
getComments().then(data => {
setBackendComments(data)
setCachedComments(data)
})
}, [])
return (
<>
{backendComments.length > 0 ? backendComments.map((comment) =>
<Comment key={comment.id} text={comment.text} replies={getReplies(comment.id)}
getReplies={getReplies} makeHidden={makeHidden}/>) : null}
</>
}
Comment.jsx
import React from 'react'
import {useState, useEffect} from 'react'
function Comment({text, replies=[], getReplies, makeHidden}) {
const [selectedReplies, setSelectedReplies] = useState([])
const openReply= (comment) => {
var newComment = <Comment key={comment.id} text={comment.text} replies=
{getReplies(comment.id)} getReplies={getReplies} makeHidden={makeHidden}/>
var newList = [...selectedReplies, newComment]
setSelectedReplies(newList)
makeHidden(comment)
return (
<>
{selectedReplies.length > 0 ? (
<div className={styles['reply-container']}>
<div className={styles['reply']}>
{selectedReplies}
</div>
</div>
) : (null)}
{replies.length > 0 ? replies.map((comment) => <ReplyButton onClick={() =>
openReply(comment)} key={comment.id} />) : null}
</>
}
Update
I found out that the issue starts only with comments that are not nested. Comment.jsx returns the comment that was selected from a reply button to CommentView.jsx, into hiddenComments. The idea is that hiddenComments acquires and holds all of the comments that are being nested, indicating that they have to be removed from the main view. But, whenever a comment that is not nested has it's replies selected, hiddenComments get reset.

Update
I finally solved my issue. I struggled a lot with this seemingly simple problem, and it allowed me to gain a better understanding on how React works.
As I said before, whenever a comment with no parent was being selected, it would not filter the selected replies from the main list. I thought to create another useState of hidden replies, instead of hiddenComments, and feed the list of comments to-be-hid in that. This resulted in the opposite problem. Now, only the replies opened from the comment with no parent were being filtered. It was almost like the list of comments to be hid was being reset every time a nested child was being interacted with.
I then looked at the way in which nested children were being created, which made me almost facepalm due to my stupidity. I was creating a new instance of a comment component (newComment) every time a new child needed to be created, and I was adding that comment component to the list of children to be displayed. I should have never made a new component outside of the return function, because it was somehow resetting the state in CommentView whenever it was being interacted with.
So, I simply made it so that the comment object itself, not the comment component, was being added to selectedReplies. Then, I simply mapped the comment objects in selectedReplies to comment components in the return, exactly how CommentView.jsx is set up.
Again, apologies if I am not explaining things clearly, but my changes did result in the behaviors I want for my little test, which I would count as a success. Although, I am noticing just how flawed my code is, and I will now work on refactoring all of it, to learn as much as I can. I will spare you from that digression, because I do not want you to suffer through another novel.

Related

React re-rendering components issue

I started my first real project in React, I'm developing a portfolio site and I have a strange issue when I use the category filter to switch the categories.
The issue is: The site shows all the projects, if you click in Artwort or switch between the buttons you will see that not all the projects are showing the transition animation, it seems that the projects in the current category are not rendering again. Another weird thing is in the react developer tools the profiler shows how all the components are rendering when y change the category.
I think this behavior will have logical explanation, but I couldn't find it because I'm using useEffect dependency with the currentCat state.
you can see the error here: https://toiatemp-manuverrastro.vercel.app/
Here is the components:
https://github.com/manuverrastro/toia/blob/main/src/components/Filter.js
https://github.com/manuverrastro/toia/blob/main/src/components/Work.js
https://github.com/manuverrastro/toia/blob/main/src/components/WorkList.js
https://github.com/manuverrastro/toia/blob/main/src/components/WorkListContainer.js
Does anyone have some idea of what is happening?
It is because the key property in your WorkList.js file. Although the categories are different, work.id is not getting changed while you switch between the tabs. Since you have given work.id as the key parameter React tries to render the same previous element without re-rendering it. Because of that you don't see any animation in those Work components.
You can change your key prop which is given to the Work component, by concatenating the current selected category. So each time you switch between the tabs, key prop will differ. It will result in re-rendering the Work components. I have changed your code as my suggestion.
WorkList.js
import Work from "./Work";
const WorkList = ({ work, currentCat }) => {
return (
<>
{currentCat
? work
.filter((work) => work.category == currentCat)
.map((work) => {
return (
<Work
key={`${currentCat}-${work.id}`}
id={work.id}
slug={work.slug}
thumbnail={work.thumbnail}
image={work.image}
title={work.title}
category={work.category}
/>
);
})
: work.map((work) => {
return (
<Work
key={`all-${work.id}`}
id={work.id}
slug={work.slug}
thumbnail={work.thumbnail}
image={work.image}
title={work.title}
category={work.category}
/>
);
})}
</>
);
};
export default WorkList;

How do I show an activity indicator every time my flatlist data changes?

I'm setting the data that my flatlist component displays using a state called selectedStream. selectedStream changes every time the user presses a different group option. I've noticed that the flatlist takes 1-3 seconds to refresh all the posts that it's currently displaying already. I want there to be a loading indicator so that by the time the indicator goes away, the list is already properly displayed with the newly updated data.
<FlatList
maxToRenderPerBatch={5}
bounces={false}
windowSize={5}
ref={feedRef}
data={selectedStream}/>
Whenever we are working with anything related to the UI, sometimes we may face delays in UI re-rendering. However, we need to first figure out what is actually causing the delay.
The right question to ask about your code would be:
Is the rendering of items taking longer than expected? Or, is the data being passed with a delay because it is dependant on an API call or any other async task?
Once you answer that question, you may end up with two scenarios:
1. FlatList taking longer to render views
This doesn't usually happen as the RN FlatList will only render views that are visible to the user at any given time and will keep rendering new views as the user scrolls through the list. However, there may be some flickering issues for which you can refer to the below article:
8 Ways to optimise your RN FlatList
2. Passing the data causes the delay
This is the most common scenario, where we may call an API endpoint and get some data and then do setState to update any view/list accordingly. A general approach is to show some sort of a progress-bar that would indicate that the application is busy and thus maintaining a proper user-experience. The easiest way to do that is by conditional rendering.
A general example would be:
const [myList, setMyList] = useState();
function callAPIforMyList(){
// logic goes here
}
return {
{myList ? <ActivityIndicator .../> : <Flatlist .... />
}
The above code will check if myList is undefined or has a value. If undefined, it will render the ActivityIndicator or else the FlatList.
Another scenario could be when myList may have existing data but you need to update/replace it with new data. This way the above check may fail, so we can put another check:
const [myList, setMyList] = useState();
const [isAPIbusy, setAPIBusy] = useState(false)
function callAPIformyList() {
setAPIBusy(true)
/// other logics or async calls or redux-dispatch
setAPIBusy(false)
}
return {
{!isAPIBusy && myList ? (<Flatlist .... />) : (<ActivityIndicator .../>)
}
You can add multiple conditions using more turneries such as isAPIBusy ? <View1> : otherBoolean ? <View2> : <Default_View_When_No_Conditions_Match)/>
Hope this helps clarify your needs.

Re-Render only a particular child

I am building a dynamic form and as of now, my component hierarchy is as follows:-
App
Caseform
DynamicFormBuilder
Context.Provider
Patients
Patient key = "patient_1"
ComponentCreator key = "patient_1_1"
Textbox
ComponentCreator key = "patient_1_2"
Textbox
ComponentCreator key = "patient_1_3"
Textbox
Patient key = "patient_2"
ComponentCreator key = "patient_2_1"
Textbox
ComponentCreator key = "patient_2_2"
Textbox
ComponentCreator key = "patient_2_3"
Textbox
As of now, I have hardcoded JSON data in caseform but eventually, it will come from the fetch call. As soon as DynamicFormBuilder receives the caseform metadata, it creates states out of it.
I am maintaining state at caseform level even for its child. I thought of doing it that way because the requirement was to support patch save(send only changed data to the backend on save button press). If anybody knows of a better way of doing this, please let me know.
I am passing in a function using context API to child components so that they can update the state in DynamicFormBuiler.
The issue I am facing is that let's say even if the user edits one textbox, the whole Dynamic form gets rendered.
I have gone through a lot of answers on StackOverflow that advise on using the shouldComponentUpdate lifecycle method, but I am not able to figure out how I will use it here.
I am adding a link to the sandbox and in the console, you can see that if a user edits a field all the things are rendered again.
CodeSandbox Link
Expected Behavior:- What I am looking for is for example:- if user-edited only one textbox say in patient 1 then only that textbox gets re-rendered again
This is my first time using react. I apologize in advance if someone feels I have not done enough research, I have read through a lot of questions but still facing some challenges, any help will be really appreciated.
If you wrap each component in the React.memo() HOC, this should prevent excessive rerenders.
https://reactjs.org/docs/react-api.html#reactmemo
-Edit-
Yes it works with class components too.
e.g.
const MyComponent = React.memo(class extends React.Component {
render () {
return <h1>TEST</h1>
}
});

Why React Component unmounts on each useEffect dependency change?

I am trying to learn React by building a web application. Since I want to learn it step by step, for now I don't use Redux, I use only the React state and I have an issue.
This is my components architecture:
App.js
|
_________|_________
| |
Main.js Side.js
| |
Game.js Moves.js
As you can see, I have the main file called App.js, in the left side we have the Main.js which is the central part of the application which contains Game.js where actually my game is happening. On the right side we have Side.js which is the sidebar where I want to display the moves each player does in the game. They will be displayed in Moves.js.
To be more clear think at the chess game. In the left part you actually play the game and in the right part your moves will be listed.
Now I will show you my code and explain what the problem is.
// App.js
const App = React.memo(props => {
let [moveList, setMovesList] = useState([]);
return (
<React.Fragment>
<div className="col-8">
<Main setMovesList={setMovesList} />
</div>
<div className="col-4">
<Side moveList={moveList} />
</div>
</React.Fragment>
);
});
// Main.js
const Main = React.memo(props => {
return (
<React.Fragment>
<Game setMovesList={props.setMovesList} />
</React.Fragment>
);
});
// Game.js
const Game= React.memo(props => {
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
return () => {
document.getElementById('board').removeEventListener('click', executeMove, false);
};
})
return (
// render the game board
);
});
// Side.js
const Side= React.memo(props => {
return (
<React.Fragment>
<Moves moveList={props.moveList} />
</React.Fragment>
);
});
// Moves.js
const Moves= React.memo(props => {
let [listItems, setListItems] = useState([]);
useEffect(() => {
let items = [];
for (let i = 0; i < props.moveList.length; i++) {
items.push(<div key={i+1}><div>{i+1}</div><div>{props.moveList[i]}</div></div>)
}
setListItems(items);
return () => {
console.log('why this is being triggered on each move?')
};
}, [props.moveList]);
return (
<React.Fragment>
{listItems}
</React.Fragment>
);
});
As you can see on my code, I have defined the state in App.js. On the left side I pass the function which updates the state based on the moves the player does. On the right side I pass the state in order to update the view.
My problem is that on each click event inside Game.js the component Moves.js unmounts and that console.log is being triggered and I wasn't expected it to behave like that. I was expecting that it will unmount only when I change a view to another.
Any idea why this is happening ? Feel free to ask me anything if what I wrote does not make sense.
Thanks for explaining your question so well - it was really easy to understand.
Now, the thing is, your component isn't actually unmounting. You've passed props.movesList as a dependency for the usEffect. Now the first time your useEffect is triggered, it will set up the return statement. The next time the useEffect gets triggered due to a change in props.movesList, the return statement will get executed.
If you intend to execute something on unmount of a component - shift it to another useEffect with an empty dependency array.
answering your question
The answer to your question
"why this is being triggered on each move"
would be:
"because useEffect wants to update the component with the changed state"
But I would be inclined to say:
"you should not ask this question, you should not care"
understanding useEffect
You should understand useEffect as something that makes sure the state is up to date, not as a kind of lifecycle hook.
Imagine for a moment that useEffect gets called all the time, over and over again, just to make sure everything is up to date. This is not true, but this mental model might help to understand.
You don't care if and when useEffect gets called, you only care about if the state is correct.
The function returned from useEffect should clean up its own stuff (e.g. the eventlisteners), again, making sure everything is clean and up to date, but it is not a onUnmount handler.
understanding React hooks
You should get used to the idea that every functional component and every hook is called over and over again. React decides if it might not be necessary.
If you really have performance problems, you might use e.g. React.memo and useCallback, but even then, do not rely on that anything is not called anymore.
React might call your function anyway, if it thinks it is necessary. Use React.memo only as kind of a hint to react to do some optimization here.
more React tips
work on state
display the state
E.g. do not create a list of <div>, as you did, instead, create a list of e.g. objects, and render that list inside the view. You might even create an own MovesView component, only displaying the list. That might be a bit too much separation in your example, but you should get used to the idea, also I assume your real component will be much bigger at the end.
Don’t be afraid to split components into smaller components.
It seems the problem is occurred by Game element.
It triggers addEventListener on every render.
Why not use onClick event handler
/* remove this part
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
})
*/
const executeMove = (e) => {
props.setMovesList(e.target);
}
return (
<div id="board" onClick={executeMove}>
...
</div>
)
If you want to use addEventListener, it should be added when the component mounted. Pass empty array([]) to useEffect as second parameter.
useEffect(() => {
function executeMove(e) {
props.setMovesList(e.target);
}
document.getElementById('board').addEventListener('click', executeMove, false);
}, [])

react-window and detecting changes in size of list items

So I have been trying to figure out a solution for big lists in react. and I almost have the perfect solution - but I can't seem to figure out how to get this one detail to work.
I have a list of cards that I want to render quickly - I'm using the react-window package
import{VariableSizeList as List} from 'react-window';
I have a component called MaterialCard, and a component that contains lists of MaterialCards:
On the MaterialCard if you click on an inspect button - it opens up the innards of the card revealing a form input section.
const [cardOpen, setCardOpen] = useState(false);
const cardInnards = //a bunch of jsx.
//in the return
<button className="btn-inspect" onClick={()=>setCardOpen(!cardOpen)}>inspect/edit</button>
{cardOpen?cardInnards:<div></div>}
In the multi-list component container - I have this code.
const materialCardsNew = materialsNew.map((x,i)=><MaterialCard props={x} key ={`${i}matsnew`} />);
//react-window function to make a row:
const Row = array=> ({ index }) => array[index] //is MaterialCard
//in the return
<List
height={755}
itemCount={materialCardsNew.length-1}
itemSize={()=>130} // this is where I'm having trouble.
width={634}
>
{Row(materialCardsNew)}
</List>
Currently the itemSize is what I'm trying to fix...
What happens currently is the item has a fixed size area it appears in - and (thanks to z-index) the innards appear over other items in the list.
What I want to happen is the item size of the MaterialCard that is open - to be of a larger size: such that it doesn't cover other cards - I want it to expand and I don't want it to cover other cards at all.
My trouble is I don't know how to read the component's internal state from the top - how do I determine in the list container component which card is open. I understand I require a function for this... but: well...
the pseudocode I've come up with is this: this of course does not work - but it is more or less what I want to do.
//resize function
const getHeight = (arr)=>(index)=>arr[index].state.cardOpen?500:100; //bigger size if the card is open
//jsx
itemSize={getHeight(materialCardsNew)}
//I've also tried arr[index].style.height,
//and searched through the arr[index] properties in the console to see
//if there was anything I could use... no luck.
My first ideas are bunk... and I'm not really sure how to approach this... I'm pretty sure I shouldn't need a massive additional array for each array of material cards (there are a few categories) to keep track of which cards are open... but I can't seem to find the correct way of going about this.
How do I accomplish this?
For this issue:
My trouble is I don't know how to read the component's internal state
from the top
Lift state up. So in the containing component, you can use a hook like in the top level component:
const [activeCard, setActiveCard] = useState()
And in the card, pass in the function:
<Card setActiveCard={setActiveCard} key={someKey} {...otherProps}/>
And, in the implementation of the card, you can have something like:
useEffect(() => setActiveCard(key), [key])
And the top level component will have the 'active' card information.
Not sure I completely was clear on the issue, but that is one mechanism for sending child information to the parent.
And, if I am understanding the issue, you could have some logic in the child component to check if the active card is equal to the card:
<Card setActiveCard={setActiveCard} activeCard={activeCard} key={someKey} {...otherProps} />
useEffect(() => activeCard === key ? setComponentSize(activeSize) : setComponentSize(defaultSize), [{dependency array}])
Of course, the setComponentSize would be in the top level component, and passed in a similar fashion to setting the card index in the top level. And if everything is set in the containing (parent component), you could just check the index vs the activeCard.
Finally, just make sure however you are checking for the active card, you cleanup and call setActiveCard(-1), or whatever the default parameter you might want to use when the active card's state changes.

Categories