Force stop container subscribtion (Meteor+React+CreateContainer) - reactjs

How can I manually stop the subscription and clean the collection on the client when you switch route via react-router?
The fact is that if I go to the page where there is a component subscribed to, such as "top 10 news" page, where published all the news, I see, as a container of news component performing a normal search the collection first finds objects with subscription last page.
export default createContainer((props)=>{
let {limitCount, userId} = props;
let query = (userId)?{'userId':userId}:{};
var handle = Meteor.subscribe('news_preview', query,limitCount);
var posts = Post.find(query, {sort:{createdAt:-1}}).fetch(); //Here, the container finds documents to which another component has been signed.
var needWait = !!handle.ready()&&!!posts;
return {
needWait:!needWait,
posts:posts
}
}, PostList)
After a moment, the container will complete its membership and will give us the relevant objects ...
How can I check that when linking the previous container has stopped its subscription and deleted objects?
In details. With the logic description
PostListContainer
This is a sample of NewsList component, with infinity scroll and subscribtion container. Is just detect scroll, and pass limitCount to child component.
export default class PostListContainer extends Component{
constructor(props){
super(props)
this.state={
limitCount:10,
}
this.handleScroll = this.handleScroll.bind(this);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll, false);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll, false);
}
handleScroll(event) {
let documentHeight = $(document).height(); //jquery is bad idea, i know
let windowHeight = $(window).height();
let scrollPosition = $(window).scrollTop();
let isLoading = this.refs.PostList.data.needWait;
if(scrollPosition+windowHeight>=documentHeight && !isLoading){
this.uppendSkipCount()
}
}
uppendSkipCount(){
let limitCount = this.state.limitCount;
this.setState({
limitCount:limitCount+5
});
}
render(){
let userId = (this.props.userId)?this.props.userId:undefined;
return(
<div>
<PostList ref="PostList" limitCount={this.state.limitCount} userId={userId} />
</div>
)
}
}
PostList
This component get properties, subscribe and render child components.
export class PostList extends Component {
constructor(props){
super(props);
}
postList(){
return(
<Masonry>
{this.props.posts.map((post)=>(
<div className="col-xs-12 col-sm-6 col-md-6 col-lg-4" key={post._id}>
<PostPreview post={post}/>
</div>
))}
</Masonry>
)
}
render() {
let content = (this.props.posts)?this.postList():undefined;
let loading = (this.props.needWait)?<Loading />:undefined;
return(
<div>
{content}
{loading}
</div>
)
}
}
export default createContainer((props)=>{
let {limitCount, userId} = props;
let query = (userId)?{'userId':userId}:{};
var handle = Meteor.subscribe('news_preview', query,limitCount);
var posts = Post.find(query, {sort:{createdAt:-1}}).fetch();
var needWait = !!handle.ready()&&!!posts;
return {
needWait:!needWait,
posts:posts
}
}, PostList)
And it work goood. But if i will come on the page from page that contain for example BestNews component i will get Posts object from that in first iteration:(
export class BestNews extends Component {
constructor(props){
super(props);
}
componentWillUnmount(){
this.props.handle.stop(); / looks like it help
}
goToPage(post){
browserHistory.push(`/news/item/${post._id}`);
}
bestPosts(){
return bestPosts = this.props.posts.map((post)=>{
return(
<div key={post._id}>
<ListItem
onTouchTap={()=>this.goToPage(post)}
leftAvatar={<Avatar src={post.author().getAvatar().link()} />}
rightIcon ={<ActionChromeReaderMode />}
primaryText={post.title}
secondaryText={post.description}
/>
<Divider />
</div>
)
});
}
render() {
let content = (this.props.needWait)?<Loading />:this.bestPosts();
return (
<div className="box-margin">
<Paper zDepth={1}>
<div className="row middle-xs center-xs">
<div className="col-xs-12">
<h2 style={{fontWeight:'300'}}>Интересные новости</h2>
</div>
</div>
<div className="row start-xs">
<div className="col-xs-12">
<List>
<Divider />
{content}
</List>
</div>
</div>
</Paper>
</div>
)
}
}
export default createContainer((props)=>{
var handle = Meteor.subscribe('best_news');
var posts = Post.find({}, {sort:{likes:-1}, limit:5}).fetch();
var needWait = !handle.ready() && !posts;
return {
handle:handle,
needWait:needWait,
posts:posts
}
}, BestNews)

I found a solution.
It was obvious. But somehow it seems to me that there is a better one.
In this case, we need to use a child component of the container. With method componentWillUnmount() we can manually stop the subscription.
What is negative? The fact that we need to transfer the subscription object in a child component, and then manually stop the subscription. And to do it in each component, which can cause a similar problem.
The answer to the question - why can not we just wait for the completion of the subscription of the container, and then - to display the content. The answer is - it is possible, but as long as you have not implemented infinite scrolling logic. Otherwise, each time the user scrolls down the content will be lost. As long as the subscription is completed.
It's funny, but I always thought that the container so stops the subscription. I was wrong?

Solution with manualy stop subscribtion is not good. You understand why.
Take a look at the pretty good solution.
We need to protect themselves from the fact that when you visit a component we can get irrelevant data with other components. Yes type have the same data, but - with the other components. It can play a cruel joke with s. I think you understand why.
At the same time we can not put a cap "loading" every time a user scrolls down and loaded data. Because at the time of subscription, all data will disappear. Option with named subscriptions you too are not satisfied. For example, if you are using Astronomy ORM by Jagi.
So. We need simply to intercept getting values ​​from the container and once finished to record the fact of the first subscription in the state of the component.
export class PostList extends Component {
constructor(props){
super(props);
this.state={
firstSubscribtion:false
}
}
componentWillReceiveProps(nextProps){
if(!this.state.firstSubscribtion && nextProps.handleReady){
this.setState({
firstSubscribtion:true
});
}
}
postList(){
return(
<Masonry>
{this.props.posts.map((post)=>(
<div className="col-xs-12 col-sm-6 col-md-6 col-lg-4" key={post._id}>
<PostPreview post={post}/>
</div>
))}
</Masonry>
)
}
render() {
let content = (this.props.posts && this.state.firstSubscribtion)?this.postList():undefined;
let loading = (this.props.needWait)?<Loading />:undefined;
return(
<div>
{content}
{loading}
</div>
)
}
}
export default createContainer((props)=>{
let {limitCount, userId} = props;
let query = (userId)?{'userId':userId}:{};
var handle = Meteor.subscribe('news_preview', query,limitCount);
var posts = Post.find(query, {sort:{createdAt:-1}}).fetch();
var needWait = !!handle.ready()&&!!posts;
return {
handleReady:!!handle.ready(),
needWait:!needWait,
posts:posts
}
}, PostList)

Related

How to make an icon clickable so it renders data below

I am trying to build a react page that shows a list of "messages subjects" received and when you click the down icon the messages relating to that subject appear directly below. (Imagine to help explain, when the user clicks the down icon on the line with 'Christmas' a white space needs to appear directly below and BEFORE the line with the 'New Year' text, so I can then display the message body, etc for each message relating to that subject.
Here is my code
import React from "react";
import "./Messages.css";
import { ReactComponent as DownIcon } from "../images/down-chevron.svg";
import Moment from 'moment';
class Messages extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
isLoading: false,
};
}
componentDidMount() {
this.setState({ isLoading: true });
const proxyurl = "https://cors-anywhere.herokuapp.com/";
const url =
"<my url>" +
this.props.location.state.userID;
fetch(proxyurl + url)
.then((res) => res.json())
.then((data) => this.setState({ data: data, isLoading: false }));
}
render() {
const { data, isLoading } = this.state;
if (isLoading) {
return <p>Loading ...</p>;
}
if (data.length === 0) {
return <p> no data found</p>;
}
return (
<div>
<div className="messageSubjectHeader">
<div className="innerMS">Message Subject</div>
<div className="innerMS">Number of linked messages</div>
<div className="innerMS">Latest message Date and Time</div>
<div className="innerMS">View Messages</div>
</div>
{data.message_Subjects.map((ms) => (
<div className="messageSubject">
<div className="innerMS">{ms.subject}</div>
<div className="innerMS">{ms.message_Chain.length}</div>
<div className="innerMS">{this.getLatestMessageDateTime(ms.message_Chain)}</div>
<div className="innerMS">
<DownIcon className="innerMSDownIcon" />
</div>
</div>
))}
</div>
);
}
getLatestMessageDateTime(messageChain){
const lastmessage = messageChain.length -1;
Moment.locale('en');
var dt = messageChain[lastmessage].dateTime;
return(Moment(dt).format('ddd DD MMM YYYY hh:mm:ss'))
}
}
export default Messages;
You have to define the selected record id in the state and Update the selected id on the click of view messages button. And also add a Content Panel inside the loop and toggle the visibility based on the selected recorded Id in the state.
{data.message_Subjects.map((ms) => (
<>
<div className="messageSubject">
<div className="innerMS">{ms.subject}</div>
<div className="innerMS">{ms.message_Chain.length}</div>
<div className="innerMS">{"12/08/2020"}</div>
<div className="innerMS">
<button onClick={() => this.handleClick(ms.id)}>
{this.state.selectedId === ms.id ? "hide" : "Show"}
</button>
</div>
</div>
// show/hide the content based on the selection --> Content Panel
{this.state.selectedId === ms.id && (
<div className="content">{ms.description}</div>
)}
</>
))}
I have created a sample Demo - https://codesandbox.io/s/vigorous-hertz-x89p8?file=/src/App.js
Let me know if your use case is different.
You should have an onClick() handler if you want something to happen when user clicks an element, and then define handleOnClick() elsewhere in the component.
And you have no sub-component for the messages of a particular dated group, so, you'll need to code that, too.
Sub-Messages Component
I see that you have not defined any sub-components for messages. I don't know how your API works, so, I'll be general in that regard, but you'll want a <DateMessages/> component. This should have a constructor and render like...
constructor(props) {
super(props);
this.state = {'messages':[]};
}
render() {
return (
this.state.map((message) => {
return message.date + ' ' message.text;
});
);
}
Then, you'll need to populate this. So, add it as a ref in your <Messages/> component, as a child of <div className="messageSubject">. Since it starts out with no messages, it should come out as blank when appended to each date group. That'll look like datekey = ms.subject; <DateMessages ref={(instance) => {this.$datekey = instance}} />.
onClick Handler
So, your onClick handler would look like: <div className="messageSubject" onClick={(e, ms.subject) => this.handleOnClick(e, ms.subject)}>. You might have a handleOnClick() like...
handleOnClick(e, ms.subject) {
var datekey = ms.subject;
this.$datekey.setState(this is where an array of messages for datekey would be stored);
}
Advantages
Why do it this way? By having the state accurately reflect the data that the user is seeing, you'll be taking advantage of all the speedups of using ReactJS.

ReactJS: How to map items in conditional statements

I have a react component that allows a user to click a button in the header and add different input types.
export default class ElementContainer extends Component {
render() {
return (
<div className="append-header">
<Headline buttonCheck={this.props.buttonCheck} />
<SubHeadline buttonCheck={this.props.buttonCheck} />
<URLButton={this.props.buttonCheck} />
</div>
)
}
}
I'm trying to implement react-beautiful-dnd into the project so that those inputs can be repositioned like a list in Trello.
Looking at that library (and other similar libraries), they use the data as lists in order to perform the drag and drop function.
The items in my app are added to the view by conditional rendering:
export default class InputShow extends Component {
render() {
const { node } = this.props;
return (
<div className="editor-div" >
{
(node.type === 'buttonA') ?
<textarea
//omitted code
id={node.id}
onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<URLButton
url={this.url}
id={node.id}
title={this.title}
/>
:
""
}
</div >
)
}
}
I've tried to map the items by creating a state for items (additions)
export default class InputShow extends Component {
constructor(props) {
super(props)
this.state = {
additions: []
}
}
render() {
const { node } = this.props;
return (
<div className="editor-div" >
{this.state.additions.map(addition => (
<div key={addition.id}>
{
(node.type === 'buttonA') ?
<textarea
//omitted code
id={node.id}
onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<URLButton
url={this.url}
id={node.id}
title={this.title}
/>
:
""
}
</div>
))}
</div >
)
}
}
I didn't get any errors, however now when I click on the buttons, no data is being displayed in the view. I've done simple maps in the past with API and local data but I've never tried it with ternary statements. Thanks for any feedback on a solution to this problem.
What you can do is separate the view logic from the code and create a functional component. Pass the values from the main as below:
{this.state.additions.map(addition => (
return <CustomTemplate id={addition.id}
nodeId={node.id} changeEvent={this.props.inputContentHandler}
nodeType={node.Type} url={this.url} title={this.title}/>))}
Create CustomTemplate something like this
const CustomeTemplate =(props)=>(
use props to get the values in the templates
)

My react component is rendering 1 too many child components

I'm new to react and struggling with working on why this is happening
I have a component, which takes an array, maps it and then for each element it renders a new component
class CallingPattern extends Component {
constructor(props) {
super(props);
const { serviceId } = this.props.match.params
this.state = {
serviceId: serviceId,
}
}
render() {
return (
<div>
<h1>CallingPattern page</h1>
<h4>Current Journey ServiceID: {this.state.serviceId}</h4>
<CallingStation serviceId={this.state.serviceId}/>
<div className="calling-stations">
// journeyData is a local JSON file
{journeyData.service.stops.map((stop, i) => {
{console.log("mapping")} // logs 8 times (length of array)
return (
<CallingStation
key={`station-${i}`}
stopInfo={stop}
serviceId={this.state.serviceId}
/>
);
})}
</div>
</div>
);
}
}
export default CallingPattern;
I'm expecting 8 CallingStations to be rendered (one for each in the array, which has a .length of 8). Here's CallingStation code:
class CallingStation extends Component {
render() {
console.log(`Rendered!`)
return (
<div>
<div>
Calling Station stop: { this.props.stopInfo ? this.props.stopInfo.location.crs : "" }
</div>
</div>
)
}
}
export default CallingStation;
On the page, there are 9 CallingStations (the first one doesn't have a 'this.props.stopInfo' but DOES have 'this.props.serviceId'.
Can anyone help me understand or point me in the direction of resources that are related?
The problem is here. There is an extra CallingStation in your render method :)
<h4>Current Journey ServiceID: {this.state.serviceId}</h4>
<CallingStation serviceId={this.state.serviceId}/>
In the JSON data the journeyData.service.stops.length will be 17. You can log it to the console using console.log(journeyData.service.stops.length) in your render method and remove this line <CallingStation serviceId={this.state.serviceId}/> it doesn't make sense before mapping. To work with large JSON data you better use a good json viewer i suggest this chrome extension it's fantastic JSON viewer Awesome.
render() {
console.log(journeyData.service.stops.length)
return (
<div>
<h1>CallingPattern page</h1>
<h4>Current Journey ServiceID: {this.state.serviceId}</h4>
<div className="calling-stations">
// journeyData is a local JSON file
{journeyData.service.stops.map((stop, i) => {
{console.log("mapping")} // logs 8 times (length of array)
return (
<CallingStation
key={`station-${i}`}
stopInfo={stop}
serviceId={this.state.serviceId}
/>
);
})}
</div>
</div>
);
}

React key cant add to element

Im new to React and feeling shy to ask this question,but I dont know why that when I try to bind key to li element,Icant find it after rendering DOM.
Which looks like:
<ul><br />
<li>I dont know why</li><br />
<li>cant find the key</li><br />
</ul><br />
in console.
And I got 'undefined' with e.target.getAttribute('key') in click event.
Here my code (a simple todolist):
class Write extends React.Component {
constructor(props){
super(props);
this.handle=this.handle.bind(this);
}
handle(){
const text=document.getElementById('todoIn').value;
this.props.onWriteDown(text);
}
render(){
return (
<div>
<input type="text" id="todoIn" />
<button onClick={this.handle}>confirm</button>
</div>
);
}
}
class Todolist extends React.Component {
constructor (props) {
super(props);
this.n=0;
this.state={list:[]};
this.todolist=[];
this.handle=this.handle.bind(this);
this.handle_del=this.handle_del.bind(this);
}
handle (m) {
this.todolist.push({thing:m,vid:this.n++});
this.setState({list:this.todolist});
}
handle_del (e) {
var d = this.todolist.forEach((v,i)=>{
if(v.vid==e.target.getAttribute('key')){
return i;
}
});
this.todolist.splice(d,1);
this.setState({list:this.todolist});
}
render(){
var that = this;
var todo=[];
//here I create React Element for each todo and bind some attribute BUT ONLY CLICK EVENT is ACCESSIBLE.I JUST WONDER...
this.state.list.forEach(function(v,i,a){
let temp=<li key={v.vid.toString()} onClick={that.handle_del} >{v.thing}</li>;
todo.push(temp);
});
return(
<div>
<Write onWriteDown={this.handle} />
<ul>
{todo}
</ul>
</div>
);
}
}
ReactDOM.render(
<Todolist />,
document.getElementById('root')
);
<div id="root"></div>
key is used internally so React knows which components have been added, changed or removed.
Also, keys don't get passed through.
For more information check ReactJS's doc.
https://facebook.github.io/react/docs/lists-and-keys.html#keys
As said in Hemerson's answer, you shouldn't be attempting to access the key attribute, it isn't passed through, and is only used as an internal reference to the object.
Instead, you can write your code as so:
let id = v.vid.toString();
let temp=<li key={id} onClick={() => that.handle_del(id)}>{v.thing}</li>;
handle_del (id) {
var d = this.todolist.forEach((v,i)=>{
if(v.vid==id){
return i;
}
});
this.todolist.splice(d,1);
this.setState({list:this.todolist});
}

User react router inside of map

I would like to use react-router (must use 2.8.1) for rendering content inside a list (using map).
However, if I display {this.props.children} outside the .map, it renders one at time.
I need it to display inline/under the list entry.
How can I achieve this?
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
movies: x.movies
};
}
render() {
return (
<div>
<h2>Repos</h2>
{x.movies.map(movie =>
<span key={movie.id}>
{movie.name}
<NavLink to={"/repos/" + movie.id + "/next"}>React Router</NavLink>
</span>
)}
{this.props.children}
</div>
);
}
}
You are rendering the children inside the loop, which i believe is causing the extra Next Id:4 to be displayed between each entry.
Try the below, by rendering the children outside the loop.
{
x.movies.map(movie => (
<span>
<Result key={movie.id} result= {movie}/>
</span>
))
}
<span>{this.props.children}</span>

Resources