Meteor - how to give tracker autorun a callback - reactjs

I have a little piece of code that renders data from the database according to the path name. My only problem is that when I try to retrieve that data, using this.state.note._id it returns an error that says it cannot find _id of undefined. How would I access my object that is put into a state? It only gives the error when I try to access the items inside the object such as _id
import React from "react";
import { Tracker } from "meteor/tracker";
import { Notes } from "../methods/methods";
export default class fullSize extends React.Component{
constructor(props){
super(props);
this.state = {
note: [],
document: (<div></div>)
};
}
componentWillMount() {
this.tracker = Tracker.autorun(() => {
Meteor.subscribe('notes');
let note = Notes.find({_id: this.props.match.params.noteId}).fetch()
this.setState({ note: note[0] });
});
}
renderDocument(){
console.log(this.state.note);
return <p>Hi</p>
}
componentWillUnmount() {
this.tracker.stop();
}
render(){
return <div>{this.renderDocument()}</div>
}
}
I know that the reason it is returning undefined is because (correct me if I am wrong) the page is rendering the function before the the tracker could refresh the data. How would I get like some sort of callback when the tracker receives some data it will call the renderDocument function?

You're initializing your note state as an array but then you're setting it to a scalar later. You're also not checking to see if the subscription is ready which means that you end up trying to get the state when it is still empty. The tracker will run anytime a reactive data source inside it changes. This means you don't need a callback, you just add any code you want to run inside the tracker itself.
You also don't need a state variable for the document contents itself, your render function can just return a <div /> until the subscription becomes ready.
Note also that .findOne() is equivalent to .find().fetch()[0] - it returns a single document.
When you're searching on _id you can shorthand your query to .findOne(id) instead of .findOne({_id: id})
import React from "react";
import { Tracker } from "meteor/tracker";
import { Notes } from "../methods/methods";
export default class fullSize extends React.Component{
constructor(props){
super(props);
this.state = {
note: null
};
}
componentWillMount() {
const sub = Meteor.subscribe('notes');
this.tracker = Tracker.autorun(() => {
if (sub.ready) this.setState({ note: Notes.findOne(this.props.match.params.noteId) });
});
}
renderDocument(){
return this.state.note ? <p>Hi</p> : <div />;
}
componentWillUnmount() {
this.tracker.stop();
}
render(){
return <div>{this.renderDocument()}</div>
}
}

Related

Rendering loop error with react axios setState

I have different ingredients(vodka, gin, whiskey...) json files in a dummy folder.
I have an IngredientList.js where I select one ingredient and pass it down to
IngredientSearch.js
The IngredientSearch.js gets the relevant json file based on the ingredient name and then I set the state of ingredientRes to the res.data.drinks
Problem I am getting is that when I print the console.log(newVals) --> the console logs the arrays from the json infinitely. Seems like I am rerendering something infinitely.
What is wrong with my setup?
IngredientSearch.js:
import React, { Component } from 'react';
import axios from 'axios';
class IngredientSearch extends Component {
constructor() {
super();
this.state = {
ingredientRes: []
};
}
componentDidUpdate(prevProps) {
let ingredient = this.props.ingredient; //for example: vodka
this.getIngredient_drinks(ingredient);
}
getIngredient_drinks = (ingredient) => {
if(ingredient !== null) {
axios.get(`../dummy/${ingredient}.json`)
.then((res)=>{
let newVals = [];
newVals.push(res.data.drinks);
//console.log(newVals); // keeps relogging the arrays
this.setState({ ingredientRes: newVals });
}).catch((err)=>{
console.log(err);
})
}
}
render() {
return (
<div>
IngredientSearch Results
I want to map the ingredientRes here
</div>
)
}
}
export default IngredientSearch;
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like -
if (this.props.ingredient !== prevProps.ingredient) {
this.getIngredient_drinks(ingredient);
}
Otherwise it will cause an infinite loop.
For reference - https://reactjs.org/docs/react-component.html#componentdidupdate

When mapStateToProps in create create a new axios call

I have created a Reactjs component that receives a mapStateToProps function call. Everything works fine except the ajax call using Axios.
The class on a mapStateToProps update needs to call the server and add its payload to the state of the component and update the textarea.
The error I am getting from the console is,
ReactDOMIDOperations.js:47 Uncaught RangeError: Maximum call stack size exceeded
Below is what I have so far. Can anyone show me how to fix this issue?
import React from "react";
import { connect } from "react-redux";
import ApiCalls from "../../../utils/ApiCalls";
const mapStateToProps = state => {
return { passFilePath: state.passFilePath };
};
/**
* This component is a template to display
* widgets of information
*/
class IdeTextEditorClass extends React.Component {
constructor() {
super();
this.state = {
newData: [],
pathData: []
}
}
/**
* Received request from server add it to
* react component so that it can be rendered
*/
componentDidUpdate() {
try {
this.setState({ pathData: this.props.passFilePath[this.props.passFilePath.length - 1] });
} catch (err) {
this.setState({ pathData: '' });
}
console.log('path', this.state.pathData.data);
ApiCalls.readSassFile(this.state.pathData.data)
.then(function (serverData) {
this.setState({ newData: serverData[0].data })
}.bind(this));
}
render() {
try {
this.state.newData
} catch (err) {
this.setState({ newData: '' });
}
return (
<fieldset>
<input type="text" value={this.state.pathData.data} />
<textarea id="ide-text-area" name="ide-text-area" value={this.state.newData} /></fieldset>
)
}
}
const IdeTextEditor = connect(mapStateToProps)(IdeTextEditorClass);
export default IdeTextEditor;
class IdeTextEditorClass extends React.Component {
constructor() {
super();
/*
based on your original code it seems the default data should be empty string ,as you set them to be empty string when you cannot get data from server.
*/
this.state = {
newData: '',
pathData: ''
}
}
/**
* Received request from server add it to
* react component so that it can be rendered
*/
componentDidMount() {
try {
this.setState({ pathData: this.props.passFilePath[this.props.passFilePath.length - 1] });
} catch (err) {
this.setState({ pathData: '' });
}
console.log('path', this.state.pathData.data);
ApiCalls.readSassFile(this.state.pathData.data)
.then(function (serverData) {
this.setState({ newData: serverData[0].data })
}.bind(this));
}
render() {
//by default your newData is already empty string. so skip the checking here.
let path = this.state.pathData ? this.state.pathData.data : '';
return (
<fieldset>
<input type="text" value={path} />
<textarea id="ide-text-area" name="ide-text-area" value={this.state.newData} /></fieldset>
)
}
}
Explanation:
The major change is to change componentDidUpdate to componentDidMount.
Putting the data initializing logic in componentDidMount because:
called only once, thus avoiding the endless update loop mentioned in the comments. Also, initialization logic is usually expected here.
this method is called after initial render, so you can at least display something to user during the wait for data (from server). for example, in your render method, you can check newData and if it is not available, display a loading icon. Then React calls componentDidMount, and fetch your data -> update state -> trigger render again -> displays your input / text area using new data fetched from server. Of course, if you don't want to bother showing a loading icon, it is also fine, because your view will probably be updated quickly, when the ajax call returns.

How to manage read-only files that don't have to be updated using React+Flux

I'm using React-Flux on an app I'm working on and I've hit a wall, so I have to make sure that so far I'm using everything properly.
I have a json file that is only for reading purposes. It should never change dynamically. I'm getting the file data from an Ajax request I'm performing in my AppActions file. So far so good. Now I'm trying to use the stored data. My question is this: In the AppStore file should I add an emitChange event to the corresponding case? How should I manage read-only files?
This whole thing came up when I tried to use the data in a component I was building and if I wasn't using an event listener inside the componentWillMount and componentWillUnmount functions I couldn't get the stored data every time I refreshed the page.
const AppStore = new AppStoreClass();
AppStore.dispatchToken = AppDispatcher.register(action =>{
switch(action.actionType){
case AppConstants.GET_NAMES:
setNames(action.names);
AppStore.emitChange();
break;
}
});
export default AppStore;
This is the Nameday component where I use the read-only data.
class Nameday extends Component {
constructor(){
super();
this.state = {
saints: AppStore.getNames(),
}
this.onChange = this.onChange.bind(this);
}
componentWillMount(){
AppStore.addChangeListener(this.onChange);
}
componentDidMount(){
AppActions.getNames();
}
onChange(){
this.setState({
saints: AppStore.getNames(),
});
}
componentWillUnmount(){
AppStore.removeChangeListener(this.onChange);
}
render() {
let dates = [];
this.state.saints.map((saint, i) => {
saint.names.map((name, j) => {
if (name === firstName) {
dates.push(saint.date);
}
return dates;
});
return null;
});
let option = ....
return (
option
);
}
}

setState in reactjs inside success function not setting state

I am using a ParsePlatform as backend storage and reactjs as front end. I am able to get the parse data using Parse.Query but unable to use the returned values as I do not know how to set the state from the successfull fetching of results. I tried like this way inside componentDidMount()
import React from 'react'
import Parse from 'parse'
class ConferenceInfo extends React.Component {
state={
someData:null
}
componentDidMount(){
this.getConferenceInfo()
}
getConferenceInfo(){
var ConferenceListing = Parse.Object.extend("ConferenceListing");
var cl = new Parse.Query(ConferenceListing);
cl.get("8glBIjeRrC", {
success: function(cl) {
// The object was retrieved successfully.
alert(cl.get("someData")) //it works
//this.setState({someData:cl.get("someData")}) not working
},
error: function(object, error) {
// The object was not retrieved successfully.
// error is a Parse.Error with an error code and message.
}
});
}
render() {
return (
<div>
{this.state.someData} //no result
</div>
)
}
}
export default ConferenceInfo
This is because this is not in the same scope. You are calling it inside success property, function callback.
To get it working we need to bind this.
First Create constructor method.
constructor(props) {
super(props);
// Our new bind method ref
this.change = this.change.bind(this);
}
Then add new method change
change(obj) {
this.setState(obj);
}
Finally your success callback
success: function(cl) {
// ...
this.change({ someData: cl.get("someData") })
},

componentDidMount() not being called when react component is mounted

I've been attempting to fetch some data from a server and for some odd reason componentDidMount() is not firing as it should be. I added a console.log() statement inside of componentDidMount() to check if it was firing. I know the request to the server works as it should As I used it outside of react and it worked as it should.
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
obj: {}
};
};
getAllStarShips () {
reachGraphQL('http://localhost:4000/', `{
allStarships(first: 7) {
edges {
node {
id
name
model
costInCredits
pilotConnection {
edges {
node {
...pilotFragment
}
}
}
}
}
}
}
fragment pilotFragment on Person {
name
homeworld { name }
}`, {}). then((data) => {
console.log('getALL:', JSON.stringify(data, null, 2))
this.setState({
obj: data
});
});
}
componentDidMount() {
console.log('Check to see if firing')
this.getAllStarShips();
}
render() {
console.log('state:',JSON.stringify(this.state.obj, null, 2));
return (
<div>
<h1>React-Reach!</h1>
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
</div>
);
}
}
render(
<App></App>,
document.getElementById('app')
);
The issue here is that the render method is crashing, because the following line is generating an error
<p>{this.state.obj.allStarships.edges[1].node.name}</p>
Fix this to not use this.state.obj.allStarships.edges[1].node.name directly, unless you can guarantee that each receiver is defined.
Check your component's key
Another thing that will cause this to happen is if your component does not have a key. In React, the key property is used to determine whether a change is just new properties for a component or if the change is a new component.
React will only unmount the old component and mount a new one if the key changed. If you're seeing cases where componentDidMount() is not being called, make sure your component has a unique key.
With the key set, React will interpret them as different components and handle unmounting and mounting.
Example Without a Key:
<SomeComponent prop1={foo} />
Example with a Key
const key = foo.getUniqueId()
<SomeComponent key={key} prop1={foo} />
Also check that you don't have more than one componentDidMount if you have a component with a lot of code. It's a good idea to keep lifecycle methods near the top after the constructor.
I encountered this issue (componentDidMount() not being called) because my component was adding an attribute to the component state in the constructor, but not in the Component declaration. It caused a runtime failure.
Problem:
class Abc extends React.Component<props, {}> {
this.state = { newAttr: false }; ...
Fix:
class Abc extends React.Component<props, {newAttr: boolean}> {
this.state = { newAttr: false }; ...

Resources