How to create a 'Load More' feature without re-rendering entire component in React/Node? - node.js

I'm trying to create a simple poll app, where you can make new polls.
In the section 'MyPolls', I want it to render only the first 5 polls that I've made instead of rendering the entire list of polls.
At the bottom is a 'Load More' button, where upon clicking, loads another 5 polls and so on.
I've been using Mongoose/MongoDB backend and my approach has been to use skip and limit.
I've managed to implement this feature, but the problem is the entire component re-renders, which is annoying for a user as you have to scroll down again the click the 'Load More' button.
Here is my app: https://voting-app-drhectapus.herokuapp.com/
(use can you these login details for convenience:
username: riverfish#gmail.com
password: 123)
And then goto the My Polls page.
MyPoll.js:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
class MyPolls extends Component {
constructor(props) {
super(props);
this.state = {
skip: 0
};
}
componentDidMount() {
this.props.fetchMyPolls(this.state.skip);
this.setState({ skip: this.state.skip + 5 });
}
sumVotes(polls) {
return polls.reduce((a, b) => {
return a.votes + b.votes;
});
}
loadMore(skip) {
this.props.fetchMyPolls(skip);
const nextSkip = this.state.skip + 5;
this.setState({ skip: nextSkip });
}
renderPolls() {
return this.props.polls.map(poll => {
return (
<div className='card' key={poll._id}>
<div className='card-content'>
<span className='card-title'>{poll.title}</span>
<p>Votes: {this.sumVotes(poll.options)}</p>
</div>
</div>
)
})
}
render() {
console.log('polls', this.props.polls);
console.log('skip:', this.state.skip);
return (
<div>
<h2>My Polls</h2>
{this.renderPolls()}
<a href='#' onClick={() => this.loadMore(this.state.skip)}>Load More</a>
</div>
);
}
}
function mapStateToProps({ polls }) {
return { polls }
}
export default connect(mapStateToProps, actions)(MyPolls);
Action creator:
export const fetchMyPolls = (skip) => async dispatch => {
const res = await axios.get(`/api/mypolls/${skip}`);
dispatch({ type: FETCH_MY_POLLS, payload: res.data });
}
Poll route:
app.get('/api/mypolls/:skip', requireLogin, (req, res) => {
console.log(req.params.skip);
Poll.find({ _user: req.user.id })
.sort({ dateCreated: -1 })
.skip(parseInt(req.params.skip))
.limit(5)
.then(polls => {
res.send(polls);
});
});
Entire github repo: https://github.com/drhectapus/voting-app
I understand that might method of implementing this feature might be the best possible solution so I'm open to any suggestions.

It looks like the re-render is triggered by the fact that clicking the "Load More" link actually causes react router to navigate to a new route, causing the entire MyPolls component to re-render.
Just replace the <a href='#' onClick={...}> with <button onClick={...}>.
If you don't want to use a button, you could also change the onClick function to
const onLoadMoreClick = e => {
e.preventDefault(); // this prevents the navigation normally occuring with an <a> element
this.loadMore(this.state.skip);
}

Related

Testing a React component that uses redux toolkit and RTKQuery

I have been making an app using redux toolkit and RTKQuery, and hit a stumbling block on how to test a component that uses slices:
Component
export const Status = () => {
const selectedKidId = useSelector(getSelectedKidId);
const { selectedKid } = useGetKidsQuery(undefined, {
selectFromResult: ({ data }) => ({
selectedKid: data?.find((kid: KidType) => kid.id === selectedKidId),
}),
});
return (
<section>
<p>
Active:{' '}
{selectedKidId !== null ? selectedKid?.firstName : 'Select a kid'}
</p>
</section>
);
};
Test
test('title renders as expected', () => {
renderWithProviders(<Status />, {
preloadedState: { kids: { selectedKidId: '0' } },
});
expect(screen.getByText(/Monsters!/i)).toBeInTheDocument();
});
As you see I can add a selectedKidId in the preloadedState but the component also uses a generated hook useGetKidsQuery which return a list of kids, I don't know how or if I can add this to preloadedState as its an apiSlice.
How would I get my list of kids data into this test?

React component not rendering after state change

As the title says, when my state changes in my component, the sub components aren't rerendering.
class App extends Component {
constructor() {
super()
this.state = {
url: ""
}
this.handleWorkerSelect = this.handleWorkerSelect.bind(this)
}
handleWorkerSelect(url) {
this.setState({ url })
}
render() {
return (
<div className="App">
<Workers className="workers" handleClick={this.handleWorkerSelect}/>
<HermesWorker url={this.state.url}/>
</div>
)
}
}
const Workers = (props) => {
return (
<div>
<button onClick={() => props.handleClick("http://localhost:5000/api")}>Worker 1</button>
<button onClick={() => props.handleClick("http://localhost:2000/api")}>Worker 2</button>
</div>
)
}
export default App
here is hermesworker.js
class HermesWorker extends Component {
constructor() {
super()
this.state = {
items: [],
visited: [{name: "This Drive", path: "#back", root: ""}]
}
this.handleFolderClick = this.handleFolderClick.bind(this)
this.handleFileClick = this.handleFileClick.bind(this)
}
componentDidMount() {
if (this.props.url.length === 0) return
fetch(this.props.url)
.then(res => res.json())
.then(items => this.setState({ items }))
}
render() {
const folders = this.state.items.map((item) => {
if (!item.isfile) {
return <Card handleClick={this.handleFolderClick} root={item.root} path={item.path} isfile={item.isfile} name={item.name} size={item.size}/>
}
})
const files = this.state.items.map((item) => {
if (item.isfile) {
return <Card handleClick={this.handleFileClick} root={item.root} path={item.path} isfile={item.isfile} name={item.name} s ize={item.size}/>
}
})
const pathButtons = this.state.visited.map((item) => {
return <PathButton handleClick={this.handleFolderClick} root={item.root} path={item.path} name={item.name}/>
})
return (
<div>
{pathButtons}
<div className="flex-container">
{folders}
{files}
</div>
</div>
)
}
}
Essentially the issue is that the HermesWorker component is not being rerendered to use the new url prop. I am not sure why this is happening because for example, in the hermesworker it renders other subcomponents that do get rerendered during a state change.
Any information is appreciated
EDIT updated to add hermes worker, the file is over 100 lines so i cut out and only pasted the stuff I thought was important to the issue, can supply more if needed
I tested that code and it seems to be working fine. Could you provide What is set in HermesWorker component?
Edit: You'll require to set your state with setState on component updates. To do this, you may look for componentDidUpdate, which will run on every update. This is different from componentDidMount, which (hopefully) will run once and then the component may update and re-render, but re-render it's not considered as "mount". So you may try this instead:
constructor(props) {
super(props);
this.state = {
url: '',
items: [],
visited: [{name: "This Drive", path: "#back", root: ""}]
}
this.fetchData = this.fetchData.bind(this);
}
componentDidMount() {
//Mount Once
}
componentDidUpdate(prevProps, prevState) {
if (this.state.url !== this.props.url) {
this.setState({url: this.props.url});
// Url state has changed.
}
if(prevState.url !== this.state.url){
//run your fetch
this.fetchData();
}
}
fetchData(){
if (this.props.url.length === 0) return
fetch(this.props.url)
.then(res => res.json())
.then(items => this.setState({ items }));
}
Note: I moved the fetch to its own function, but that's completly up to you.
Also notice i added url to the state. Make sure to keep your props set to avoid unexpected behaviours.
Edit 2: componentDidUpdate will hand you prevProps and prevState as parameters. With prevProps you get access to whatever props you got on the previous update, and with prevState, as you may guess, you get access to whatever-your-state-was on the previous update. And by "on the previous update" i mean before the update got executed.

React: How to update the DOM with API results

My goal is to take the response from the Google API perspective and display the value into a div within the DOM.
Following a tutorial : https://medium.com/swlh/combat-toxicity-online-with-the-perspective-api-and-react-f090f1727374
Form is completed and works. I can see my response in the console. I can even store the response into an object, array, or simply extract the values.
The issue is I am struggling to write the values to the DOM even though I have it saved..
In my class is where I handle all the API work
class App extends React.Component {
handleSubmit = comment => {
axios
.post(PERSPECTIVE_API_URL, {
comment: {
text: comment
},
languages: ["en"],
requestedAttributes: {
TOXICITY: {},
INSULT: {},
FLIRTATION: {},
THREAT: {}
}
})
.then(res => {
myResponse= res.data; //redundant
apiResponse.push(myResponse);//pushed api response into an object array
console.log(res.data); //json response
console.log(apiResponse);
PrintRes(); //save the values for the API for later use
})
.catch(() => {
// The perspective request failed, put some defensive logic here!
});
};
render() {
const {flirty,insulting,threatening,toxic}=this.props
console.log(flirty); //returns undefined, makes sense upon initialization but does not update after PrintRes()
return (
<div className="App">
<h1>Please leave a comment </h1>
<CommentForm onSubmit={this.handleSubmit} />
</div>
);
}
}
When I receive a response from the API I use my own function to store the data, for use later, the intention being to write the results into a div for my page
export const PrintRes=() =>{
// apiResponse.forEach(parseToxins);
// myResponse=JSON.stringify(myResponse);
for (var i = 0; i < apiResponse.length; i++) {
a=apiResponse[i].attributeScores.FLIRTATION.summaryScore.value;
b=apiResponse[i].attributeScores.INSULT.summaryScore.value;
c=apiResponse[i].attributeScores.THREAT.summaryScore.value;
d=apiResponse[i].attributeScores.TOXICITY.summaryScore.value;
}
console.log("hell0");//did this function run
// render(){ cant enclose the return in the render() because I get an error on the { , not sure why
return(
<section>
<div>
<p>
Your comment is:
Flirty: {flirty}
</p>
</div>
<div>
<p>
Your comment is:
insulting: {insulting}
</p>
</div>
<div>
<p>
Your comment is:
threatening: {threatening}
</p>
</div>
<div>
<p>
Your comment is:
toxic: {toxic}
</p>
</div>
</section>
);
}
Variables and imports at the top
import React from "react";
//needed to make a POST request to the API
import axios from "axios";
import CommentForm from "../components/CommentForm";
var myResponse;
var apiResponse= [];
let a,b,c,d;
let moods = {
flirty: a,
insulting:b,
threatening:c,
toxic:d
}
If I understand correctly You need to create a state where you store data from api.
States in react works like realtime stores to refresh DOM when something change. this is an example to use it
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
apiData: undefined
};
}
fetchData() {
this.setState({
apiData: "Set result"
});
}
render() {
const { apiData } = this.state;
return (
<div>
<button onClick={this.fetchData.bind(this)}>FetchData</button>
<h3>Result</h3>
<p>{apiData || "Nothing yet"}</p>
</div>
);
}
}
you can check it here: https://codesandbox.io/s/suspicious-cloud-l1m4x
For more info about states in react look at this:
https://es.reactjs.org/docs/react-component.html#setstate

how can I pass data like the name of my user and put it in the post they created

so I am making an application for events and for some reason when a user creates an event the even info shows but the user info like their name and photo doesn't show up please help I've been having this problem for almost a week now.
THIS IS THE componentDidMount function
async componentDidMount() {
const { data } = await getCategories();
const categories = [{ _id: "", name: "All Categories" }, ...data];
const { data: events } = await getEvents();
this.setState({ events, categories });
console.log(events);
}
THIS IS THE STATE
class Events extends Component {
state = {
events: [],
user: getUser(),
users: getUsers(),
showDetails: false,
shownEventID: 0,
showUserProfile: false,
shownUserID: 0,
searchQuery: ""
};
THIS IS THE EVENTS FILE WHERE THE USER'S NAME AND PHOTO SHOULD BE DISPLAYED
<Link>
<img
className="profilePic mr-2"
src={"/images/" + event.hostPicture}
alt=""
onClick={() => this.handleShowUserProfile(event.userId)}
/>
</Link>
<Link style={{ textDecoration: "none", color: "black" }}>
<h4
onClick={() => this.handleShowUserProfile(event.userId)}
className="host-name"
>
{getUser(event.userId).name}
</h4>
</Link>
This is the userService file where the getUser function is
import http from "./httpService";
const apiEndPoint = "http://localhost:3100/api/users";
export function register(user) {
return http.post(apiEndPoint, {
email: user.email,
password: user.password,
name: user.name
});
}
export function getUsers() {
return http.get(apiEndPoint);
}
export async function getUser(userId) {
const result = await http.get(apiEndPoint + "/" + userId);
return result.data;
}
This is the eventService file where the event is
import http from "./httpService";
const apiEndPoint = "http://localhost:3100/api/events";
export function getEvents() {
return http.get(apiEndPoint);
}
export function getEvent(eventId) {
return http.get(apiEndPoint + "/" + eventId);
}
export function saveEvent(event) {
if(event._id){
const body = {...event}
delete body._id
return http.put(apiEndPoint + '/' + event._id, body)
}
return http.post(apiEndPoint, event);
}
export function deleteEvent(eventId) {
return http.delete(apiEndPoint + "/" + eventId);
}
First, you have some mistakes to use the class in <div> elements.
please use className instead class.
And then second I am not sure what it is.
class Events extends Component {
state = {
... ...
user: getUser(),
... ...
};
As you seen getUser() function requires one parameter userId.
But you did not send this.
So you met internal server error to do it.
Since I did not investigate all projects, I could not provide perfectly solution.
However, it is main reason, I think.
Please check it.

Cannot redirect to new page on first submission of form with history.push()

Edit
I've done some more debugging and here is the problem:
CreateProfile.js calls profileActions.createProfile() and passes data to be operated on and this.props.history so that it can push a new path onto the history stack.
profileActions.createProfile() successfully sends data to database. Database successfully uses the data.
profileActions.createProfile() pushes new path onto stack. The component at the path loads and successfully calls a reducer.
The URL in the browser does not reflect the path that is pushed onto the history stack. The new component does not load.
This only happens when creating an entry in the database. When updating an entry, the program works as expected.
I'm currently trying to redirect to a new page with react/redux. On the first submission, the form submits to the backend and creates an entry in the database but fails to redirect to the next page. On the second submission, however, it redirects just fine.
I'm using this.props.history.push() to do the redirect.
I think It may be an issue with the the response received from the backend but I cannot seem to figure out what the issue is. The reason I believe this is because it is hitting different logic because on the second submission, it is updating and not creating an entry.
Here is my component (CreateProfile.js)
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { createProfile } from '../../actions/profileActions';
import TextAreaGroup from '../common/TextAreaGroup';
import InputGroup from '../common/InputGroup';
class CreateProfile extends Component {
// Constructor
// componentWillRecieveProps()
onSubmit = (evt) => {
evt.preventDefault();
const profileData = {
handle: this.state.handle,
bio: this.state.bio,
website: this.state.website,
twitter: this.state.twitter,
instagram: this.state.instagram,
youtube: this.state.youtube,
linkedin: this.state.linkedin,
github: this.state.github,
vsco: this.state.vsco
};
this.props.createProfile(profileData, this.props.history);
}
//onChange()
render() {
// render logic
return (
// markup
<form onSubmit={this.onSubmit}>
// markup
<input
type="submit"
value="Create Profile"
className="btn btn-info btn-block mt-4"
/>
</form>
</div>
</div>
</div>
</div>
)
}
}
CreateProfile.propTypes = {
createProfile: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
profile: state.profile,
errors: state.errors
});
export default connect(mapStateToProps, { createProfile })(withRouter(CreateProfile));
Here is my action file that submits to the backend (profileActions.js):
import axios from 'axios';
// import types
import { GET_PROFILE, PROFILE_LOADING, GET_ERRORS, CLEAR_CURRENT_PROFILE } from './types';
// Create Profile
export const createProfile = (profileData, history) => dispatch => {
axios.post('/api/profile', profileData)
.then(res => history.push('/login'))
.catch(err => {
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
})
};
}
And here is the route in my backend that is being submitted to:
router.post('/', passport.authenticate('jwt', { session: false }), (req, res) => {
const { errors, isValid } = validateProfileInputs(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const profileFields = {}; //code setting fields omitted
Profile.findOne({user: req.user.id}).then(profile => {
if (profile) {
// Update Profile
Profile.findOneAndUpdate(
{ user: req.user.id },
{ $set: profileFields },
{ new: true }
).then(profile => res.json(profile)); // SUCCESSFUL PUSH ONTO THIS.PROPS.HISTORY
} else {
// Create Profile
// Check if handle exists
Profile.findOne({ handle: profileFields.handle })
.then(profile => {
if (profile) {
errors.handle = 'That handle already exists';
res.status(400).json(errors);
}
new Profile(profileFields).save().then(profile => res.json(profile)); // PUSH ONTO THIS.PROPS.HISTORY NOT OCCURRING
});
}
});
});
Any and all help would be greatly appreciated. I have tried my hardest but cannot seem to figure out what the issue is.
This problem arose because of my lack of understanding of how asynchronous javascript works.
The issue was with a few lines of code in the component that I was trying to push too.
componentDidMount() {
this.props.getProfile(); // Async function, sets profile object in store
}
render() {
const { profile } = this.state.profile;
if(!Object.keys(profile).length > 0) { // This is always evaluates to true
// because it executes before
// this.props.getProfile() returns
this.props.history.push('/create-profile');
}
}

Resources