I'm following some code from a guide and I'm getting that error.
In my App.js I have:
deleteComments = async (commentId) => {
try {
await axios.delete(`/comments/${commentId}`);
const comments = await this.getComments();
this.setState({ comments });
} catch (error) {
console.log(error);
}
};
CommentsList component
const CommentsList = (props) => {
const comments = props.comments.map((comment) => {
return (
<Comment
{...comment}
deleteComments={props.deleteComments}
key={comment.id}
/>
);
});
Comment component where I call the function.
import React from 'react';
const Comment = (props) => {
const deleteComments = () => {
props.deleteComments(props.id);
};
return (
<div>
...
<div>
<button onClick={deleteComments}>Delete</button>
</div>
</div>
);
};
It's quite obvious that CommentList doesn't get deleteComments prop passed. You need to pass it:
<CommentList deleteComments={deleteComments} />
Related
I have this component in my React project -
const ViewPost = (props: Props) => {
const [listingData, setListingData] = useState<any>({})
const [auctionData, setAuctionData] = useState<any>({})
useEffect(() => {
if (props.listingId) {
getListingData()
}
}, [props.listingId])
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
setListingData(data?.data)
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
const auctions = async (auctionId: any) => {
const auction = await getAuctions(auctionId)
console.log('auction', auction.data)
setAuctionData(auction.data)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}
export default ViewPost
Basically, I'm getting data from an API and assigning it to auctionData.
console.log(auction.data) shows me the desired result but when I pass auctionData as props into Details I get an empty object which leads to a lot of issues, since useState is async.
How can I overcome this problem?
const [auctionData, setAuctionData] = useState<any>({})
your default value is an empty object, that causes the problems.
should set null or undefined as default value, and hide the Details when not have the data.
Use loading state. Once data is fully fetched from api then pass to child component. I think what is happeing here is that child component is called with empty state variable while data is still being fetched.
const [isLoading, setIsLoading] = useState(true)
const getListingData = async () => {
const { data } = await getListingById(props.listingId)
.then((data) => {setListingData(data)})
.then((data) => {
setTimeout(() => {
setIsLoading(false)
}, 1000)
})
if (data.data.isTimedAuction) {
auctions(data.data.auctionId)
}
}
and then return
if (isLoading) {
return (
<div>
Loading...
</div>
)
}
return (
<>
<Navbar />
<div className={classes.viewPostPage}>
<div className={classes.bodyContainer}>
<Details
data={listingData as any}
updateListing={getListingData}
auctionData={auctionData}
/>
</div>
</div>
</>
)
}
I'm trying to simulate the 'see more' functionality to a blog.
It works as expected on the browser but when I simulate the behavior on react testing library it doesn't.
describe('when 12 blogs', () => {
describe('fetch more blogs', () => {
beforeEach(() => {
const twelveBlogs = generateBlogs(12);
const twoBlogs = generateBlogs(10);
Api.query.mockReturnValueOnce(twelveBlogs);
Api.query.mockReturnValueOnce(twoBlogs);
});
test('should fetch more blog posts when clicking on "See More" button', async () => {
render(
<MemoryRouter>
<Blog />
</MemoryRouter>
);
const seeMoreButton = await screen.findByRole('button', {
name: /See More/i,
});
fireEvent.click(seeMoreButton);
await waitFor(() => expect(Api.query).toHaveBeenCalledTimes(2));
await waitFor(
() =>
expect(screen.getAllByText(/NaN de undefined de NaN/)).toHaveLength(
15
)
);
});
});
});
And the implementation
import React from 'react';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import Api from '../../api/api';
import BlogPreview from '../../components/BlogPreview/BlogPreview';
import './Blog.css';
function Blog() {
const [blogPosts, setBlogPosts] = useState([]);
const pageSize = 12;
const category = ['document.type', 'blog'];
const orderings = '[my.blog.data desc]';
const [apiPage, setApiPage] = useState(1);
const [shouldFetchMoreBlogs, setShouldFetchMoreBlogs] = useState(true);
useEffect(() => {
async function fetchApi(options) {
return Api.query(category, options);
}
const options = { pageSize, page: apiPage, orderings };
fetchApi(options).then((response) => {
if (response?.length > 0) {
if (blogPosts.length !== 0) {
setBlogPosts([...blogPosts, response]);
} else {
setBlogPosts(response);
}
} else {
setShouldFetchMoreBlogs(false);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiPage]);
async function handleSeeMoreClick() {
setApiPage(apiPage + 1);
}
function renderBlogPosts() {
if (blogPosts.length > 0) {
return blogPosts.map((blog, index) => (
<Link to={{ pathname: `/blog/${blog.uid}`, ...blog }} key={index}>
<BlogPreview key={index} {...blog} />
</Link>
));
}
}
function renderSeeMoreButton() {
debugger;
if (blogPosts.length > 0) {
if (blogPosts?.length % 12 === 0 && shouldFetchMoreBlogs) {
return (
<div className="see-more-container">
<button className="see-more-button" onClick={handleSeeMoreClick}>
Veja Mais
</button>
</div>
);
}
}
}
return (
<section className="content blog">
<h1>BLOG</h1>
<div className="blog-posts">{renderBlogPosts()}</div>
{renderSeeMoreButton()}
</section>
);
}
export default Blog;
It fails 'cause it only finds the initial 12 blog posts, even though it shows that the api was called twice.
There's obviously some async issue here.
I've tried switching from fireEvent to userEvent, from waitFor to find*, but it still doesn't work.
Thanks
I have my InputLocation Component with children:
InputName
DrawLocation
I want to make some unit test to check my component, but I have a problem with simulating componentDidMount cycle.
InputName component depends on and displays when DrawLocation is mounted. See below please:
input-location.js
class InputLocation extends React.Component {
state = {
isMapReady: false
}
setMapReady = () => this.setState({ isMapReady: true })
onScrollTop = () => {
const { handlers } = this.props
handlers.onScrollTop && handlers.onScrollTop()
}
render() {
const {
autoFocus, captionTip, children, id, handlers, trackEvents,
mapId, value, coordinates, inputNameErrorMessage, isNameError
} = this.props
const { isMapReady } = this.state
return (
<div className='input-location'>
<div className='input-location__module'>
{!!isMapReady && (
<InputName
autoFocus={autoFocus}
caption={i18n.t('INPUT_LOCATION.NAME_LABEL')}
captionTip={captionTip}
id={id}
isError={isNameError}
handlers={handlers}
placeholder={i18n.t('INPUT_LOCATION.NAME_PLACEHOLDER')}
requiredMessage={inputNameErrorMessage}
value={value}
/>
)}
</div>
{children}
<div className='input-location__module'>
<DrawLocation
coordinates={coordinates}
mapId={mapId}
handlers={{
...handlers,
onMount: callAll(
this.setMapReady,
this.onScrollTop
)
}}
trackEvents={trackEvents}
/>
</div>
</div>
)
}
}
And a small piece of DrawLocation component to show where is onMount handlers there.
draw-location.js
class DrawLocation extends React.PureComponent {
componentDidMount() {
const { handlers } = this.props
handlers.onMount && handlers.onMount()
}
...
render() {
return (
<div>
Something...
</div>
)
}
}
I've started to create some test using nwb and expect
input-location-test.js
import expect from 'expect'
const containerPath = '.input-location'
const inputNamePath = '.input-location__name'
const errorPath = '.validation'
const injectInputLocation = require('inject-loader!./input-location')
const InputLocation = injectInputLocation({
'./input-name': () => <div className='input-location__name'></div>,
'./tip': () => <div />,
'cm/components/map/draw-location': ({ handlers, children }) => (
<div className="map__container" handlers={handlers.onMount}>
{children}
</div>
)
}).default
describe('InputLocation component', () => {
let node
beforeEach(() => {
node = document.createElement('div')
})
afterEach(() => {
ReactDOM.unmountComponentAtNode(node)
})
it('renders component', () => {
ReactDOM.render(
<InputLocation />,
node
)
const container = node.querySelector(containerPath)
expect(container).toExist()
})
it('doesn\'t render input-name if no properties specified', () => {
ReactDOM.render(
<InputLocation />,
node
)
const elementNode = node.querySelector(inputNamePath)
expect(elementNode).toNotExist()
})
it('renders input-name', () => {
ReactDOM.render(
<InputLocation
handlers={{
onMount: () => {}
}}
/>,
node
)
const elementNode = node.querySelector(inputNamePath)
expect(elementNode).toExist()
})
})
First and second test is OK and give me positive results, but the last one failed.
Please about your help to solve the issue.
I am trying to make a rest call, edit the data and then render it. The problem is that, while editing, I am getting a error - undefined even after checking if the data is there.
component I am making the rest call from:
function Header ({timesheetData, loadTimesheet}) {
useEffect(() => {
loadTimesheet(date)
}, [])
return(
<>
<header className="header">
<div className="wrap">
<span className="btn-icon">
<IconPlus onClick={() => setIsOpen(true)} className="icon icon-plus js-modal-init"/>
</span>
<div className="header-blockquote">
<h1 className="header-quote">{currentQuote.quote}</h1>
<div className="header-cite">{currentQuote.author}</div>
</div>
</div>
<div className="header-inner">
<div className="wrap">
<VegaIcon className="logo" alt="VegaIT"/>
<div className="date-wrap">
<IconCalendar className="icon icon-plus js-modal-init"/>
//
<time>{timesheetData.timesheet.loading ? "asd" : edit(timesheetData) }</time>
//
</div>
</div>
</div>
</header>
</>
)
}
function edit(timesheetData){
let newDate = timesheetData.timesheet.date
newDate = newDate.split("-")
newDate = newDate.reverse()
return newDate.join("/")
}
the redux action:
export const loadTimesheet = (date) => {
let url = "http://localhost:8080/api/timesheet/" + date
return (dispatch) => {
dispatch(getTimesheet)
axios.get(url)
.then((response) => {
const timesheet = response.data
dispatch(getTimesheetSuccess(timesheet))
})
.catch(error => {
const errorMsg = error.message
dispatch(getTimesheetFailure)
})
}
}
Edit: added my mapStateToProps and mapDispatchToProps
const mapStateToProps = state => {
return {
timesheetData: state.timesheet
}
}
const mapDispatchToProps = dispatch => {
return {
loadTimesheet: (date) => dispatch(loadTimesheet(date))
}
}
Edit2: The code: https://codesandbox.io/s/adoring-tharp-o9ibe
use mapStateToProps, mapDispatchToProps:
import getTimesheet from '../actions/...'
Header ({timesheetData, loadTimesheet}) => {
}
const mapDispatchToProps = dispatch => {
return {
loadTimesheet: () => dispatch(getTimesheet()),
}
}
const mapStateToProps = (state) => {
return { timesheetData: state.timesheetData };
};
export default connect(mapStateToProps, mapDispatchToProps)(Header);
also in component:
before render:
check that you have:
timesheetData - not undefined
timesheetData.timesheet - not undefined
timesheetData.timesheet.loading - not undefined
const hasData = timesheetData && !timesheetData.timesheet.loading;
const time = hasData ? edit(timesheetData): "asd";
in render:
<time>{time}</time>
I am trying to to an asynchronous fetch call once my modal is opened. I need that call because It fetched images and it will take around 5 seconds for the fetch to get a response.
So the modal should show first with the data and then once the fetch is complete, it should show the fetch data also.
My problem is that at the moment when calling the function with this () => {this.fetchImages(id) it is not called. I assume it's because the function is being assigned and not called.
But when I call the function fetchImages() without the () =>, I get this error :
Invariant Violation: Minified React error #31
This is my code, irrelevant part has been removed for simplicity:
renderModal = () => {
...
return (
<Modal open={openModal != null} onClose={this.closeModal}
little showCloseIcon={true} styles={modalStyles}>
<div className="item">
...
{() => {this.fetchImages(id)
.then(r => console.log("Fetch result" + r))}}
</div>
</Modal>
);
}
fetchImages = async (id) =>{
console.log("Request started")
try{
let myImages = null;
if(typeof(id) !== 'undefined' && id != null) {
myImages = await fetchImages(id);
console.log("Images: " + myImages);
return (
<div>
{Array.isArray(myImages) && myImages.length !== 0 && myImages.map((item, key) =>
<p>Image name: {item.name}, En device name: {item.name.en_US}</p>
)}
</div>
);
}
} catch (e) {
console.log("Failed")
}
}
EDIT
By changing the code as suggested by jack.benson and GalAbra, I ran into an issue where I am stuck in an endless loop. I will add the new code:
Once the page loads up the renderModal is called in render() method:
{this.renderModal()}
Then I have a button that would show the modal since modal contains a line :
<Modal open={openModal != null} onClose={this.closeModal}
little showCloseIcon={true} styles={modalStyles}>
It is called from here:
myMethod = (task) => {
...
return (
<div {...attrs}>
...
<button onClick={() => {this.showModal(documents[0])}}>{translate('show_more')} ยป</button>
</div>
}
</div>
</div>
);};
And the part to show the modal:
showModal = (document) => {
this.setState({ modalOpen: document });
};
And now the new renderModal():
renderModal = () => {
const doThing = async () => {
try {
const newImages = await downloadDeviceImages(id);
return { data: newImages };
} catch (e) {
console.log("Failed")
}
};
const test = async (id) => {
const imageData = await doThing(id);
console.log("after", imageData.data);
this.setState({
imageData: imageData.data
});
};
if(typeof(id) !== 'undefined' && id != null) {test(id);}
return (
<Modal open={openModal != null} onClose={this.closeModal}
little showCloseIcon={true} styles={modalStyles}>
<div className="item">
...
<div>
{this.state.imageData.map((item, key) =>
<p>Device name: {item.name}, En device name: {item.name.en_US}</p>
)}
</div>
</Modal>
);
}
The main part here is that the button will be clicked multiple times and it might have different ID on every click so a new request should be made every time.
So, this.fetchImages(id) returns a Promise, which React has no idea how to render. That is why you get the error. When you wrap it in () => { ... } you are actually creating a function that wraps your promise, but you are never calling the function. That is why it is not called. It is like saying:
function aThingToDo() {
console.log('Did thing!')
}
function doThing() {
aThingToDo();
}
Notice that I declared to functions, and neither of them were called. You need to call the function by explicitly adding parentheses after it (like doThing()).
That explains your error and the fact that your function is not called. Now, how to handle it. You want to wrap your fetching in a useEffect (as async stuff during render should be done). Then, if you want the data during render you can set state once it completes which will trigger a re-render. I put together a quick example of what I am talking about:
import React, { useEffect, useState } from "react";
import "./styles.css";
const doThing = async () => {
console.log("doing a thing");
return { data: "My data" };
};
export default function App() {
const [data, setData] = useState("");
useEffect(() => {
const test = async () => {
const data = await doThing();
setData(data);
};
test();
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{data && data.data}
</div>
);
}
Here is a similar example using class components.
import React from "react";
import "./styles.css";
const doThing = async () => {
console.log("doing a thing");
return { message: "New message" };
};
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: "Default message"
};
}
componentDidMount() {
const test = async () => {
const data = await doThing();
console.log("after", data.message);
this.setState({
data: data.message
});
};
test();
}
render() {
console.log("rendering", this.state.data);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{this.state.data}
</div>
);
}
}
Hope that helps!
Currently your code contains an async function mixed up with HTML elements.
Instead you have React's state that'll take care of the re-rendering once the data is fetched:
const ModalWrapper = ({ id }) => {
const [myImages, setMyImages] = React.useState([]);
const fetchImages = async (id) => {
try {
const newImages = await fetchImagesCall(id);
setMyImages(newImages); // Will trigger a re-render of the component, with the new images
} catch (e) {
console.log("Failed")
}
}
React.useEffect(() => {
fetchImages(id);
}, []); // This empty array makes sure the `fetchImages` call will occur only once the component is mounted
return (
<Modal>
<div className="item">
<div>
{myImages.map((item, key) =>
<p>Image name: {item.name}, En device name: {item.name.en_US}</p>
)}
</div>
</div>
</Modal>
);
}