Get meteor to unsubscribe from data - reactjs

I'm not sure if this is possible due to the way meteor works. I'm trying to figure out how to unsubscribe and subscribe to collections and have the data removed from mini mongo on the client side when the user clicks a button. The problem I have in the example below is that when a user clicks the handleButtonAllCompanies all the data is delivered to the client and then if they click handleButtonTop100 it doesn't resubscribe. So the data on the client side doesn't change. Is it possible to do this?
Path: CompaniesPage.jsx
export default class CompaniesPage extends Component {
constructor(props) {
super(props);
this.handleButtonAllCompanies = this.handleButtonAllCompanies.bind(this);
this.handleButtonTop100 = this.handleButtonTop100.bind(this);
}
handleButtonAllCompanies(event) {
event.preventDefault();
Meteor.subscribe('companiesAll');
}
handleButtonTop100(event) {
event.preventDefault();
Meteor.subscribe('companiesTop100');
}
render() {
// console.log(1, this.props.companiesASX);
return (
<div>
<Button color="primary" onClick={this.handleButtonAllCompanies}>All</Button>
<Button color="primary" onClick={this.handleButtonTop100}>Top 100</Button>
</div>
);
}
}
Path: Publish.js
Meteor.publish('admin.handleButtonAllCompanies', function() {
return CompaniesASX.find({});
});
Meteor.publish('admin.handleButtonTop100', function() {
return CompaniesASX.find({}, { limit: 100});
});

This is definitely possible, but the way to do that is to fix your publication. You want to think MVC, i.e., separate as much as possible the data and mode from the way you are going to present it. This means that you should not maintain two publications of the same collection, for two specific purposes. Rather you want to reuse the same publication, but just change the parameters as needed.
Meteor.publish('companies', function(limit) {
if (limit) {
return CompaniesASX.find({}, { limit });
} else {
return CompaniesASX.find({});
}
});
Then you can define your button handlers as:
handleButtonAllCompanies(event) {
event.preventDefault();
Meteor.subscribe('companies');
}
handleButtonTop100(event) {
event.preventDefault();
Meteor.subscribe('companies', 100);
}
This way you are changing an existing subscription, rather than creating a new one.
I'm not yet super familiar with react in meteor. But in blaze you wouldn't even need to re-subscribe. You would just provide a reactive variable as the subscription parameter and whenever that would change, meteor would update the subscription reactively. The same may be possible in react, but I'm not sure how.

Ok, first thanks to #Christian Fritz, his suggestion got me onto the right track. I hope this helps someone else.
I didn't realise that subscriptions should be controlled within the Container component, not in the page component. By using Session.set and Session.get I'm able to update the Container component which updates the subscription.
This works (if there is a better or more meteor way please post) and I hope this helps others if they come across a similar problem.
Path CompaniesContainer.js
export default UploadCSVFileContainer = withTracker(({ match }) => {
const limit = Session.get('limit');
const companiesHandle = Meteor.subscribe('companies', limit);
const companiesLoading = !companiesHandle.ready();
const companies = Companies.find().fetch();
const companiesExists = !companiesLoading && !!companies;
return {
companiesLoading,
companies,
companiesExists,
showCompanies: companiesExists ? Companies.find().fetch() : []
};
})(UploadCSVFilePage);
Path: CompaniesPage.jsx
export default class CompaniesPage extends Component {
constructor(props) {
super(props);
this.handleButtonAllCompanies = this.handleButtonAllCompanies.bind(this);
this.handleButtonTop100 = this.handleButtonTop100.bind(this);
}
handleButtonAllCompanies(event) {
event.preventDefault();
// mongodb limit value of 0 is equivalent to setting no limit
Session.set('limit', 0)
}
handleButtonTop100(event) {
event.preventDefault();
Session.set('limit', 100)
}
render() {
return (
<div>
<Button color="primary" onClick={this.handleButtonAllCompanies}>All</Button>
<Button color="primary" onClick={this.handleButtonTop100}>Top 100</Button>
</div>
);
}
}
Path: Publish.js
Meteor.publish('companies', function() {
if (limit || limit === 0) {
return Companies.find({}, { limit: limit });
}
});

Path CompaniesContainer.js
export default CompaniesContainer = withTracker(() => {
let companiesHandle; // or fire subscribe here if you want the data to be loaded initially
const getCompanies = (limit) => {
companiesHandle = Meteor.subscribe('companies', limit);
}
return {
getCompanies,
companiesLoading: !companiesHandle.ready(),
companies: Companies.find().fetch(),
};
})(CompaniesPage);
Path: CompaniesPage.jsx
export default class CompaniesPage extends Component {
constructor(props) {
super(props);
this.handleButtonAllCompanies = this.handleButtonAllCompanies.bind(this);
this.handleButtonTop100 = this.handleButtonTop100.bind(this);
}
getCompanies(limit) {
this.props.getCompanies(limit);
}
handleButtonAllCompanies(event) {
event.preventDefault();
// mongodb limit value of 0 is equivalent to setting no limit
this.getCompanies(0);
}
handleButtonTop100(event) {
event.preventDefault();
this.getCompanies(100);
}
render() {
return (
<div>
<Button color="primary" onClick={this.handleButtonAllCompanies}>All</Button>
<Button color="primary" onClick={this.handleButtonTop100}>Top 100</Button>
</div>
);
}
}
Path: Publish.js
Meteor.publish('companies', function(limit) {
if (!limit) { limit = 0; }
return Companies.find({}, { limit: limit });
});

Related

Why is setState being reset?

I'm working on a community site where users can share and comment on websites (here it is)[https://beta.getkelvin.com/]. Websites can be filtered by category, but when a user goes into a specific page then backs out, the filter is removed.
Here's the process in steps:
User selects filter from drop down list, and this.state.topicSelected reflects the new value
User clicks a link to see a show page of an instance (a summary of a website), this.state.topicSelected reflects the correct value
User goes back to main page, and this.state.topicSelected is reverted back to 0
Instead of reverting back to 0, I want the value to still apply so the filter remains on the same category that the user selected before.
The problem seems to be that getInitialState is resetting the value of this.state.topicSelected back to 0 (as it's written in the code). When I try to put a dynamic value in 0's place, I get an undefined error.
Here's the getInitialState code:
var SitesArea = React.createClass({
getInitialState: function () {
return {
sortSelected: "most-recent",
topicSelected: 0 // need to overwrite with value of this.state.topicSelected when user selects filter
// I removed other attributes to clean it up for your sake
}
}
On here's the event:
onTopicClick: function (e) {
e.preventDefault();
this.setState({topicSelected: Number(e.target.value)});
if (Number(e.target.value) == 0) {
if (this.state.sortSelected == 'highest-score') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.score});
});
} else if (this.state.sortSelected == 'most-remarked') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.remarkable});
});
} else if (this.state.sortSelected == 'most-visited') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.visited});
});
} else if (this.state.sortSelected == 'most-recent') {
this.setState({sites: []}, function () {
this.setState({sites: this.state.recent});
});
}
} else {
this.getSites(this.state.sortSelected, Number(e.target.value));
this.setState({sites: []}, function () {
this.setState({sites: this.state.filter_sites});
});
}
And lastly, the dropdown menu:
<select
value={this.state.topicSelected}
onChange={this.onTopicClick}
className="sort"
data-element>
{
// Add this option in the .then() when populating siteCategories()
[<option key='0'value='0'>Topics</option>].concat(
this.state.topics.map(function (topic) {
return (<option
key={topic.id}
value={topic.id}>{topic.name}</option>);
}))
}
How do I get it so that this.state.topicSelected doesn't get reset when a user goes back to the main page?
I think your main page is getting unmounted (destroyed) when the user navigates from the main page to the summary page. React creates a brand new instance of the main page component when they return. That reconstruction initializes the selected topic back to 0.
Here is a codepen that shows what might be happening. Foo == your main page, Bar == summary page. Click Increment topic a couple times, then navigate from Foo to Bar and back again. Console logs show that the Foo component gets unmounted when you navigate away, and reconstructed on return.
Note You seem to be using an older version of react, as evidenced by the presence of getInitialState and React.createClass. My pen follows the more modern approach of initializing state in the class constructor.
To solve the problem, you have to save that state outside the main component in something that isn't getting deleted and re-created as they navigate. Here are some choices for doing that
Expose an onTopicSelected event from your main page. The parent of the main page would pass in a function handler as a prop to hook that event. The handler should save the selected topic in the component state of the parent. This is kind of messy solution because the parent usually should not know or care about the internal state of its children
Stuff the selected topic into something that isn't react, like window.props. This is an ugly idea as well.
Learn about redux and plug it into your app.
Redux is the cleanest way to store this state, but it would require a bit of learning. I have implemented the first solution in this codepen if you want to see what it would look like.
The original codepen showing the problem is pasted below as a snippet. Switch to Full page mode if you try to run it here.
//jshint esnext:true
const Header = (props) => {
const {selectedPage, onNavigationChange} = props;
const disableFoo = selectedPage == 'foo'
const disableBar = selectedPage == 'bar';
return (
<div>
<h1>Header Component : {selectedPage}</h1>
<button disabled={disableFoo} onClick={() => onNavigationChange('foo')}>Foo page</button>
<button disabled={disableBar} onClick={() => onNavigationChange('bar')}>Bar page</button>
<hr/>
</div>
);
};
class Foo extends React.Component {
constructor(props) {
console.log('Foo constructor');
super(props);
this.state = {
topicSelected: 0
};
this.incrementTopic = this.incrementTopic.bind(this);
}
incrementTopic() {
const {topicSelected} = this.state
const newTopic = topicSelected + 1
console.log(`incrementing topic: old=${topicSelected} new=${newTopic}`)
this.setState({
topicSelected: newTopic
})
}
render() {
console.log('Foo::render');
return (<div>
<h2>The Foo content page : topicSelected={this.state.topicSelected}</h2>
<button onClick={this.incrementTopic}>Increment topic</button>
</div>
);
}
componentWillMount() {
console.log('Foo::componentWillMount');
}
componentDidMount() {
console.log('Foo::componentDidMount');
}
componentWillUnmount() {
console.log('Foo::componentWillUnmount');
}
componentWillUpdate() {
console.log('Foo::componentWillUpdate');
}
componentDidUpdate() {
console.log('Foo::componentDidUpdate');
}
}
const Bar = (props) => {
console.log('Bar::render');
return <h2>The Bar content page</h2>
}
const Body = (props) => {
console.log('Body::render');
const {selectedPage} = props;
if (selectedPage == 'foo') {
return <Foo/>;
} else if (selectedPage == 'bar') {
return <Bar/>
}
};
class Application extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedPage: 'foo'
};
}
render() {
console.log('Application::render');
const {selectedPage} = this.state;
const navigationChange = (nextPage) => {
this.setState({
selectedPage: nextPage
})
}
return (
<div>
<Header selectedPage={selectedPage} onNavigationChange={navigationChange}/>
<Body selectedPage={selectedPage}/>
</div>
);
}
}
ReactDOM.render(<Application/>,
document.getElementById('main')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="main"></div>

Relay local state management. How to add and use flag on client side?

The problem:
I want to have simple boolean flag that will be true when modal is opened and false when it is closed. And I want to update other components reactively depends on that flag
I hope there is a way to do it with relay only (Apollo has a solution for that). I don't want to connect redux of mobx or something like that (It is just simple boolean flag!).
What I already have:
It is possible to use commitLocalUpdate in order to modify your state.
Indeed I was able to create and modify my new flag like that:
class ModalComponent extends PureComponent {
componentDidMount() {
// Here I either create or update value if it exists
commitLocalUpdate(environment, (store) => {
if (!store.get('isModalOpened')) {
store.create('isModalOpened', 'Boolean').setValue(true);
} else {
store.get('isModalOpened').setValue(true);
}
});
}
componentWillUnmount() {
// Here I mark flag as false
commitLocalUpdate(environment, (store) => {
store.get('isModalOpened').setValue(false);
});
}
render() {
// This is just react component so you have full picture
return ReactDOM.createPortal(
<div
className={ styles.modalContainer }
>
dummy modal
</div>,
document.getElementById('modal'),
);
}
}
The challenge:
How to update other components reactively depends on that flag?
I can't fetch my flag like this:
const MyComponent = (props) => {
return (
<QueryRenderer
environment={ environment }
query={ graphql`
query MyComponentQuery {
isModalOpened
}`
} //PROBLEM IS HERE GraphQLParser: Unknown field `isModalOpened` on type `Query`
render={ ({ error, props: data, retry }) => {
return (
<div>
{data.isModalOpened}
<div/>
);
} }
/>);
};
Because Relay compiler throws me an error: GraphQLParser: Unknown field 'isModalOpened' on type 'Query'.
And the last problem:
How to avoid server request?
That information is stored on client side so there is no need for request.
I know there a few maybe similar questions like that and that. But they doesn't ask most difficult part of reactive update and answers are outdated.
If you need to store just one flag as you said, I recommend you to use React Context instead of Relay. You could do next:
Add Context to App component:
const ModalContext = React.createContext('modal');
export class App extends React.Component {
constructor() {
super();
this.state = {
isModalOpened: false
}
}
toggleModal = (value) => {
this.setState({
isModalOpened: value
})
};
getModalContextValue() {
return {
isModalOpened: this.state.isModalOpened,
toggleModal: this.toggleModal
}
}
render() {
return (
<ModalContext.Provider value={this.getModalContextValue()}>
//your child components
</ModalContext.Provider>
)
}
}
Get value from context everywhere you want:
const MyComponent = (props) => {
const { isModalOpened } = useContext(ModalContext);
return (
<div>
{isModalOpened}
</div>
);
};
If you will use this solution you will get rid of using additional libraries such as Relay and server requests.

Reactjs - Not able to render in my component's this.state.item values from my servlet api

From my ReactJS application, am submitting a JSONRequest to External system via Java Servlet and getting response from that Servlet. Those response i have to set it in my React component.
But, am having issue. That am not able to set the values for the very first response. If i manually refresh the page those values are setting properly. And then, if i submitting request for 2nd time with the same form values that time am able to render the response values without any manual refresh.
During very first time response of the form am unable to set the response value.
Please find my below code that am trying.
class Orders extends React.Component {
constructor(props) {
super(props);
this.publishToOracle = this.publishToOracle.bind(this);
}
publishToOracle(id) {
var item=this.state.item;
ajax({
url: 'api/publish_oracle',
method: 'POST',
data: { id: id }
},
(data) => {
if (data.error == null) {
alert('Order has been published to Oracle Successfully')
item.oracle_number=data.result.oracle_number;
this.setState({ view: 'Details', item: data.result});
console.log("Order Number-->"+JSON.stringify(this.state.item.oracle_number)); // Oracle Number is printing here every time. But not in the component.
} else if (data.error != null) {
alert(data.error);
}
});
}
render() {
if (this.state.view == 'Details') {
return (
<Details
item={ this.state.item }
publishToOracle={this.publishToOracle}
/>
);
}
}
}
class Details extends React.Component {
constructor(props) {
super(props);
this.state = { item: props.item };
}
handlePublishToOracle(e) {
var item = this.state.item;
alert("Publishing to Oracle");
e.preventDefault();
item["oracle_order_number"] = "";
item["oracle_response_message"]="";
item["oracle_response_status"]="";
this.props.publishToOracle(item.id);
}
handleActionButtons() {
if(this.state.action == "PublishToOracle") {
return (
<button
type="submit"
onClick={ this.handlePublishToOracle}
>
Publish To Oracle
</button>
);
}
}
render() {
return (
<select
name="action"
value={this.state.action}
onChange={this.handleAction}
>
<option value="PublishToOracle"> Publish To Oracle</option>
</select>
{ this.handleActionButtons() }
<input
name="oracle_number"
type="text"
maxLength="20"
value={ this.state.item.oracle_number }
/>
);
}
}
Actually, the functionality of the API is to get the response save it in my MongoDB and render in React UI.
This problem is due to the below elements are undefined for the very first time. so am creating those elements with empty value and once we get the response from the api, its settings those values in the created elements and its works fine.
item["oracle_order_number"] = "";
item["oracle_response_message"]="";
item["oracle_response_status"]="";
this.props.publishToOracleEBS(item);
This solution has been updated in my questions too.

How to render a long list in react-relay?

A lazy scrolling list component for react-web
backing onto a Relay Connection, with paginated fetching,
while offering good performance and memory characteristics.
Two things to manage
Data fetching through providing pagination parameters to query, manipulation of the relay store
Component rendering, manipulation of the virtual DOM
Is there some particular list component which handles this well?
Is there an established pattern for implementing this common mechanism?
This pattern is pretty much the representative scenario for connections. Here's a hypothetical <PostsIndex> component that shows a list of posts with a "load more" button. If you don't want to explicitly change the UI when in the isLoading state you could delete the constructor and the setVariables callback. Adding viewport based infinite scrolling would not be hard either; you'd just need to wire a scroll listener up to you setVariables call.
class PostsIndex extends React.Component {
constructor(props) {
super(props);
this.state = {isLoading: false};
}
_handleLoadMore = () => {
this.props.relay.setVariables({
count: this.props.relay.variables.count + 10,
}, ({ready, done, error, aborted}) => {
this.setState({isLoading: !ready && !(done || error || aborted)});
});
}
render() {
return (
<div>
{
this.props.viewer.posts.edges.map(({node}) => (
<Post key={node.id} post={node} />
))
}
{
this.props.viewer.posts.pageInfo.hasNextPage ?
<LoadMoreButton
isLoading={this.state.isLoading}
onLoadMore={this._handleLoadMore}
/> :
null
}
</div>
);
}
}
export default Relay.createContainer(PostsIndex, {
initialVariables: {
count: 10,
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
posts(first: $count) {
edges {
node {
id
${Post.getFragment('post')}
}
}
pageInfo {
hasNextPage
}
}
}
`,
},
});

Differentiate between single and double click in React

Trying to find out what is the best way of differentiating between single and double click in a React component.
The built-in mouse events (onClick and onDoubleClick) work fine if you don't want apply them on the same DOM element. There is the jQuery dblclick which I don't want to use. There is also a React Timer Mixin which could be used, but I use ES6 classes so then I would need to use something like React Mixin and I just think it's an overkill for doing such a simple thing.
From what I found out people would set the timeout on the click event, so I followed that direction and came with this:
import React, { Component } from 'react'
export default class Click extends Component {
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
this.state = {
clickCount: 0
}
}
handleClick() {
let clickCount = this.state.clickCount
let singleClickTimer
clickCount++
this.setState({ clickCount: this.state.clickCount + 1 })
if (clickCount === 1) {
singleClickTimer = setTimeout(() => {
if (this.state.clickCount === 1) {
clickCount = 0
this.setState({ clickCount: 0 })
console.log('single click')
}
}, 300)
} else if (clickCount === 2) {
clearTimeout(singleClickTimer)
clickCount = 0
this.setState({ clickCount: 0 })
console.log('double click')
}
}
render() {
return (
<div>
<button onClick={this.handleClick}>Click me</button>
</div>
)
}
}
It works, but I'm trying to find out if there is a better way of doing it.
Edit:
The big problem with this is that setTimeout is async, so I added another check (if (this.state.clickCount === 1) {), but that seems wrong to me.
Another problem with this is the delay, the double click is called immediately, but the single click will be called after whatever timeout was set (around 300ms).
I know this is an old question but I spun my wheels on this problem a bit also, so thought I'd share my solution for those who also end up here.
const DOUBLECLICK_TIMEOUT = 300;
handleImagePress = () => {
this.setState({ pressCount: this.state.pressCount+1 });
setTimeout(() => {
if (this.state.pressCount == 0) { return }
if (this.state.pressCount == 1) {
// Do single press action
} else if (this.state.pressCount == 2) {
// Do double press action
}
this.setState({ pressCount: 0 });
}, DOUBLECLICK_TIMEOUT)
}

Resources