The current state object is this:
this.state = {
loadedPostList: [],
top: 0,
end: 0,
thresh: 20,
page: 0,
loadingPostList: false,
filter: [],
lazying: false
};
I'm utilising some react lifecycle methods to control my redux store updates, and dataflow:
shouldComponentUpdate(nextProps, nextState) {
return this.props.posts.loadedPosts !== nextProps.posts.loadedPosts || this.state.loadedPostList !== nextState.loadedPostList;
}
componentDidUpdate(prevProps) {
let { loadedPosts, changeEvent, updatedId, deleteId } = this.props.posts;
if(prevProps.posts.loadedPosts !== loadedPosts) {
switch(changeEvent) {
case 'insert':
if(this.state.top === 0) {
this.refreshList();
}
console.log('new post in thread...');
break;
case 'update':
this.refreshList();
console.log('post updated in thread...');
break;
case 'delete':
this.refreshList();
console.log('post deleted in thread...');
break;
default:
break;
}
}
}
refreshList = () => {
let newList = this.props.posts.loadedPosts.slice(this.state.top, this.state.end);
this.setState({
loadedPostList: [...newList]
}, () => {
console.log('refreshed post list')
})
}
And in the render function, I'm mapping the elements of this.state.loadedPostList to PostList components. The problem is that when my redux store updates, and subsequently my this.state.loadedPostList, the mapping in re-render doesn't update to show the updated array. Logging to the console and observing the redux actions and store via redux dev tools show that the array is indeed updating correctly, and the functions such us refreshList() are working as they should. However, the problem remains with the DOM not re-rendering/updating according to the state changes. The render function:
render() {
return (
<div id="question-list-wrapper">
{
this.state.loadingPostList || this.state.lazying ?
<MiniViewLoader textToRender="Loading questions..."/>
:
<div id="question-list-inner-wrapper">
<div id="question-list-inner-wrapper-nav">
<h1 className="text" id='inner-wrapper-nav-header'> Recent Questions </h1>
</div>
{
this.state.loadedPostList.length < 1 ? <h1 className="no-questions-text">No questions availabe</h1> : ""
}
{
this.state.loadedPostList.map((post, index) => {
return (
<PostSlot idx={index} key={index+""} postData={post}/>
)
})
}
<div id="question-list-write-btn" alt="Ask Question">
<span>Ask Question</span>
<WritePostButton />
</div>
</div>
}
</div>
)
}
(EDIT) Further information if it helps. The component with the issue is being rendered by a react router Switch wrapper inside the render() function of another component.
The Other Component:
render() {
return(
<div className="page-wrapper">
{
this.state.settingUp ?
<FullPageLoaderWithText textToRender="Setting things up for you..."/>
:
<div id="main-hoc-wrapper-inner-wrapper">
<div id="main-hoc-nav-bar" className="item-a">
<span id="main-hoc-nav-logo" className="main-hoc-nav-elem">
PA
</span>
<span id="main-hoc-nav-search" className="main-hoc-nav-elem">
<input type="text" placeholder="Search..." className="placeholderEdit"/>
</span>
</div>
<div id="main-hoc-sidebar" className="item-c">
<span className="main-hoc-sidebar-tabs">{this.props.user.data}</span>
<span className="main-hoc-sidebar-tabs">tags</span>
<span className="main-hoc-sidebar-tabs">community</span>
<span className="main-hoc-sidebar-tabs">opportunities</span>
</div>
<div id="main-hoc-main-view" className="item-b">
<Switch>
{/* <Route path="/:param1/:param2/path" component={QuestionList} /> */}
<Route path="/:param1/:param2/path" render={() => <QuestionList connect={socket}/>} />
</Switch>
</div>
<div id="main-hoc-right-bar" className="item-d">
Blog posts & ads
</div>
</div>
}
</div>
)
}
I think the problem is your if condition, you cannot compare two array with !==.
Example:
var t = [1,2,3];
var f = [1,2,3];
if (t !== f) {
console.log("different");
} else {
console.log("same");
}
// by your logic the output should be "same"
// but it will not be
// because internally they both are object and we cannot compare
// two objects using `===` operator.
// you will have to loop through and check for difference
// or better let REACT HANDLE SUCH THINGS
the problem is in this line
let newList = this.props.posts.loadedPosts.slice(this.state.top, this.state.end);,
this.state.top and this.state.end is both 0, so it will always return an empty array, make sure you are updating the top and end after each refresh.
Related
So I'm trying to build a Notifications component in React. The component's state holds an array of notifications which are objects. One of their keys is 'seen'. The purpose of seen, as you can probably guess, is mainly visual. Everytime a user clicks on a notification, I run a function that's supposed to set the notification as seen in the database (for consistency) and in the local state (for UI).
The database part works great, but for some reason the state change doesn't work. When I put some console.logs, weirdly enough I see that the 'seen' property changes to 1 before I even call this.setState. I've been at it for hours now and I can't figure out what's happening.
And now, some code:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import classes from './Notifications.module.css';
import * as actions from '../../../store/actions';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import moment from 'moment';
import {Redirect} from 'react-router-dom';
class Notifications extends Component {
constructor(props) {
super(props);
// Set an interval to update notifications every 4 minutes.
this.update = setInterval(() => {
this.props.fetchNotifications(this.props.username)
}, 240000)
}
state = {
opened: false,
count: 0,
notifications: []
}
componentDidUpdate(prevProps, prevState) {
if (!prevProps.username && this.props.username) this.props.fetchNotifications(this.props.username);
if (!prevProps.notifications && this.props.notifications) {
this.setState({notifications: this.props.notifications, count: this.props.notifications.filter(not => !not.seen).length});
}
if (this.props.notifications) {
if (JSON.stringify(this.state.notifications) !== JSON.stringify(prevState.notifications)) {
this.setState({count: this.state.notifications.filter(not => !not.seen).length})
}
}
}
componentWillUnmount() {
// Clear the update interval
clearInterval(this.update);
}
redirect(model, model_id) {
switch (model) {
case 'sealant_customer':
return <Redirect to={`/profile/sealant-customer/${model_id}`} />;
case 'unapproved_business':
return <Redirect to={`/profile/unapproved-business/${model_id}`} />;
case 'business':
return <Redirect to={`/profile/business/${model_id}`} />;
case 'missed_call':
return <Redirect to={`/data/missed-calls`} />;
default: return null;
}
}
render() {
let content = (
<React.Fragment>
<div className={classes.svgWrapper}>
<p className={classes.counter} style={{opacity: this.state.count === 0 ? '0' : '1'}}>{this.state.count}</p>
<FontAwesomeIcon icon='bell' onClick={() => this.setState(prevState => ({opened: !prevState.opened}))} />
</div>
{this.state.opened && <div className={classes.notificationsWrapper}>
<ul className={classes.notificationsList}>
{this.state.notifications.length !== 0 ? Array.from(this.state.notifications).map(notifi => {
let icon;
switch (notifi.model) {
case 'sealant_customer':
case 'person':
icon = 'user';
break;
case 'business':
case 'unapproved_business':
icon = 'warehouse';
break;
default: icon = 'user';
}
let classArray = [classes.notification];
if (!notifi.seen) classArray.push(classes.unseen);
return (
<li key={notifi.id} className={classArray.join(' ')} onClick={ () => {
// If already seen, simply redirect on click.
if (notifi.seen) return this.redirect(notifi.model, notifi.model_id);
let newNotifications = [...this.state.notifications];
// If not seen, mark as seen in State & in Database.
let index = newNotifications.findIndex(not => notifi.id === not.id);
newNotifications[index].seen = 1;
this.setState({ notifications: newNotifications});
this.props.markSeen(notifi.id, this.props.username);
// Redirect.
return this.redirect(notifi.model, notifi.model_id);
}}>
<div className={classes.iconWrapper}>
<FontAwesomeIcon icon={icon} />
</div>
<div className={classes.textWrapper}>
<p className={classes.msg}>
{notifi.message}
</p>
<label className={classes.ago}>
{moment(notifi.date).fromNow()}
</label>
</div>
</li>
)
}) : <li className={classes.notification} style={{cursor: 'default'}}><p style={{whiteSpace: 'nowrap'}}>No notifications to show...</p></li>}
</ul>
</div>}
</React.Fragment>
)
return (
<div className={classes.wrapper}>
{content}
</div>
)
}
}
const mapStateToProps = state => {
return {
username: state.auth.username,
notifications: state.data.notifications
};
};
const mapDispatchToProps = dispatch => {
return {
fetchNotifications: username => dispatch(actions.fetchNotifications(username)),
markSeen: (id, username) => dispatch(actions.markSeen(id, username))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Notifications);
Any help would be appreciated.
For future readers - the SOLUTION:
The problem was that when I called the line let newNotifications = [...this.state.notifications]; I really did create a copy of notifications, but a copy which holds the original notification objects inside.
Once I changed the line newNotifications[index].seen = 1 to newNotifications[index] = {...newNotifications[index], seen: 1} everything worked like a charm.
I am trying to get No list items only when there's nothing coming from the backend. Right now, onload, I get the loading spinner and No List items before I fetch the data.
So, I thought I would add a timeout to deal with this so that it will only show up after the fetching is done, and there are no items
getList() {
if(this.state.list.length != 0){
return (this.state.list.map(data => {
return <div data={data} key={data.id}/>
}))
}else{
return <div>No List items</div>
}
}
render() {
return (
<div>
<Spinner active={this.state.active} />
<div>{setTimeout(this.getList, 1000)}</div>
</div>
);
}
}
When i use this, I am getting numbers on the browser. The active state of spinner changes on componentDidMount to false
That's what setTimeout returns: an id number, which you can use later if you want to cancel the timeout.
The render method is synchronous. If you want to render nothing for the case where you don't have data, then you can have render return null. Then in componentDidMount, do any async work you need, and when it completes, call this.setState to update the state and rerender (this time without a null)
class Items extends React.Component {
constructor(props) {
super();
this.state = {
active: true,
is_loading: false,
}
}
componentDidMount() {
this.timeout_number = setTimeout(() => {
this.setState({
active: false,
is_loading: true
});
}, 1000);
}
componentWillUnmount() {
clearTimeout(this.timeout_number);
}
getList() {
if(this.state.list.length)
return this.state.list.map(data => <div data={data} key={data.id}/>)
else
return <div>No List items</div>
}
render() {
return (
<div>
<Spinner active={this.state.active} />
{this.state.is_loading
? this.getList()
: null}
</div>
);
}
}
export default Items;
Don't use a timeout here. I would just set the initial state of list to null. Then just flip your logic so that it is:
getList() {
if(this.state.list && this.state.list.length == 0){
return <div> No List items </div>
}else{
return (this.state.list.map(data => {
return <div data={data} key={data.id}/>
}))
}
}
There are 100 ways to solve this but this is the easiest based on your code. ALso don't forget the difference between != and !==.
I am somewhat new to ReactJS
I have a react class that is rendering a number of items: (Sample)
var app = app || {};
app.Results = React.createClass({
componentDidMount: function () {
},
handleUpdateEvent: function(id){
var _self = this;
var handler = function()
{
var query = _self.props.results.query;
_self.props.onSearch(query); // re-does the search to re-render items ...
// obviously this is wrong since I have to click the button twice to see the results
//
}
var optionsURL = {dataType: 'json'};
optionsURL.type= 'POST';
optionsURL.url = 'http://localhost:8983/solr/jcg/dataimport?command=delta-import&clean=false&commit=true&json.nl=map&wt=json&json.wrf=?&id='+id;
// updates index for specific item.
jQuery.ajax(optionsURL).done(handler);
},
render: function () {
var tdLabelStyle = {
width: '150px'
}
return (
<div id="results-list">
{this.props.results.documents.map(function (item) {
return (
<div id={item.id} key={item.id} className="container-fluid result-item">
<div className="row">
<div className="col-md-6">
<table>
<tr><td colspan="2">{item.name}</td></tr>
<tr style={{marginTop:'5px'}}><td style={tdLabelStyle}><b>Amount:</b></td><td>{item.amount}
<button type="Submit" onClick={() => {this.handleUpdateEvent(item.id)}} title="Refresh Amount" >Refresh</button>
</td></tr>
</table>
</div>
</div>
</div>
)
},this)}
</div>
);
}
});
I have a button within the table that makes a call out to SOLR to perform a delta import, then re-calls the select function in order to grab the new data.
I'm obviously doing the handleUpdateEvent function incorrectly, however, I'm not 100% sure how to go about getting either the entire thing to re-render, or just the individual item to re-render.
(Hopefully I've made sense...)
Any help is appreciated.
(onSearch Function)
handleSearchEvent: function (query) {
if (this.state.query != null)
{
if (this.state.query.filters != null)
{
query.filters = this.state.query.filters;
}
}
$("#load-spinner-page").show();
if (app.cache.firstLoad) {
$("body").css("background","#F8F8F8");
app.cache.firstLoad = false;
}
var _self = this;
app.cache.query = query;
docSolrSvc.querySolr(query, function(solrResults) {
_self.setState({query: query, results: solrResults});
$("#load-spinner-page").hide();
});
},
The first thing to change is the use of React.createClass. This has been depracated in favour ES6 syntax. Also, I dont't suggest using jQuery along side React. It's not impossible to do, but there are other things to consider. Read this for more. I'll use it here, but consider something like fetch or axios (or one of the many other libraries) for fetching the data.
I think you're on the right track, but a few things to update. Because the available options are changing, I would put them into the components state, then having the handleUpdateEvent function update the state, which will trigger a re-render.
Your class would look something like this:
class Results extends React.Component {
constructor(props) {
super(props);
// this sets the initial state to the passed in results
this.state = {
results: props.results
}
}
handleUpdateEvent(id) {
const optionsURL = {
dataType: 'json',
type: 'POST',
url: `http://localhost:8983/solr/jcg/dataimport?command=delta-import&clean=false&commit=true&json.nl=map&wt=json&json.wrf=?&id=${ id }`
};
// Instead of calling another function, we can do this right here.
// This assumes the `results` from the ajax call are the same format as what was initially passed in
jQuery.ajax(optionsURL).done((results) => {
// Set the component state to the new results, call `this.props.onSearch` in the callback of `setState`
// I don't know what `docSolrSvc` is, so I'm not getting into the `onSearch` function
this.setState({ results }, () => {
this.props.onSearch(results.query);
});
});
}
render() {
const tdLabelStyle = {
width: '150px'
};
// use this.state.results, not this.props.results
return (
<div id="results-list">
{
this.state.results.documents.map((item) => (
<div>
<div id={ item.id } key={ item.id } className="container-fluid result-item">
<div className="row">
<div className="col-md-6">
<table>
<tr><td colspan="2">{item.name}</td></tr>
<tr style={{marginTop:'5px'}}>
<td style={ tdLabelStyle }><b>Amount:</b></td>
<td>{item.amount}
<button type="button" onClick={ () => { this.handleUpdateEvent(item.id) } } title="Refresh Amount" >Refresh</button>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
))
}
</div>
);
}
}
First watch this, so you can see the behavior going on.
Timing Issue (JS in one component relies on another component to exist first)
I need to be able to somehow check that another component exists before I apply this JS in this component's ComponentDidMount
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
const el = ReactDOM.findDOMNode(this);
console.log("table of contents mounted");
if(document.getElementById('interview-heading') && el) {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
}
},
it does hit my if statement and does hit the Sticky() function but I still think I have problems when I refresh the page whereas this JS isn't working on the interview-heading component for some reason.
Note the id="interview-heading" below.
const InterviewContent = Component({
componentDidMount() {
console.log("InterviewContent mounted");
},
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
const InterviewContentMain = Component({
componentDidMount() {
console.log("InterviewContentMain mounted");
},
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents company={company}/>
</div>
<div className="all-80">
<InterviewContainer company={company}/>
</div>
</div>
</div>
)
}
})
export default InterviewContent;
I realize TableOfContents is being rendered before InterviewContent because it's a child of TableOfContents and I believe in React children are rendered before their parents (inside-out)?
I think you need to rethink your component structure. I don't know your entire setup, but it looks like you should probably have a shared parent component pass the message from TableOfContents to InterviewContent:
const InterviewContentMain = Component({
getInitialState() {
return {
inkEnabled: false
}
},
componentDidMount() {
console.log("InterviewContentMain mounted");
},
enableInk() {
this.setState({ inkEnabled: true });
}
render(){
var company = this.props.company;
return (
<div id="interview-content" className="clear-both">
<div className="column-group">
<div className="all-20">
<TableOfContents inkEnabled={this.state.inkEnabled} company={company}/>
</div>
<div className="all-80">
<InterviewContainer enableInk={this.enableInk} company={company}/>
</div>
</div>
</div>
)
}
})
const TableOfContents = Component({
store: Store('/companies'),
componentDidMount() {
console.log("table of contents mounted");
this.props.enableInk();
},
...
const InterviewContent = Component({
enableInk() {
new Ink.UI.Sticky(el, {topElement: "#interview-heading", bottomElement: "#footer"});
},
// willReceiveProps isn't called on first mount, inkEnabled could be true so
componentDidMount() {
if (this.props.inkEnabled) {
this.enableInk();
}
},
componentWillReceiveProps(nextProps) {
if (this.props.inkEnabled === false && nextProps.inkEnabled === true) {
this.enableInk();
}
}
render(){
var company = this.props.company;
return (
<div id="ft-interview-content">
<p className="section-heading bold font-22" id="interview-heading">Interview</p>
<InterviewContentMain company={company}/>
</div>
)
}
})
Then have componentDidMount trigger this.props.enableInk().
Or better yet, why not just put the Ink.UI.Sticky call in componentDidMount of InterviewContent?
I have an object with the property home.ready = false. When the object is done getting data, cleaning it etc it changes to home.ready= true.
I need my component to register the change and update. My component:
class HomeNav extends React.Component {
render() {
let data = this.props.data;
let uniqueTabs = _.uniq(_.map(data, x => x.tab)).sort();
let tabs = uniqueTabs.map((tab, index) => {
let itemsByTab = _.filter(data, (x => x.tab == tab));
return <Tabs key={tab} tab={tab} index={index} data={itemsByTab} />;
});
console.log(this.props)
return (
<section>
<div className="wb-tabs">
<div className="tabpanels">
{ this.props.ready ? {tabs} : <p>Loading...</p> }
</div>
</div>
</section>
)
}
};
ReactDOM.render(
<HomeNav data={home.data.nav} ready={home.ready}/>,
document.getElementById('home-nav')
);
This is the home object. It's a simple object that gets data and once the data is ready the property ready changes from false to true. I can't get React to recognize that change. And at times React will say home is undefined.
Since you didn't post any code around the request, or data formatting, I will assume you got all that figured out. So, for your component to work the way it is currently written, you need to drop the curly braces around tabs ({ this.props.ready ? tabs : <p>Loading...</p> }), then, this.props.data should always contain a valid Array, otherwise it will break when you try to sort, filter, etc.
Or, you can do an early dropout, based on the ready property:
class HomeNav extends React.Component {
render() {
if(!this.props.ready){
return <section>
<div className="wb-tabs">
<div className="tabpanels">
<p>Loading...</p>
</div>
</div>
</section>
}
let data = this.props.data;
let uniqueTabs = _.uniq(_.map(data, x => x.tab)).sort();
let tabs = uniqueTabs.map((tab, index) => {
let itemsByTab = _.filter(data, (x => x.tab == tab));
return <Tabs key={tab} tab={tab} index={index} data={itemsByTab} />;
});
console.log(this.props)
return (
<section>
<div className="wb-tabs">
<div className="tabpanels">
{tabs}
</div>
</div>
</section>
)
}
};