React memo child component render - javascript

I'm having trouble rendering my comment components.
So I have a listComment component and it has 2 child component CommentItem and CommentGroup.
My CommentGroup component is like a dropdown where you can open and close it.
I tried to use React.memo() but it still rendering children thats already rendered
My problem is that every time I add a new comment it renders again the child components that's already rendered. So the comments that's already open the CommentGroup closes. And i use redux for state-management.
PS sorry for the bad english.
Comment Data
[{
body: "comment 1",
comment_counter: 0,
createdAt: "2020-06-14T13:42:38.465Z",
heart_counter: 0,
ownerId: "5edce08cabc7ab1860c7bdf4",
postId: "5ee3770495bfce029842bc68",
_id: "5ee6294eb7295a1c04b62374"
}, {
body: "comment 2",
comment_counter: 0,
createdAt: "2020-06-14T13:42:38.465Z",
heart_counter: 0,
ownerId: "5edce08cabc7ab1860c7bdf4",
postId: "5ee3770495bfce029842bc68",
_id: "5ee6294eb7295a1c04b62374"
}]
ListComments.js
const comments = useSelector(state => state.comment.comments)
return comments.map(comment => {
return (
<div key={comment._id}>
<CommentItem comment={comment} type="post_comment" />
<div className={classes.mLeft}>
<CommentGroup counter={comment.comment_counter} />
</div>
</div >
)
})
CommentGroup.js
const CommentGroup = React.memo((props) => {
const [open, setOpen] = useState(false)
const onOpen = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div>
<Button
size="small"
color="primary"
startIcon={
!open ? <ArrowDropDownOutlinedIcon /> : <ArrowDropUpOutlinedIcon />
}
onClick={
!open ? () => onOpen() : () => onClose()
}
>
{!open ? 'View' : 'Hide'} {1} Replies
</Button>
CommentGroupOpen: {open ? 'true' : 'false'}
</div>
)
}, (prevProps, nextProps) => {
console.log(prevProps) // not getting called
if (prevProps.counter !== nextProps.counter) {
return false
}
return true
})
export default CommentGroup
CommentItem is just a display component

It's likely because that all the comments have the same comment._id which is used as the key. I made a similar example and it worked fine. https://codesandbox.io/s/mutable-framework-stk5g

Related

Trying to mutate the state of React component

I'm trying to create a quizz app and got stuck at the moment when I need to change the background of the Answers (button) when the user clicked on it. Well, function holdAnswer does console.log the id of the answer, but doesn't change its background. What's missing here?
I assume I have also stored all answers that the user chose in some array in order to count how many answers the user guessed.
After I check if the answers are correct or not they have to be highlighted accordingly (correct/incorrect), so again it needs to mutate the state.
Is the code missing something from the beginning?
here is CodeSandBox link
App.js
import { useState } from "react";
import QuestionSet from "./QuestionSet";
import Answers from "./Answers";
import { nanoid } from "nanoid";
function App() {
const [isQuesLoaded, setIsQuesLoaded] = useState(false);
const [questions, setQuestions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
async function startQuiz() {
try {
setIsQuesLoaded(!isQuesLoaded);
const response = await fetch(
"https://opentdb.com/api.php?amount=5&category=12&difficulty=easy&type=multiple"
);
const data = await response.json();
const allQuestions = data.results;
const listOfQuestions = allQuestions.map((item) => {
const allAnswers = [
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[0],
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[1],
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[2],
},
{
id: nanoid(),
isCorrect: true,
isChosen: false,
answer: item.correct_answer,
},
];
return {
id: nanoid(),
question: item.question,
answers: allAnswers,
};
});
setQuestions(listOfQuestions);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function holdAnswer(id) {
console.log(id);
setQuestions((prevQuestion) =>
prevQuestion.map((question) =>
question.answers.id === id
? {
...question,
answers: question.answers.map((answer) =>
answer.id === id
? { ...answer, isChosen: !answer.isChosen }
: answer
),
}
: question
)
);
}
const questionElm = questions.map((question, index) => {
return (
<section key={index}>
<QuestionSet question={question.question} key={question.id} />
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={holdAnswer}
/>
</section>
);
});
return (
<div className="App">
{!isQuesLoaded ? (
<main>
<h1 className="title-app">Quizzical</h1>
<p className="desc-app">Some description if needed</p>
<button className="btn" onClick={startQuiz}>
Start Quiz
</button>
</main>
) : (
<main className="quest-box">
{loading && <div>Loading data...</div>}
{error && <div>{`There is a problem fetchning data = ${error}`}</div>}
<section className="quest-content">{questionElm}</section>
<button className="answer-btn">Check Answers</button>
</main>
)}
</div>
);
}
export default App;
Answers.js
export default function Answers(props) {
const styles = {
backgroundColor: props.answers.isChosen ? "#D6DBF5" : "transparent",
};
return (
<section className="answer-container">
<div
className="answer-div"
style={styles}
id={props.answers[3].id}
onClick={() => props.holdAnswer(props.answers[3].id)}
>
<p>{props.answers[3].answer}</p>
</div>
<div
className="answer-div"
style={styles}
id={props.answers[1].id}
onClick={() => props.holdAnswer(props.answers[1].id)}
>
<p>{props.answers[1].answer}</p>
</div>
<div
className="answer-div"
style={styles}
id={props.answers[2].id}
onClick={() => props.holdAnswer(props.answers[2].id)}
>
<p>{props.answers[2].answer}</p>
</div>
<div
className="answer-div"
style={styles}
id={props.answers[0].id}
onClick={() => props.holdAnswer(props.answers[0].id)}
>
<p>{props.answers[0].answer}</p>
</div>
</section>
);
}
Your making confusions on object fields, using typescript will prevent you from doing this kind of error.
function holdAnswer(id) {
console.log(id);
setQuestions((prevQuestion) =>
prevQuestion.map((question) =>
question.answers.id === id // question.answers is an array
// you need to pass the questionId to holdAnswer and use it here
? {
...question,
answers: question.answers.map((answer) =>
answer.id === id
? { ...answer, isChosen: !answer.isChosen }
: answer
)
}
: question
)
);
}
// props.answers is also an array, create a component for Answer and use style here
const styles = {
backgroundColor: props.answers.isChosen ? "#D6DBF5" : "transparent"
};
Here is the codesandbox if you want to see how to fix it.
I wont give any direct answer, its up to you to find out.
First, there is an issue with the React.StrictMode as it runs some of the react api twice such as useEffect (and in ur case the setter for setQuestions), you can remove it for now since i dont think you really need it for this.
Lastly, if you look carefully at where you are changing the styles conditionally you'll see that you are referencing some object fields incorrectly.
it looks like you're just starting out react, so good luck and happy coding.

React functional component child won't re-render when props change

I'm passing an array of objects to a child as props, and I wanted the child component to re-render when the aray changes, but it doesn't:
parent:
export default function App() {
const [items, setItems] = useState([]);
const buttonCallback = () => {
setItems([...items, { text: "new item" }]);
};
return (
<div className="App">
<h1>Items list should update when I add item</h1>
<button onClick={buttonCallback}>Add Item</button>
<Items />
</div>
);
}
child:
const Items = ({ itemlist = [] }) => {
useEffect(() => {
console.log("Items changed!"); // This gets called, so the props is changing
}, [itemlist]);
return (
<div className="items-column">
{itemlist?.length
? itemlist.map((item, i) => <Item key={i} text={item.text + i} />)
: "No items"}
<br />
{`Itemlist size: ${itemlist.length}`}
</div>
);
};
I found this question with the same issue, but it's for class components, so the solution doesn't apply to my case.
Sandbox demo
<Items propsname={data} />
const buttonCallback = () => {
setItems([...items, { text: "new item" }]);
};
but you should put it as:
const buttonCallback = () => {
setItems([...items, { text: "new item", id: Date.now() }]);
};
Because is not recommended to use index as a key for react children. Instead, you can use the actual date with that function. That is the key for React to know the children has changed.
itemlist.map((item) => <Item key={item.id} text={item.text} />)
Try below:
you are adding array as a dependency, to recognise change in variable, you should do deep copy or any other thing which will tell that useEffect obj is really different.
`
const Items = ({ itemlist = [] }) => {
const [someState,someState]=useState(itemlist);
useEffect(() => {
someState(itemlist)
}, [JSON.stringify(itemlist)]);
return (
<div className="items-column">
{someState?.length
? someState.map((item, i) => <Item key={i} text={item.text
+ i}
/>)
: "No items"}
<br />
{`Itemlist size: ${someState.length}`}
</div>
);
};

Component data was gone after re rendering, even though Component was react.memo already

I have two components.
First is called: BucketTabs
Second is called:BucketForms
To have a better idea. Below pictures illustrate it.
When I switching tab, different form will be showed below.
Q: Whenever I switch from one tab to other tab, and then switch back, the content in the previous BucketForms will be gone. But, gone data are supposed to be stored into a state of that BucketForms.
In fact, I've memo the BucketForms already, so I've expected the content(data) would not be gone.
What's the problem and how could I prevent the data to be gone after switching tab.
My BucketTabs:
import { BucketForms } from '~components/BucketForms/BuckForms'
export const BucketTabs: React.FC = () => {
const items = useMemo<ContentTabsItem[]>((): ContentTabsItem[] => {
return [
{
title: '1',
renderContent: () => <BucketForms key="1" bucketCategory="1" />,
},
{
title: '2',
renderContent: () => <BucketForms key="2" bucketCategory="2" />,
},
]
}, [])
return (
<div className="row">
<div className="col">
<ContentTabs items={tabs} kind="tabs" />
</div>
</div>
)
}
BucketForms
function PropsAreEqual(prev, next) {
const result = prev.bucketCategory === next.bucketCategory;
return result;
}
interface IData {
portfolioValue?: number
}
export const BucketForms: React.FC<IProps> = React.memo(props => {
const { bucketCategory } = props
const [data, setData] = useState<IData>({
})
const view = ({
portfolioValue,
}: IData) => {
return (
<>
<div className="row portfolio">
<FormNumericInput
key="input-portfolio-value"
name="portfolioValue"
required
value={portfolioValue}
/>
</div>
</>
)
}
return (
<Form
onChange={e => {
setData({ ...data, ...e, })
}}
>
{view(data)}
</Form>
)
}, PropsAreEqual)

React state wrongly updated in nested components with CodeMirror 6 Editor

I have a few components nested inside a larger "controller" component.
The whole demo app is below. There's also a StackBlitz.
import React, { useState } from 'react';
import CodeEditor from './CodeEditor';
import './style.css';
const PostEditor = ({ value, onChange }) => {
return (
<>
{value && (
<>
<CodeEditor value={value} onChange={value => onChange(value)} />
</>
)}
</>
);
};
const Wrapper = ({ post, updatePostProperty }) => {
return (
<>
<h2>Selected post: {post && post.title}</h2>
{post && post.title && (
<PostEditor
value={post.title}
onChange={value => {
console.log('update title->', value);
updatePostProperty(post.id, 'title', value);
}}
/>
)}
{post && post.subTitle && (
<PostEditor
value={post.subTitle}
onChange={value => {
console.log('update subTitle->', value);
updatePostProperty(post.id, 'subTitle', value);
}}
/>
)}
</>
);
};
export default function App() {
const [posts, setPosts] = useState([
{ id: 1, title: 'post no 1', subTitle: 'subtitle no 1' },
{ id: 2, title: 'post no 2', subTitle: 'subtitle no 2' },
{ id: 3, title: 'post no 3', subTitle: 'subtitle no 3' }
]);
const [post, setPost] = useState();
const updatePostProperty = (id, property, value) => {
const newPosts = [...posts];
const index = newPosts.findIndex(post => (post.id === id));
newPosts[index] = {
...newPosts[index],
[property]: value
};
setPosts(newPosts);
};
return (
<div>
<ul>
{posts &&
posts.length > 0 &&
posts.map((post, index) => (
<li
style={{ cursor: 'pointer' }}
onClick={() => {
setPost(post);
}}
>
{post.title} - {post.subTitle}
</li>
))}
</ul>
<Wrapper post={post} updatePostProperty={updatePostProperty} />
</div>
);
}
The App component hosts the updatePostProperty that is passed on to the Wrapper component which uses it when the PostEditor component triggers onChange the CodeEditor which is a wrapper for CodeMirror.
The issue here is that after you click one of the posts and edit the title and then the subtitle, the title gets reverted to the initial value.
Scenario:
Click on the first post and try to edit the title. Add an ! to the title. You'll see the post on the list gets updated.
After you edit the subtitle by adding a character to it, you'll see the title gets reverted to the previous state (without the !) in the App component.
Why is react doing this "revert" update?
Update:
New StackBlitz.
I made a few adjustments to the script to use useEffect before changing the original posts array.
I added a regular input element to see if the problem persists. It seems that the issue is fixed with regular inputs.
However, I'd love someone's input on why does the issue still persists with the way CodeMirror is wired up.
Inside updatePostProperty you're updating the wrong object.
You're updating:
posts[index] = {
...newPosts[index],
[property]: value
};
But you want to update newPosts instead, so you have to do:
newPosts[index] = {
...newPosts[index],
[property]: value
};
it needs to be newPosts instead of posts
no need to destructuring
you are using 1 = here newPosts.findIndex(post => (post.id = id)); there suppose to be 2 == like newPosts.findIndex(post => (post.id == id));
checkout this code
const updatePostProperty = (id, property, value) => {
const newPosts = [...posts];
const index = newPosts.findIndex(post => (post.id == id));
newPosts[index][property] = value
setPosts(newPosts);
};

Displaying one List at a time on Clicking button

I have a web page where i am displaying list of records using React JS table and each row has a button that shows user a list of action item.If the user clicks on a button in a row and clicks on another button in another row the previous list should hide but both the list are displaying.How to solve this?
const dataset = [
{"id" : 1, "buttons" : (<ActionItemList />)},
{"id" : 2, "buttons" : (<ActionItemList />)}
]
<ReactTable data={dataset} />
const ActionItemList = () => {
return (
<div>
<button>...</button>
</div>
<div>
<ul>
<li>action-item1</li>
<li>action-item2</li>
<li>action-item3</li>
</ul>
/>
</div>
</div>
)
}
If you can use hooks you can have your menu set the openId with a method passed by a wrapper. Here is an example of how you can do it:
import React, { useState, memo, useMemo } from 'react';
import ReactTable from 'react-table';
//make ActionItemList a pure component using React.memo
const ActionItemList = memo(
({ isOpen, setOpenId, id }) =>
console.log('in render', id) || (
<button
onClick={() =>
isOpen ? setOpenId() : setOpenId(id)
}
>
{isOpen ? 'Close' : 'Open'} Menu
</button>
)
);
const dataset = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
];
const columns = [
{ id: 1, accessor: 'id' },
{
id: 2,
//if accessor is a function table will render what function returns
// passing in the data where we added isOpen and setOpenId
accessor: ({ Menu, ...props }) => (
//passing in the needed props to pure component Menu
<Menu {...props} />
),
},
];
//wrap this on your react table, adding isOpen and setOpenId
export default () => {
//set state for open id
const [openId, setOpenId] = useState();
//memoize creating the data prop based on openId changing
const dataWithProps = useMemo(
() =>
dataset.map(item => ({
...item,
isOpen: item.id === openId,
setOpenId,
Menu: ActionItemList,
})),
[openId]
);
//return the react table with the extra props in data
return (
<ReactTable data={dataWithProps} columns={columns} />
);
};

Categories

Resources