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.
Related
Here is the code in actions.js
export function exportRecordToExcel(record) {
return ({fetch}) => ({
type: EXPORT_RECORD_TO_EXCEL,
payload: {
promise: fetch('/records/export', {
credentials: 'same-origin',
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(response) {
return response;
})
}
});
}
The returned response is an .xlsx file. I want the user to be able to save it as a file, but nothing happens. I assume the server is returning the right type of response because in the console it says
Content-Disposition:attachment; filename="report.xlsx"
What am I missing? What should I do in the reducer?
Browser technology currently doesn't support downloading a file directly from an Ajax request. The work around is to add a hidden form and submit it behind the scenes to get the browser to trigger the Save dialog.
I'm running a standard Flux implementation so I'm not sure what the exact Redux (Reducer) code should be, but the workflow I just created for a file download goes like this...
I have a React component called FileDownload. All this component does is render a hidden form and then, inside componentDidMount, immediately submit the form and call it's onDownloadComplete prop.
I have another React component, we'll call it Widget, with a download button/icon (many actually... one for each item in a table). Widget has corresponding action and store files. Widget imports FileDownload.
Widget has two methods related to the download: handleDownload and handleDownloadComplete.
Widget store has a property called downloadPath. It's set to null by default. When it's value is set to null, there is no file download in progress and the Widget component does not render the FileDownload component.
Clicking the button/icon in Widget calls the handleDownload method which triggers a downloadFile action. The downloadFile action does NOT make an Ajax request. It dispatches a DOWNLOAD_FILE event to the store sending along with it the downloadPath for the file to download. The store saves the downloadPath and emits a change event.
Since there is now a downloadPath, Widget will render FileDownload passing in the necessary props including downloadPath as well as the handleDownloadComplete method as the value for onDownloadComplete.
When FileDownload is rendered and the form is submitted with method="GET" (POST should work too) and action={downloadPath}, the server response will now trigger the browser's Save dialog for the target download file (tested in IE 9/10, latest Firefox and Chrome).
Immediately following the form submit, onDownloadComplete/handleDownloadComplete is called. This triggers another action that dispatches a DOWNLOAD_FILE event. However, this time downloadPath is set to null. The store saves the downloadPath as null and emits a change event.
Since there is no longer a downloadPath the FileDownload component is not rendered in Widget and the world is a happy place.
Widget.js - partial code only
import FileDownload from './FileDownload';
export default class Widget extends Component {
constructor(props) {
super(props);
this.state = widgetStore.getState().toJS();
}
handleDownload(data) {
widgetActions.downloadFile(data);
}
handleDownloadComplete() {
widgetActions.downloadFile();
}
render() {
const downloadPath = this.state.downloadPath;
return (
// button/icon with click bound to this.handleDownload goes here
{downloadPath &&
<FileDownload
actionPath={downloadPath}
onDownloadComplete={this.handleDownloadComplete}
/>
}
);
}
widgetActions.js - partial code only
export function downloadFile(data) {
let downloadPath = null;
if (data) {
downloadPath = `${apiResource}/${data.fileName}`;
}
appDispatcher.dispatch({
actionType: actionTypes.DOWNLOAD_FILE,
downloadPath
});
}
widgetStore.js - partial code only
let store = Map({
downloadPath: null,
isLoading: false,
// other store properties
});
class WidgetStore extends Store {
constructor() {
super();
this.dispatchToken = appDispatcher.register(action => {
switch (action.actionType) {
case actionTypes.DOWNLOAD_FILE:
store = store.merge({
downloadPath: action.downloadPath,
isLoading: !!action.downloadPath
});
this.emitChange();
break;
FileDownload.js
- complete, fully functional code ready for copy and paste
- React 0.14.7 with Babel 6.x ["es2015", "react", "stage-0"]
- form needs to be display: none which is what the "hidden" className is for
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
function getFormInputs() {
const {queryParams} = this.props;
if (queryParams === undefined) {
return null;
}
return Object.keys(queryParams).map((name, index) => {
return (
<input
key={index}
name={name}
type="hidden"
value={queryParams[name]}
/>
);
});
}
export default class FileDownload extends Component {
static propTypes = {
actionPath: PropTypes.string.isRequired,
method: PropTypes.string,
onDownloadComplete: PropTypes.func.isRequired,
queryParams: PropTypes.object
};
static defaultProps = {
method: 'GET'
};
componentDidMount() {
ReactDOM.findDOMNode(this).submit();
this.props.onDownloadComplete();
}
render() {
const {actionPath, method} = this.props;
return (
<form
action={actionPath}
className="hidden"
method={method}
>
{getFormInputs.call(this)}
</form>
);
}
}
You can use these two libs to download files http://danml.com/download.html https://github.com/eligrey/FileSaver.js/#filesaverjs
example
// for FileSaver
import FileSaver from 'file-saver';
export function exportRecordToExcel(record) {
return ({fetch}) => ({
type: EXPORT_RECORD_TO_EXCEL,
payload: {
promise: fetch('/records/export', {
credentials: 'same-origin',
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(response) {
return response.blob();
}).then(function(blob) {
FileSaver.saveAs(blob, 'nameFile.zip');
})
}
});
// for download
let download = require('./download.min');
export function exportRecordToExcel(record) {
return ({fetch}) => ({
type: EXPORT_RECORD_TO_EXCEL,
payload: {
promise: fetch('/records/export', {
credentials: 'same-origin',
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}).then(function(response) {
return response.blob();
}).then(function(blob) {
download (blob);
})
}
});
I have faced the same problem once too.
I have solved it by creating on empty link with a ref to it like so:
linkRef = React.createRef();
render() {
return (
<a ref={this.linkRef}/>
);
}
and in my fetch function i have done something like this:
fetch(/*your params*/)
}).then(res => {
return res.blob();
}).then(blob => {
const href = window.URL.createObjectURL(blob);
const a = this.linkRef.current;
a.download = 'Lebenslauf.pdf';
a.href = href;
a.click();
a.href = '';
}).catch(err => console.error(err));
basically i have assigned the blobs url(href) to the link, set the download attribute and enforce one click on the link.
As far as i understand this is the "basic" idea of the answer provided by #Nate.
I dont know if this is a good idea to do it this way... I did.
This worked for me.
const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
fetch(`${url}`, requestOptions)
.then((res) => {
return res.blob();
})
.then((blob) => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'config.json'); //or any other extension
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch((err) => {
return Promise.reject({ Error: 'Something Went Wrong', err });
})
I managed to download the file generated by the rest API URL much easier with this kind of code which worked just fine on my local:
import React, {Component} from "react";
import {saveAs} from "file-saver";
class MyForm extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
event.preventDefault();
const form = event.target;
let queryParam = buildQueryParams(form.elements);
let url = 'http://localhost:8080/...whatever?' + queryParam;
fetch(url, {
method: 'GET',
headers: {
// whatever
},
})
.then(function (response) {
return response.blob();
}
)
.then(function(blob) {
saveAs(blob, "yourFilename.xlsx");
})
.catch(error => {
//whatever
})
}
render() {
return (
<form onSubmit={this.handleSubmit} id="whateverFormId">
<table>
<tbody>
<tr>
<td>
<input type="text" key="myText" name="myText" id="myText"/>
</td>
<td><input key="startDate" name="from" id="startDate" type="date"/></td>
<td><input key="endDate" name="to" id="endDate" type="date"/></td>
</tr>
<tr>
<td colSpan="3" align="right">
<button>Export</button>
</td>
</tr>
</tbody>
</table>
</form>
);
}
}
function buildQueryParams(formElements) {
let queryParam = "";
//do code here
return queryParam;
}
export default MyForm;
I needed to just download a file onClick but I needed to run some logic to either fetch or compute the actual url where the file existed. I also did not want to use any anti-react imperative patterns like setting a ref and manually clicking it when I had the resource url. The declarative pattern I used was
onClick = () => {
// do something to compute or go fetch
// the url we need from the server
const url = goComputeOrFetchURL();
// window.location forces the browser to prompt the user if they want to download it
window.location = url
}
render() {
return (
<Button onClick={ this.onClick } />
);
}
I think this solution is maybe a bit more "reactive" than others:
import React, { forwardRef, useImperativeHandle, useLayoutEffect, useState } from 'react';
export interface DownloadHandle {
download: (params: { title: string; data?: Blob }) => void;
}
export const Download = forwardRef<DownloadHandle, {}>((props, ref) => {
const linkRef = React.useRef<HTMLAnchorElement>(null);
const [download, setDownload] = useState<{ title: string; data: Blob }>();
useImperativeHandle(ref, () => ({
download: (params) => {
if (params.data) {
setDownload(params as typeof download);
}
},
}));
//trigger download and clear data
useLayoutEffect(() => {
if (download) {
linkRef?.current?.click();
}
setDownload(undefined);
}, [download]);
if (!download) {
return null;
}
const { title, data } = download;
return <a href={window.URL.createObjectURL(data)} download={title} ref={linkRef} />;
});
export type DownloadElement = React.ElementRef<typeof Download>;
Usage
const App = () => {
const downloadRef = useRef<DownloadElement>(null);
const handleDownload = () => {
fetch(url, requestOptions)
.then((res) => res.blob())
.then((data) => {
downloadRef.current?.download({ title: `myFile.txt`, data});
});
}
return (
<div>
<Download ref={downloadRef} />
<button onClick={}>Download</button>
</div>
)
}
My Gatsby site use the same GSAP timeline on every page, so I want to stay DRY and my idea is to include my timeline in my Layout component in that order.
But I don't know how to pass refs that I need between children and layout using forwardRef.
In short, I don't know how to handle the sectionsRef part between pages and layout.
sectionsRef is dependant of the page content (children) but is needed in the timeline living in layout.
How can I share sectionsRef between these two (I tried many things but always leading to errors)?
Here's a codesandbox without the refs in the Layout:
https://codesandbox.io/s/jolly-almeida-njt2e?file=/src/pages/index.js
And the sandbox with the refs in the layout:
https://codesandbox.io/s/pensive-varahamihira-tc45m?file=/src/pages/index.js
Here's a simplified version of my files :
Layout.js
export default function Layout({ children }) {
const containerRef = useRef(null);
const sectionsRef = useRef([]);
sectionsRef.current = [];
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const scrollTimeline = gsap.timeline();
scrollTimeline.to(sectionsRef.current, {
x: () =>
`${-(
containerRef.current.scrollWidth -
document.documentElement.clientWidth
)}px`,
ease: 'none',
scrollTrigger: {
trigger: containerRef.current,
invalidateOnRefresh: true,
scrub: 0.5,
pin: true,
start: () => `top top`,
end: () =>
`+=${
containerRef.current.scrollWidth -
document.documentElement.clientWidth
}`,
},
});
}, [containerRef, sectionsRef]);
return (
<div className="slides-container" ref={containerRef}>
{children}
</div>
);
}
index.js (page)
import { graphql } from 'gatsby';
import React, { forwardRef } from 'react';
import SectionImage from '../components/sections/SectionImage';
import SectionIntro from '../components/sections/SectionIntro';
import SectionColumns from '../components/sections/SectionColumns';
const HomePage = ({ data: { home } }, sectionsRef) => {
const { sections } = home;
const addToRefs = (el) => {
if (el && !sectionsRef.current.includes(el)) {
sectionsRef.current.push(el);
}
};
return (
<>
{sections.map((section) => {
if (section.__typename === 'SanitySectionIntro') {
return (
<SectionIntro key={section.id} section={section} ref={addToRefs} />
);
}
if (section.__typename === 'SanitySectionImage') {
return (
<SectionImage key={section.id} section={section} ref={addToRefs} />
);
}
if (section.__typename === 'SanitySectionColumns') {
return (
<SectionColumns
key={section.id}
section={section}
ref={addToRefs}
/>
);
}
return '';
})}
</>
);
};
export default forwardRef(HomePage);
export const query = graphql`
query HomeQuery {
// ...
}
`;
Any clue greatly appreciated :)
I have a userId array and I need to show the list of names related to that array. I want to call API call inside the render method and get the username. But this is not working. How can I fix this issue?
Below is my render method:
render(){
...
return(
<div>
{this.state.users.map(userId => {
return (
<div> {this.renderName(userId )} </div>
)
})}
</div>
)
...
}
Below is the renderName function:
renderName = (userId) => {
axios.get(backendURI.url + '/users/getUserName/' + userId)
.then(res => <div>{res.data.name}</div>)
}
Basically you cannot use asynchronous calls inside a render because they return a Promise which is not valid JSX. Rather use componentDidMount and setState to update the users array with their names.
Generally, you do not change state or fetch data in the render method directly. State is always changed by actions/events (clicks, input or whatever). The render method is called everytime a prop/state changes. If you change the state within the render method directly, you end up having an infinite loop.
You should use the lifecycle methods or hooks to load data from an api. Here's an example from the official React FAQ: https://reactjs.org/docs/faq-ajax.html
This will not render anything as the API calls are asynchronous and since renderName function isn't returning anything, it'll return undefined.
You should create a function, which will call api for all the userIds and update in state
getNames = () => {
const promises = [];
this.state.users.forEach((userId) => {
promises.push(axios.get(backendURI.url+'/users/getUserName/'+userId));
})
// Once all promises are resolved, update the state
Promise.all(promises).then((responses) => {
const names = responses.map((response) => response.data.names);
this.setState({names});
})
}
Now you can call this function in either componentDidMount or componentDidUpdate, whenever users data is available.
And finally, you can iterate over names directly and render them
<div>
{this.state.names.map((name) => {
return <div> {name} </div>;
})}
</div>
You could make user name it's own component:
const request = (id) =>
new Promise((resolve) =>
setTimeout(resolve(`id is:${id}`), 2000)
);
const UserName = React.memo(function User({ userId }) {
const [name, setName] = React.useState('');
React.useEffect(() => {
//make the request and set local state to the result
request(userId).then((result) => setName(result));
}, [userId]);
return <div> {name} </div>;
});
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
users: [1, 2],
};
}
render() {
return (
<ul>
{this.state.users.map((userId) => (
<UserName key={userId} userId={userId} />
))}
</ul>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
export default ()=> {
let [users,setUsers] = useState([]);
useEffect(()=>{
let fetchUsersInfoRemote = Promise.all([...Array(10)].map(async (_,index)=>{
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${index+1}`);
return response.data;
}
catch(error) {
return ;
}
}));
fetchUsersInfoRemote.then(data=> setUsers(data));
},[]);
return (
<div className="App">
<ul>
{
users.map(user=>(<li><pre>{JSON.stringify(user,null,2)}</pre></li>))
}
</ul>
</div>
);
}
I have the following code:
In App.js:
class App extends React.Component {
constructor() {
super();
this.state = {
loggedStatus: {
username: undefined,
isLoggedIn: undefined,
}
}
}
componentDidMount() {
let username = undefined;
let isLoggedIn = undefined;
if (localStorage.getItem("token")) {
fetch("https://localhost:8000/user", {
method: "POST",
headers: {
"Authorization": `Bearer ${localStorage.getItem("token")}`
}
}).then(response => response.json()).then(response => {
if (response.success) {
username = response.username;
isLoggedIn = true;
} else {
username = undefined;
isLoggedIn = false;
localStorage.removeItem("token");
}
this.setState({
loggedStatus: {
username: username,
isLoggedIn: isLoggedIn
}
})
})
}
}
render() {
return (
<div className="App">
<Router>
<Navbar loggedStatus={this.state.loggedStatus}/>
<Switch>
<Route path="/register">
<RegisterForm />
</Route>
<Route path="/login">
<LoginForm />
</Route>
</Switch>
</Router>
</div>
)
}
}
and in Navbar.js:
logout = () => {
localStorage.removeItem("token");
}
render() {
return (
<nav className="Navbar">
{this.props.loggedStatus.isLoggedIn ?
<>
<ul className="Navbar-list">
<li className="Navbar-item">
<span className="Navbar-greeting">Hello, {this.props.loggedStatus.username}</span>
</li>
<li className="Navbar-item">
<button className="Navbar-logoutBtn" onClick={this.logout}>Sign Out</button>
</li>
</ul>
</>
:
<>
<ul className="Navbar-list">
<li className="Navbar-item">
<Link to="/register" className="Navbar-link">Sign Up</Link>
</li>
<li className="Navbar-item">
<Link to="/login" className="Navbar-link">Log In</Link>
</li>
</ul>
</>
}
</nav>
)
}
The problem I'm having is I would like my navbar component to update when either the user logs in, or logs out. With my current code, I have to refresh the page in order for it to update. I've been messing around with things with no luck. I understand that componentDidMount is only called once through the entire process, which is why setState is only called upon refresh.
Edit: Login.
fetch(`${this.apiURL}/user/login`, {
method: "POST",
body: JSON.stringify(user),
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
})
.then(response => response.json())
.then(response => {
if (response.success) {
status = {
statusMsg: <p className="LoginForm-statusMsg">{response.statusMsg}</p>
}
localStorage.setItem("token", response.token);
this.setState({
status: status
}, () => setTimeout(() => {
this.props.history.push("/");
}, 5000));
} else {
status = {
statusMsg: <p className="LoginForm-statusMsg">{response.statusMsg}</p>
}
this.setState({
status : status
})
}
});
}
}
setState({}) always forces to re-render. (unless you return false in: shouldComponentUpdate(nextProps, nextState)) You can check this by putting a console log in
componentDidUpdate(prevProps, prevState) {
console.log("Component did update")
}
It's not clear what your JobsScreenTabs component consists of but make sure that for changes you expect to happen inside the JobsScreenTabs component it actually changes its state. Pass properties from your WorkshopJobsScreen component or make changes directly in the JobsScreenTabs component.
Also important:
Using State Correctly
There are three things you should know about setState().
Do Not Modify State Directly
For example, this will not re-render a component:
// Wrong
this.state.comment = 'Hello';
Instead, use setState():
// Correct
this.setState({comment: 'Hello'});
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
Neither parent nor child components can know if a certain component is stateful or stateless, and they shouldn’t care whether it is defined as a function or a class.
This is why state is often called local or encapsulated. It is not accessible to any component other than the one that owns and sets it.
So if you wish to make changes in your component make sure to manipulate the state of the correct component.
Read more about React lifecycles at: https://reactjs.org/docs/state-and-lifecycle.html
So just some general info. Answer starts here:
You can pass your state in props from app js to navbar component through the route:
let loggedStatus = {
username: undefined,
isLoggedIn: false
}
<Route path="/" render={(props) => <NavBar props={loggedStatus} {...props} /> } exact />
In NavBar you can access it:
export class NavBar extends Component {
constructor(props) {
super(props);
console.log(props)
this.state = {
isLoggedIn: this.props.loggedStatus.isLoggedIn
As I'm not aware of the login flow you are following. But as far as what I understood. The code below should match your requirement.
In NavBar.js
logout = () => {
this.props.handleLogout()
}
In app.js
handleLogout = () => {
localStorage.removeItem("token");
this.setState({
loggedStatus: {
username: undefined,
isLoggedIn: false
}
})
}
render(){
return(
..
..
<Navbar loggedStatus={this.state.loggedStatus} handleLogout={this.handleLogout}/>
)
}
The state changes so it will re-render the component.
Hope this helps you.
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);
}