React re-rendering components issue - javascript

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;

Related

How to render ONE of multiple components conditionally that works well with React's change detector?

In my CRUD app, I have implemented several reuseable components like a "generic" DialogComponent and several non-reusable components. I have come across many scenarios where I need to (on the same page) either:
a) render one of multiple different non-reusable components conditionally like so:
return(
<>
{ condition111 && <Component_A>}
{ condition222 && <Component_B>}
</>
)
or b) pass different props to the same component conditionally. DialogComponent contains a form which renders different fields based on whether it is an ADD or EDIT dialog (depending on the props passed in):
return(<>
{
openAddDialog &&
<DialogComponent
rowAction={Utils.RowActionsEnum.Add}
setOpenDialog={setOpenAddDialog} />
}
{
openEditDialog &&
<DialogComponent
rowAction={Utils.RowActionsEnum.Edit}
setOpenDialog={setOpenEditDialog} />
}
</>)
^ This works fine, but idk if it is best practice to do it this way.
I was thinking maybe I could render a function that returns a component conditionally like below. QUESTION 1: Is this a good/bad idea in terms of React rendering?
export const GridComponent = (props) => {
...
const [gridName, setgridName] = useState(null);
useEffect(() => {
setGridName(props.gridName);
}, []);
const renderGrid = () => {
switch (gridName) {
case GridNameEnum.Student:
return <StudentGridComponent />;
case GridNameEnum.Employee:
return <EmployeeGridComponent />;
default:
return <h1>Grid rendering error.</h1>;
}
};
return(<>
{ renderGrid() }
</>)
}
QUESTION 2: What is the best way to handle this conditional rendering of a) one of multiple different components and b) same component rendered conditionally with different props? (Note: these are "child components" to be rendered on the same page)
You're asking two questions in one here and for opinions which is generally frowned upon but for the sake of trying to provide some guidance let's approach this in a functional way.
QUESTION 1: Is this a good/bad idea in terms of React rendering?:
It's fine because you only do one OR the other, never both but I'd still use a ternary if I were writing this myself to make it very clear that only one of these situations can ever occur:
enum DialogOptions {
Add,
Edit,
Delete
}
interface MyComponentProps {
DialogOption: DialogOptions
}
const MyComponent = ({ DialogOption }: MyComponentProps) => {
return DialogOption === DialogOptions.Add ? 'Add' : DialogOption === DialogOptions.Edit ? 'Edit' : DialogOption === DialogOptions.Delete ? 'Delete' : null
}
QUESTION 2: What is the best way to handle this conditional rendering of a) one of multiple different components and b) same component rendered conditionally with different props?
There are many ways you could do this and people will have different opinions. In the limited situation you've described your grid component obfuscates what is happening and means I have to dig in the code which is not helpful. It would be cleaner to:
Use a generic grid component which can be applied in multiple places (if this is possible, otherwise ignore this).
Create your StudentGridComponent and EmployeeGridComponent components which implement the generic grid component, or their own unique grid component if nothing can be shared.
Call the appropriate grid directly where it is required (whether this is a page or another component) using the ternary suggestion above (but put this inside your return and conditionally render the appropriate component or return some sort of empty message)
In terms of the way you've built your grid component I don't see the point of this. At the time where you're rendering your grid you should already be conditionally rendering either of the grids because you know you have that data, or you don't. There's no need for the complex state logic you've introduced within the grid component itself.

Render form fields based on selected option in React

I want to implement a form that is made of base fields (e.g.: product name, price and type), then type-specific fields depending on the type selected.
The solution seemed straightfoward at first (see this codesandbox), since I managed to render different components based on the type found in the current product state.
However, because these components are being rendered conditionally while using hooks internally such as useEffect, they are breaking the Rules of Hooks and causing the following error when I change from one product type to another:
Should have a queue. This is likely a bug in React. Please file an issue.
Warning: React has detected a change in the order of Hooks called by TypeSpecificForm. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks (...)
Previous render Next render
------------------------------------------------------
1. useState useEffect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
in TypeSpecificForm (at ProductForm.js:36)
in div (at ProductForm.js:14)
in ProductForm (at App.js:8)
in App (at index.js:15)
in StrictMode (at index.js:14)
What is the correct way to render these partial fields dynamically?
As I suspected, the solution was something rather silly.
Instead of returning the render function for the dynamic field...
function TypeSpecificForm({ product, onChange }) {
const productType = productTypes[product.type];
if (!productType?.renderForm) return null;
return productType.renderForm({ product, onChange });
}
...I called it as a React Node component like so:
function TypeSpecificForm({ product, onChange }) {
const productType = productTypes[product.type];
if (!productType?.renderForm) return null;
const Component = productType.renderForm
return <Component product={product} onChange={onChange} />
}
Now React no longer complains about breaking the Rules of Hooks and everything works as expected.
I remember however seeing something on the React docs that these 2 approaches could be interchangeable, which clearly they are not. I'll update my answer once I find the exact root cause for this issue.
Edit: I still couldn't find anything on the official docs that highlights the differences between rendering components via a render function vs JSX components, but I did come across an interesting discussion about this very subject.

React - Creating a recursive comment system that moves the reply

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.

What counts as mutating state in React?

Background is at the top, my actual question is simple enough, at the bottom, but I provided the context in case I'm going about this totally wrong and my question turns out to not even be relevant. I have only been using react for about two weeks.
What I'm trying to do is create a singleton, re-usable backdrop that can be closed either by clicking it, or by clicking a control on the elements that use a backdrop. This is to avoid rendering multiple backdrops in multiple places in the DOM (e.g. grouping a backdrop with each different type of modal, side drawer or content preview) or have multiple sources of truth for the state of the backdrop.
What I've done is create the Backdrop itself, which is not exported
const Backdrop = props => (
props.show ? <div onClick={props.onClose} className={classes.Backdrop}></div> : null
);
I've also created a backdrop context, managed by a WithBackdrop higher order class component which manages the state of the backdrop and updates the context accordingly
class WithBackdrop extends Component {
state = {
show: true,
closeListeners: []
}
show() {
this.setState({ show: true });
}
hide() {
this.state.closeListeners.map(f => f());
this.setState({ show: false, closeListeners: [] });
}
registerCloseListener(cb) {
// this.setState({ closeListeners: [...this.state.closeListeners, cb]});
// Does this count as mutating state?
this.state.closeListeners.push(cb);
}
render() {
const contextData = {
isShown: this.state.show,
show: this.show.bind(this),
hide: this.hide.bind(this),
registerCloseListener: this.registerCloseListener.bind(this)
};
return (
<BackdropContext.Provider value={contextData}>
<Backdrop show={this.state.show} onClose={this.hide.bind(this)} />
{this.props.children}
</BackdropContext.Provider>
);
}
}
export default WithBackdrop;
I've also exported a 'backdropable' HOC which wraps a component with the context consumer
export const backdropable = Component => (props) => (
<BackdropContext.Consumer>
{value => <Component {...props} backdropContext={value}/>}
</BackdropContext.Consumer>
);
The usage of this API would be as follows: Wrap the part of your Layout/App that you want to potentially have a backdrop, and provide the context to any component that would activate a backdrop. 'Backdropable' is a just a lazy word I used for 'can trigger a backdrop' (not shown here, but I'm using TypeScript and that makes a little more sense as an interface name). Backdropable components can call show() or hide() and not have to worry about other components which may have triggered the backdrop, or about multiple sources of truth about the backdrop's state.
The last problem I had, however, was how to trigger a backdropable components close handler? I decided the WithBackdrop HOC would maintain a list of listeners so that components that need to react when the backdrop is closed by clicking the backdrop (rather than by that backdropable component's close button or something). Here is the modal component I'm using to test this
const modal = (props) => {
props.backdropContext.registerCloseListener(props.onClose);
return (
<div
className={[
classes.Modal,
(props.show ? '' : classes.hidden)
].join(' ')}>
{props.children}
<button onClick={() => {
props.onClose();
props.backdropContext.hide()
}}>Cancel</button>
<button onClick={props.onContinue}>Continue</button>
</div>
)
}
export default backdropable(modal);
As far as I understand, it is best practice to never mutate state. My question is, does pushing to an array maintained in state count as mutating state, and what potentially bad consequences should I expect from this? Should I copy the array into a new array with the new element every single time, or will I only get undefined React behaviour if I try to change the reference of a state member. As far as I understand react only shallowly compares previous and next state to determine re-renders and provides utilities for more complicated comparisons, and so this should be fine right? The reason is that the array copying method triggers a re-render, then the modal tries to re-register the closeListener, then WithBackdrop tries to add it again...and I get an infinite state update loop.
Even if there is nothing wrong with simply pushing to the same array, do you think there is a better way to go about doing this?
Thanks, I sincerely appreciate the efforts anyone who tries to answer this long question.
EDIT: this.setState({ closeListeners: [...this.state.closeListeners, cb]}); results in an infinite state-update loop.
Mutating state in React is when you change any value or referenced object in state without using setState.
As far as I understand, it is best practice to never mutate state. My
question is, does pushing to an array maintained in state count as
mutating state,
Yes
and what potentially bad consequences should I expect from this?
You can expect to change the value of state and not see the ui update.
Should I copy the array into a new array with the new element every
single time,
Yes:
const things = [...this.state.things]
// change things
this.setState({ things })
or will I only get undefined React behaviour if I try to
change the reference of a state member. As far as I understand react
only shallowly compares previous and next state to determine
re-renders and provides utilities for more complicated comparisons,
and so this should be fine right?
It will compare if you call setState and update if necessary. If you do not use setState, it won't even check.
Any changes directly to the state (without setState()) = mutating the state. In your case it is this line:
this.state.closeListeners.push(cb);
As #twharmon mentioned, you change the values in the memory but this does not trigger the render() of your component, but your component will eventually updated from the parent components leading to ugly and hard to debug side effects.
The solution for your problem using destructuring assignment syntax:
this.setState({
closeListeners: [...this.state.closeListeners, cb]
});
PS: Destructuring also helps to keep your code cleaner:
const Backdrop = ({ show, onClose }) => (
show ? <div onClick={onClose} className={classes.Backdrop}></div> : null
);

Nested component list does not update correctly

I have a recursively defined component tree which is something like this:
class MyListItem extends Component {
...
componentDidMount() {
this.listener = dataUpdateEvent.addListener(event, (newState) => {
if(newState.id == this.state.id) {
this.setState(newState)
}
})
}
...
render() {
return (
<div>
<h1>{this.state.title}</h1>
<div>
{this.state.children.map( child => {
return (<MyListItem key={child.id} data={child} />)
})}
</div>
</div>
)
}
}
So basically this view renders a series of nested lists to represent a tree-like data structure. dataUpdateEvent is triggered various ways, and is intended to trigger a reload of the relevant component, and all sub-lists.
However I'm running into some strange behavior. Specifically, if one MyListItem component and its child update in quick succession, I see the top level list change as expected, but the sub-list remains in an un-altered state.
Interestingly, if I use randomized keys for the list items, everything works perfectly:
...
return (<MyListItem key={uuid()} data={child} />)
...
Although there is some undesirable UI lag. My thought is, maybe there is something to do with key-based caching that causes this issue.
What am I doing wrong?
React uses the keys to map changes so you need those. There should be a warning in the console if you don't use unique keys. Do you have any duplicate ids? Also try passing all your data in as props instead of setting state, then you won't need a listener at all.

Categories