Recursive component does not load the child in React - javascript

I have the following component to display a set of comments (and their replies). If a comment has a non-empty childPosts array, then the component is again called with the ids of the childPosts. All comments are accessible from redux store through props.discussionPosts.
import * as React from 'react';
import { useState } from 'react';
import { connect } from 'react-redux';
const CommentItem = (props) => {
return (
<div>
{
props.commentIds && props.discussionPosts &&
props.commentIds.map(commentId => {
console.log(props.discussionPosts[commentId].childPosts)
var ident = 10;
return (
<div key={commentId} className="comment">
<div>{props.discussionPosts[commentId].content}</div>
{
props.discussionPosts[commentId].childPosts.length > 0 &&
<div style={{ marginLeft: '10px'}}>
<CommentItem
key={commentId}
commentIds={props.discussionPosts[commentId].childPosts}
/>
</div>
}
</div>
)
})
}
</div>
)
}
const mapStateToProps = (state) => ({
discussionPosts: state.alignPerspectivesReducer.discussionPosts,
error: state.alignPerspectivesReducer.error,
loading: state.alignPerspectivesReducer.loading
})
export default connect(mapStateToProps)(CommentItem);
I place this component in the main page with the following code, where the commentIds is set to the ids of the comments with no parent comment.
<CommentItem
commentIds={props.discussionPosts.filter(function (x) {
return props.discussionPosts[x].parentPostId === null;
})}
/>
However, the code only displays the top level comments with no parent comment:
Comment 1
Comment 2
What I want to achieve is this:
Comment 1
Reply to c#1
Comment 2
Reply to c#2
But, for some reason the following code inside the CommentItem component is not producing them:
<CommentItem
key={commentId}
commentIds={props.discussionPosts[commentId].childPosts}
/>
I am able to print the childPosts values correctly in the console. Below in the image you can see how the discussionPosts data looks like:
I am not receiving any error, but this recursive component does not work properly. Any help?

I don't have enough knowledge about redux but what if you create a component with connect and render it?
const CommentItem = (props) => {
return (
<div>
{
props.commentIds && props.discussionPosts &&
props.commentIds.map(commentId => {
console.log(props.discussionPosts[commentId].childPosts)
var ident = 10;
return (
<div key={commentId} className="comment">
<div>{props.discussionPosts[commentId].content}</div>
{
props.discussionPosts[commentId].childPosts.length > 0 &&
<div style={{ marginLeft: '10px'}}>
{// render connected component}
<ConnectedCommentItem
key={commentId}
commentIds={props.discussionPosts[commentId].childPosts}
/>
</div>
}
</div>
)
})
}
</div>
)
}
const mapStateToProps = (state) => ({
discussionPosts: state.alignPerspectivesReducer.discussionPosts,
error: state.alignPerspectivesReducer.error,
loading: state.alignPerspectivesReducer.loading
})
// create connected component
const ConnectedCommentItem = connect(mapStateToProps)(CommentItem);
export default ConnectedCommentItem;

It looks like your childPosts might be an object rather than an array. If the data is structured like this:
childPosts: {
0: 2
}
then childPosts.length will be undefined and your recursive element will never be rendered.
What you want is an array:
childPosts: [ 2 ]
then your code should work as written.

Use hasChildPosts instead of props.discussionPosts[commentId].childPosts.length > 0

Related

How to test this component staying aligned with React Testing Library Principles?

I am testing a component and wondering what the best approach would be that would test the component without testing implementation details.
The component renders conditionally based on the width and isDropdown state, but not sure how to test how the DOM would look by changing these values or even if that would be the correct approach.
Here is the component:
import { useState } from 'react'
import useWindowDimension from '../../hooks/useWindowDimensions'
import { QueryType } from '../../constants'
type choiceProps = {
type: QueryType
isChosen: boolean
setFeedChoice: (query: QueryType) => void
}
const FeedChoice = ({ type, isChosen, setFeedChoice }: choiceProps) => {
return (
<div
className={isChosen ? 'feed-choice chosen' : 'feed-choice'}
onClick={() => setFeedChoice(type)}
>
<img
src={require(`../../../public/images/${type}${
isChosen ? '-fill' : ''
}.svg`)}
/>
<span>{type.charAt(0).toUpperCase() + type.slice(1)}</span>
</div>
)
}
type Props = {
feedChoice: QueryType
setFeedChoice: (query: QueryType) => void
}
const Feed = ({ feedChoice, setFeedChoice }: Props) => {
const { width } = useWindowDimension()
const [isDropdown, setDropdown] = useState<boolean>(false)
const setChoice = (query: QueryType) => {
setFeedChoice(query)
setDropdown(false)
}
if (width !== null && width <= 600) {
return (
<div className="feed-row">
{isDropdown ? (
<div>
<FeedChoice
type={QueryType.New}
isChosen={feedChoice === QueryType.New}
setFeedChoice={setChoice}
/>
<FeedChoice
type={QueryType.Boost}
isChosen={feedChoice === QueryType.Boost}
setFeedChoice={setChoice}
/>
<FeedChoice
type={QueryType.Comments}
isChosen={feedChoice === QueryType.Comments}
setFeedChoice={setChoice}
/>
<FeedChoice
type={QueryType.Squash}
isChosen={feedChoice === QueryType.Squash}
setFeedChoice={setChoice}
/>
</div>
) : (
<FeedChoice
type={feedChoice}
isChosen={true}
setFeedChoice={() => setDropdown(true)}
/>
)}
</div>
)
} else {
return (
<div className="feed-row">
<FeedChoice
type={QueryType.New}
isChosen={feedChoice === QueryType.New}
setFeedChoice={setFeedChoice}
/>
<div className="divider"></div>
<FeedChoice
type={QueryType.Boost}
isChosen={feedChoice === QueryType.Boost}
setFeedChoice={setFeedChoice}
/>
<div className="divider"></div>
<FeedChoice
type={QueryType.Comments}
isChosen={feedChoice === QueryType.Comments}
setFeedChoice={setFeedChoice}
/>
<div className="divider"></div>
<FeedChoice
type={QueryType.Squash}
isChosen={feedChoice === QueryType.Squash}
setFeedChoice={setFeedChoice}
/>
</div>
)
}
}
export default Feed
Here is my initial test that is only testing if the text properly appears in the DOM, which it does:
import React from 'react'
import { screen, render } from '#testing-library/react'
import Feed from '../components/feed/feed'
import useWindowDimensions from '../hooks/useWindowDimensions'
test('should render all of the QueryTypes in the Feed', () => {
render(<Feed />)
expect(screen.getByText(/new/i)).toBeInTheDocument()
expect(screen.getByText(/boost/i)).toBeInTheDocument()
expect(screen.getByText(/comments/i)).toBeInTheDocument()
expect(screen.getByText(/squash/i)).toBeInTheDocument()
})
Thanks!
I would write tests for each set of props for Feed, and one test mocking the setFeedChoice props and asserting on its calls :
const setFeedChoiceMock = jest.fn();
test('calls callback correctly', () => {
render(<Feed feedChoice=... setFeedChoice={setFeedChoiceMock} />)
// click on whatever makes the drop down open
userEvent.click(...);
// click on one option
userEvent.click(screen.getByText(...));
// check that the callback was called
expect(setFeedChoiceMock).toHaveBeenCalledWith("expected parameter, option text...");
});
// write other tests changing the feedChoice props to ensure all cases work well
test('respects feedChoice props', () => {
render(<Feed feedChoice="other value" setFeedChoice={setFeedChoiceMock} />)
// check that feedChoice is correctly used
expect(screen.getByText(...)).toBeInTheDocument()
});
Therefore you are staging the Feed component, and you test how it behaves regarding only inputs (props) and outputs (DOM generated, dropdown opening, texts, callback calls). You don't test internal implementation, only what's in and out.
The difficult part may be to interact correctly with the dropdown and make it open/close correctly. You may have to use waitFor, or replace userEvent by fireEvent (in my experience sometimes fireEvent is more reliable - but less realistic). See React Testing Library: When to use userEvent.click and when to use fireEvent on this subject.

React: Persisting State Using Local Storage

I am coding an app in which there is a collection of reviews and a person can respond to a review, but each review can only have one response. So far, I am doing this by rendering a ReviewResponseBox component in my ReviewCardDetails component and passing the review_id as props.
I have implemented the logic so that once there is one ReviewResponse, the form to write another will no longer appear. However, before I was initializing the state in this component with an empty array, so when I refreshed my page the response went away and the form came back up. (This is now commented out)
I am trying to resolve this by persisting my state using React LocalStorage but am having trouble writing my method to do this. Here is what I have so far:
Component that renders ReviewResponseBox and passes review_id as props:
import React from "react";
import './Review.css';
import { useLocation } from "react-router-dom";
import StarRatings from "react-star-ratings";
import ReviewResponseBox from "../ReviewResponse/ReviewResponseBox";
const ReviewCardDetails = () => {
const location = useLocation();
const { review } = location?.state; // ? - optional chaining
console.log("history location details: ", location);
return (
<div key={review.id} className="card-deck">
<div className="card">
<div>
<div className='card-container'>
<h4 className="card-title">{review.place}</h4>
<StarRatings
rating={review.rating}
starRatedColor="gold"
starDimension="20px"
/>
<div className="card-body">{review.content}</div>
<div className="card-footer">
{review.author} - {review.published_at}
</div>
</div>
</div>
</div>
<br></br>
<ReviewResponseBox review_id={review.id}/>
</div>
);
};
export default ReviewCardDetails;
component that I want to keep track of the state so that it can render the form or response:
import React from 'react';
import ReviewResponse from './ReviewResponse';
import ReviewResponseForm from './ReviewResponseForm';
import { reactLocalStorage } from "reactjs-localstorage";
class ReviewResponseBox extends React.Component {
// constructor() {
// super()
// this.state = {
// reviewResponses: []
// };
// }
fetchResponses = () => {
let reviewResponses = [];
localStorage.setResponses
reviewResponses.push(reviewResponse);
}
render () {
const reviewResponses = this.getResponses();
const reviewResponseNodes = <div className="reviewResponse-list">{reviewResponses}</div>;
return(
<div className="reviewResponse-box">
{reviewResponses.length
? (
<>
{reviewResponseNodes}
</>
)
: (
<ReviewResponseForm addResponse={this.addResponse.bind(this)}/>
)}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({ reviewResponses: this.state.reviewResponses.concat([reviewResponse]) }); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id} />
);
});
}
}
export default ReviewResponseBox;
Any guidance would be appreciated
You would persist the responses to localStorage when they are updated in state using the componentDidUpdate lifecycle method. Use the componentDidMount lifecycle method to read in the localStorage value and set the local component state, or since reading from localStorage is synchronous directly set the initial state.
I don't think you need a separate package to handle this either, you can use the localStorage API easily.
import React from "react";
import ReviewResponse from "./ReviewResponse";
import ReviewResponseForm from "./ReviewResponseForm";
class ReviewResponseBox extends React.Component {
state = {
reviewResponses: JSON.parse(localStorage.getItem(`reviewResponses-${this.props.review_id}`)) || []
};
storageKey = () => `reviewResponses-${this.props.review_id}`;
componentDidUpdate(prevProps, prevState) {
if (prevState.reviewResponses !== this.state.reviewResponses) {
localStorage.setItem(
`reviewResponses-${this.props.review_id}`,
JSON.stringify(this.state.reviewResponses)
);
}
}
render() {
const reviewResponses = this.getResponses();
const reviewResponseNodes = (
<div className="reviewResponse-list">{reviewResponses}</div>
);
return (
<div className="reviewResponse-box">
{reviewResponses.length ? (
<>{reviewResponseNodes}</>
) : (
<ReviewResponseForm addResponse={this.addResponse.bind(this)} />
)}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({
reviewResponses: this.state.reviewResponses.concat([reviewResponse])
}); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id}
/>
);
});
}
}

How to use forEach in react js

I want to create a function which iterate over all element with same class and remove a specific class.
It could be done easily using JavaScript.
const boxes = document.querySelectorAll(".box1");
function remove_all_active_list() {
boxes.forEach((element) => element.classList.remove('active'));
}
But how can I do this similar thing is ReactJs. The problem which I am facing is that I can't use document.querySelectorAll(".box1") in React but, I can use React.createRef() but it is not giving me all elements, it's only giving me the last element.
This is my React Code
App.js
import React, { Component } from 'react';
import List from './List';
export class App extends Component {
componentDidMount() {
window.addEventListener('keydown', this.keypressed);
}
keypressed = (e) => {
if (e.keyCode == '38' || e.keyCode == '40') this.remove_all_active_list();
};
remove_all_active_list = () => {
// boxes.forEach((element) => element.classList.remove('active'));
};
divElement = (el) => {
console.log(el);
el.forEach((element) => element.classList.add('active'))
};
render() {
return (
<div className="container0">
<List divElement={this.divElement} />
</div>
);
}
}
export default App;
List.js
import React, { Component } from 'react';
import data from './content/data';
export class List extends Component {
divRef = React.createRef();
componentDidMount() {
this.props.divElement(this.divRef)
}
render() {
let listItem = data.map(({ title, src }, i) => {
return (
<div className="box1" id={i} ref={this.divRef} key={src}>
<img src={src} title={title} align="center" alt={title} />
<span>{title}</span>
</div>
);
});
return <div className="container1">{listItem}</div>;
}
}
export default List;
Please tell me how can I over come this problem.
The short answer
You wouldn't.
Instead you would conditionally add and remove the class to the element, the component, or to the collection.map() inside your React component.
Example
Here's an example that illustrates both:
import styles from './Example.module.css';
const Example = () => {
const myCondition = true;
const myCollection = [1, 2, 3];
return (
<div>
<div className={myCondition ? 'someGlobalClassName' : undefined}>Single element</div>
{myCollection.map((member) => (
<div key={member} className={myCondition ? styles.variant1 : styles.variant2}>
{member}
</div>
))}
</div>
);
};
export default Example;
So in your case:
You could pass active prop to the <ListItem /> component and use props.active as the condition.
Alternatively you could send activeIndex to <List /> component and use index === activeIndex as the condition in your map.
Explanation
Instead of adding or removing classes to a HTMLElement react takes care of rendering and updating the whole element and all its properties (including class - which in react you would write as className).
Without going into shadow dom and why react may be preferable, I'll just try to explain the shift in mindset:
Components do not only describe html elements, but may also contain logic and behaviour. Every time any property changes, at the very least the render method is called again, and the element is replaced by the new element (i.e. before without any class but now with a class).
Now it is much easier to change classes around. All you need to do is change a property or modify the result of a condition (if statement).
So instead of selecting some elements in the dom and applying some logic them, you would not select any element at all; the logic is written right inside the react component, close to the part that does the actual rendering.
Further reading
https://reactjs.org/docs/state-and-lifecycle.html
Please don't hessitate to add a comment if something should be rephrased or added.
pass the ref to the parent div in List component.
...
componentDidMount() {
this.props.divElement(this.divRef.current)
}
...
<div ref={this.divRef} className="container1">{listItem}</div>
then in App
divElement = (el) => {
console.log(el);
el.childNodes.forEach((element) => element.classList.add('active'))
}
hope this will work. here is a simple example
https://codesandbox.io/s/staging-microservice-0574t?file=/src/App.js
App.js
import React, { Component } from "react";
import List from "./List";
import "./styles.css";
export class App extends Component {
state = { element: [] };
ref = React.createRef();
componentDidMount() {
const {
current: { divRef = [] }
} = this.ref;
divRef.forEach((ele) => ele?.classList?.add("active"));
console.log(divRef);
window.addEventListener("keydown", this.keypressed);
}
keypressed = (e) => {
if (e.keyCode == "38" || e.keyCode == "40") this.remove_all_active_list();
};
remove_all_active_list = () => {
const {
current: { divRef = [] }
} = this.ref;
divRef.forEach((ele) => ele?.classList?.remove("active"));
// boxes.forEach((element) => element.classList.remove('active'));
console.log(divRef);
};
render() {
return (
<div className="container0">
<List divElement={this.divElement} ref={this.ref} />
</div>
);
}
}
export default App;
List.js
import React, { Component } from "react";
import data from "./data";
export class List extends Component {
// divRef = React.createRef();
divRef = [];
render() {
let listItem = data.map(({ title, src }, i) => {
return (
<div
className="box1"
key={i}
id={i}
ref={(element) => (this.divRef[i] = element)}
>
<img src={src} title={title} align="center" alt={title} width={100} />
<span>{title}</span>
</div>
);
});
return <div className="container1">{listItem}</div>;
}
}
export default List;
Create ref for List component and access their child elements. When key pressed(up/down arrow) the elements which has classname as 'active' will get removed. reference

TypeError: this.state.userInfo.map is not a function

Sorry, I'm kinda new to react ,why I'm not being able to map through the data.
I have tried a different couple of things but nothing has helped.
Maybe the reason is that it's an object.
Can any one help?
import React, { Component } from "react";
import axios from "axios";
import "./Profile.css";
import ProfileCard from "../ProfileCard/ProfileCard";
class Profile extends Component {
state = {
userInfo: {}
};
componentDidMount() {
const { id } = this.props.match.params;
axios
.get(`/api/user/info/${id}`)
.then(
response => this.setState({ userInfo: { ...response.data, id } }),
() => console.log(this.state.userInfo)
);
}
render() {
let userInfoList= this.state.userInfo.map((elem,i)=>{
return(
<div> name={elem.name}
id={elem.id}</div>
)
})
console.log(this.state.userInfo);
return (
<div>
{/* <p>{this.state.userInfo}</p> */}
{/* <div >{userInfoList}</div>
<ProfileCard profilePic={this.state.userInfo} /> */}
</div>
);
}
}
export default Profile;
I think I understand what youre trying to do.
First you should change userInfo to an empty array instead of an empty object as others have stated.
Next since you are making an async api call you should use a ternary expression in your render method, because currently React will just render the empty object without waiting for the api call to complete. I would get rid of the userInfoList variable and refactor your code to the following:
RenderProfile = (props) => (
<div>
{props.elem.name}
</div>
)
{ this.state.userInfo
? this.state.userInfo.map(elem => < this.RenderProfile id={elem.id} elem={elem} /> )
: null
}
Let me know if it worked for you.

Why I get props is undefined?

import React from "react";
import styles from "../articles.css";
const TeamInfo = props => (
<div className={styles.articleTeamHeader}>
<div className={styles.left}>
style={{
background: `url('/images/teams/${props.team.logo}')`
}}
</div>
<div className={styles.right}>
<div>
<span>
{props.team.city} {props.team.name}
</span>
</div>
<div>
<strong>
W{props.team.stats[0].wins}-L{props.team.stats[0].defeats}
</strong>
</div>
</div>
</div>
);
export default TeamInfo;
the code that render this
import React from 'react';
import TeamInfo from '../../Elements/TeamInfo';
const header = (props) => {
const teaminfofunc = (team) => {
return team ? (
<TeamInfo team={team}/>
) : null
}
return (
<div>
{teaminfofunc(props.teamdata)}
</div>
)
}
export default header;
and I am getting error TypeError: props is undefined in line 8 why is that ?
Line 8 is
background: url('/images/teams/${props.team.logo}')
Update:
I found that in index.js the componentWillMount bring the data correctly but in the render() those data (article and team) was not passed to render, any idea why ?
import React, {Component} from 'react';
import axios from 'axios';
import {URL} from "../../../../config";
import styles from '../../articles.css';
import Header from './header';
import Body from './body';
class NewsArticles extends Component {
state = {
article:[],
team: []
}
componentWillMount() {
axios.get(`${URL}/articles?id=${this.props.match.params.id}`)
.then(response => {
let article = response.data[0];
axios.get(`${URL}/teams?id=${article.team}`)
.then(response => {
this.props.setState({
article,
team:response.data
})
})
})
}
render() {
const article = this.state.article;
const team = this.state.team;
return (
<div className={styles.articleWrapper}>
<Header teamdata={team[0]} date={article.date} author={article.author} />
<Body />
</div>
)
}
}
export default NewsArticles;
You render your component immediately, long before your AJAX call finishes, and pass it the first element of an empty array:
<Header teamdata={team[0]}
componentWillMount does not block rendering. In your render function, short circuit if there's no team to render.
render() {
const { article, team, } = this.state;
if(!team || !team.length) {
// You can return a loading indicator, or null here to show nothing
return (<div>loading</div>);
}
return (
<div className={styles.articleWrapper}>
<Header teamdata={team[0]} date={article.date} author={article.author} />
<Body />
</div>
)
}
You're also calling this.props.setState, which is probably erroring, and you should never call setState on a different component in React. You probably want this.setState
You should always gate any object traversal in case the component renders without the data.
{props && props.team && props.team.logo ? <div className={styles.left}>
style={{
background: `url('/images/teams/${props.team.logo}')`
}}
</div> : null}
This may not be you exact issue, but without knowing how the prop is rendered that is all we can do from this side of the code.
Update based on your edit. You can't be sure that props.teamdata exists, and therefore your component will be rendered without this data. You'll need to gate this side also, and you don't need to seperate it as a function, also. Here is an example of what it could look like:
import React from 'react';
import TeamInfo from '../../Elements/TeamInfo';
const header = (props) => (
<div>
{props.teamdata ? <TeamInfo team={props.teamdata}/> : null}
</div>
)
export default header;
First -- while this is stylistic -- it's not good practice to pass props directly to your functional component. Do this instead.
const TeamInfo = ({team}) => (
<div className={styles.articleTeamHeader}>
<div className={styles.left}>
style={{
background: `url('/images/teams/${team.logo}')`
}}
</div>
<div className={styles.right}>
<div>
<span>
{team.city} {team.name}
</span>
</div>
<div>
<strong>
W{team.stats[0].wins}-L{team.stats[0].defeats}
</strong>
</div>
</div>
</div>
);
Second, you might just want to do some kind of null check. If team is undefined the first time the component tries to render, you might just want to render null so you're not wasting cycles.
In case this isn't the issue, you'd learn a lot by console.log-ing your props so you know what everything is each time your component tries to render. It's okay if data is undefined if you're in a state that will soon resolve.

Categories

Resources