ReactiveVar not re-rendering React component - javascript

I'm quite new to React in Meteor.
TL;DR : In my data container, changing the value of my ReactiveVar do not rerender my view.
I've got this code :
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
// Mail composer - interface for generating enews
class MailComposer extends Component {
constructor(props) {
super(props);
}
handleSubmit( event ) {
event.preventDefault();
const text = this.refs.mailText.value.trim(),
title = this.refs.mailTitle.value.trim(),
rcpts = this.refs.mailRecpts.value.trim();
console.log(text, title, rcpts);
if ( text && title && rcpts ) {
// ...
this.props.hasTestedIt.set( true );
} else {
// ...
}
}
componentDidMount () {
console.log(this);
$( this.refs.textArea ).autosize();
}
getBtnText () {
return ( this.props.hasTestedIt.get() ? "Send it" : "Test it" );
}
render() {
let self = this;
Tracker.autorun(function(){
console.log(self.props.hasTestedIt.get());
});
return (
<div className="panel panel-default panel-primary">
<div className="panel-heading">
<h3 className="panel-title">Mail composer</h3>
</div>
<div className="panel-body">
<form className="form-group" onSubmit={this.handleSubmit.bind(this)}>
<div className="input-group">
<span className="input-group-addon" id="basic-addon1">Title</span>
<input type="text" className="form-control" ref="mailTitle" />
</div>
<br/>
<label htmlFor="js-recipients">Recipients:</label>
<select className="form-control" width="width: initial;" id="js-recipients" ref="mailRecpts">
<option>Admins</option>
<option>All</option>
</select>
<br/>
<label htmlFor="comment">Text:</label>
<textarea className="form-control" rows="5" ref="mailText"></textarea>
<br/>
<button className="btn btn-primary" type="submit">
{this.getBtnText()}
</button>
</form>
</div>
</div>
);
}
}
MailComposer.propTypes = {
hasTestedIt: PropTypes.object.isRequired
};
export default createContainer( () => {
return {
hasTestedIt: new ReactiveVar( false )
};
}, MailComposer);`
But when I set my ReactiveVar prop in my submit handler, the text returned by the getBtnText method in the button do not change. I've tried to put the ternary directly inside the HTML, but I got the same result.
The var is correctly setted to true, as the autorun correctly log me the change.
In another component, from which this one has been copied, I do correctly rerender the component, but using a .fetch() on a find to map the returned array in a renderTasks method which render a list of new components.
What am I missing please ? And how could I fix it ?
Thanks a lot !

The component doesn't update because the passed hasTestedIt prop is not changed itself. It's the value it holds that changed.
Watch the value you are actually interested in instead.
// Declare the reactive source somewhere appropriately.
const hasTestedIt = new ReactiveVar( false );
// Watch its value instead.
export default createContainer( () => {
return {
hasTestedIt: hasTestedIt.get()
};
}, MailComposer);`
Note that it is now the value passed to MailComposer, not a ReactiveVar that can be referenced to update the value anymore. How do we update it in this case?
One simplest approach is to pass the hasTestedIt as well as before, though this wouldn't be my personal recommendation.
// Watch its value instead.
export default createContainer( () => {
return {
// Reactive source that triggers re-rendering.
valueOfHasTestedIt: hasTestedIt.get(),
// Provided for the contained component to reference to set new values.
// Leaving this alone doesn't trigger re-rendering!
hasTested
};
}, MailComposer);
It's not elegant IMO. Another one is to pass a callback function to MailComposer which can be used to update the value.
const updateHasTestedIt = ( value ) => hasTestedIt.set( value );
// Watch its value instead.
export default createContainer( () => {
return {
hasTestedIt: hasTestedIt.get(),
updateHasTestedIt,
};
}, MailComposer);
class MailComposer extends Component {
...
handleSubmit( event ) {
...
this.props.updateHasTestedIt( true );
...
}
...
};
It's better this time.
It is possible to develop more, but it really depends on your preference for your very application. You and only you can make the design decision.

Related

Conditional rendering in React based on current component state

I am struggling with figuring out how to implement conditional rendering in React. Basically, what I want to do is this: if there is a reviewResponse in the reviewResponses array, I no longer want to render the reviewResponseForm. I only want to render that ReviewResponse. In other words, each review can only have one response in this app.
I am not sure what I am doing wrong when trying to implement this logic. I know I need to implement some kind of conditional statement saying if the length of my reviewResponses array is greater than 0, I need to render the form. Otherwise, I need to render that reviwResponse. Every statement I have written has not worked here. Does anybody have a suggestion?
Here is my code so far:
My review cardDetails component renders my ReviewResponseBox component and passed the specific reviewId as props:
import React from "react";
import { useLocation } from "react-router-dom";
import StarRatings from "react-star-ratings";
import ReviewResponseBox from "../ReviewResponse/ReviewResponseBox";
const ReviewCardDetails = () => {
const location = useLocation();
const { review } = location?.state; // ? - optional chaining
console.log("history location details: ", location);
return (
<div key={review.id} className="card-deck">
<div className="card">
<div>
<h4 className="card-title">{review.place}</h4>
<StarRatings
rating={review.rating}
starRatedColor="gold"
starDimension="20px"
/>
<div className="card-body">{review.content}</div>
<div className="card-footer">
{review.author} - {review.published_at}
</div>
</div>
</div>
<br></br>
{/*add in conditional logic to render form if there is not a response and response if there is one*/}
<ReviewResponseBox review_id={review.id}/>
</div>
);
};
export default ReviewCardDetails;
Then eventually I want this component, ReviewResponseBox, to determine whether to render the responseform or the reviewresponse itself, if it exists already.
import React from 'react';
import ReviewResponse from './ReviewResponse';
import ReviewResponseForm from './ReviewResponseForm';
class ReviewResponseBox extends React.Component {
constructor() {
super()
this.state = {
reviewResponses: []
};
}
render () {
const reviewResponses = this.getResponses();
const reviewResponseNodes = <div className="reviewResponse-list">{reviewResponses}</div>;
return(
<div className="reviewResponse-box">
<ReviewResponseForm addResponse={this.addResponse.bind(this)}/>
<h3>Response</h3>
{reviewResponseNodes}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({ reviewResponses: this.state.reviewResponses.concat([reviewResponse]) }); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id} />
);
});
}
}
export default ReviewResponseBox;
Here are the ReviewResponseForm and ReviewResponse components:
import React from "react";
class ReviewResponseForm extends React.Component {
render() {
return (
<form className="response-form" onSubmit={this.handleSubmit.bind(this)}>
<div className="response-form-fields">
<input placeholder="Name" required ref={(input) => this.author = input}></input><br />
<textarea placeholder="Response" rows="4" required ref={(textarea) => this.body = textarea}></textarea>
</div>
<div className="response-form-actions">
<button type="submit">Post Response</button>
</div>
</form>
);
}
handleSubmit(event) {
event.preventDefault(); // prevents page from reloading on submit
let review_id = this.review_id
let author = this.author;
let body = this.body;
this.props.addResponse(review_id, author.value, body.value);
}
}
export default ReviewResponseForm;
import React from 'react';
class ReviewResponse extends React.Component {
render () {
return(
<div className="response">
<p className="response-header">{this.props.author}</p>
<p className="response-body">- {this.props.body}</p>
<div className="response-footer">
</div>
</div>
);
}
}
export default ReviewResponse;
Any advice would be helpful, thank you.
If I understand your question correctly, you want to render ReviewResponseForm if the this.state.reviewResponses state array is empty.
Use the truthy (non-zero)/falsey (zero) array length property to conditionally render either UI element.
render () {
const reviewResponses = this.getResponses();
const reviewResponseNodes = <div className="reviewResponse-list">{reviewResponses}</div>;
return(
<div className="reviewResponse-box">
{reviewResponses.length
? (
<>
<h3>Response</h3>
{reviewResponseNodes}
</>
)
: (
<ReviewResponseForm addResponse={this.addResponse.bind(this)}/>
)}
</div>
);
}

Trying to get state into a const React class - always get TypeError: this.setState is not a function

Sorry, I'm new to React and asked a similar question earlier, but changed the form of my code. I am trying to get a state value in a const react class.
Here in my Article.js
function changeClassName(childData) {
console.log("GETS HERE!");
this.setState({
dynamicClassName: childData
});
}
const Articles = ({ data, isLoading, error }) => {
console.log(data);
console.log(isLoading);
const article_wrapper = data.nodes || [];
if (error) {
return <p>{error.message}</p>;
}
if (isLoading) {
return <p>Loading ...</p>;
}
return (
<div className="article">
{article_wrapper.map( article =>
<div key={article.node.nid} className="article-main-display">
<h1 className="title" dangerouslySetInnerHTML={createMarkup(article.node.title)}/>
<div className="img-div"><img src={article.node.field_image.src} /></div>
<ControlForm parentMethod={changeClassName} />
<div dangerouslySetInnerHTML={createMarkup(article.node.field_user_hsk_level)} />;
<div className="field-name-field-chinese">
<div dangerouslySetInnerHTML={createMarkup(article.node.chinese)} />;
</div>
</div>
)}
</div>
);
}
Here in my ControlForm.js
Part of the render:
<div className="form-item form-type-select form-group">
<label className="control-label">Font Size</label>
<select
value={this.state.value}
onChange={this.handleSizeSelect}
id="font-size"
className="form-control form-select"
>
<option value="0">Small</option>
<option value="1">Normal</option>
<option value="2">Large</option>
<option value="3">XL</option>
</select>
</div>
And the class initiation looks like this:
class ControlForm extends Component {
constructor() {
super();
this.state = { toggleActive: false, sizeSelect: "0", speed: 1.3, volume: .6};
this.onToggle = this.onToggle.bind(this);
this.handleSpeedChange = this.handleSpeedChange.bind(this);
this.handleVolumeChange = this.handleVolumeChange.bind(this);
this.handleSizeSelect = this.handleSizeSelect.bind(this);
}
When I try to use this, I get this error:
TypeError: this.setState is not a function
However, anytime I try to convert const Articles = ... into a formal class X extends Component structure, I get a bunch of other errors.
How can I get state into this component successfully? I have been banging my head against a wall all day and I can't figure out a way to do this.
If you are using const then your file should be of .ts extension. To access the state in .ts, you need to define the type of the state in it. Please refer to this example of how you can do that:
[Reactj, typescript Property 'setState' is missing in type
Otherwise, if you want to work with .jsx then I would suggest you drop the const and declare the state outside the constructor.
First of all, your Articles is not a React Component, that's why you cannot use setState function, then your function changeClassName
isn't declared in the React.Component, that's why you see this.setState is not a function. try this:
class Article extends React.Component {
changeClassName = () => {
//function body
}
//your codes...
}
if you seeing this.setState is not a function, most of the time it just because you do not bind your function. when declaring your function in React class, try just using es6 arrow function like:
yourFunction = (args) => {
this.setState({
//.....
});
}
then you do not need mannualy bind your function in constuctor anymore
Your problem begins with this whole block of code:
function changeClassName(childData) {
console.log("GETS HERE!");
this.setState({
dynamicClassName: childData
});
Is this a React component? If so, is a functional component or a class based component?
What I am getting at is that you need to create a class-based React component like so:
class App extends React.Component {
render() {
return (
<div>
</div>
);
}
}
export default App;
Now, you will notice, once you have a class-based React component, you must include a render() method or else you will get an error. What you return inside of it is project specific.
You also need to create a class-based component so you can initialize state like so:
class App extends React.Component {
state = { images: [] };
render() {
return (
<div>
</div>
);
}
}
export default App;
In this case I have set an initial state inside my App component of images to be an empty array.
Then only within that class can you update your state via this.setState({ images: response.data.results }); for example.

Component is not receiving current, but previous input values

I have two components to represent a list of articles and a filtering form. Every time any form field is changed, I need to send a HTTP request including the selected filters.
I have the following code for the SearchForm:
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const SearchForm = ({ onFormChange }) => (
<form>
<Field component='select' name='status' onChange={onFormChange}>
<option>All</option>
<option value='published'>Published</option>
<option value='draft'>Draft</option>
</Field>
<Field
component='input'
type='text'
placeholder='Containing'
onChange={onFormChange}
/>
</form>
);
export default reduxForm({ form: 'myCustomForm' })(SearchForm);
And the following for the PostsList:
import React, { Component } from 'react';
import SearchForm from './SearchForm';
import { dispatch } from 'redux';
class PostsList extends Component {
constructor(props) {
super();
this.onFormChange = this.onFormChange.bind(this);
}
onFormChange() {
// Here I need to make the HTTP Call.
console.info(this.props.myCustomForm.values);
}
componentWillMount() {
this.props.actions.fetchArticles();
}
render() {
return (
<div>
<SearchForm onFormChange={this.onFormChange} />
<ul>
{ this.props.articles.map((article) => (<li>{article.title}</li>)) }
</ul>
</div>
);
}
}
const mapStateToProps = (state) => ({
myCustomForm: state.form.myCustomForm
});
const mapDispatchToProps = (dispatch) => ({
actions: {
fetchArticles: dispatch({ type: 'FETCH_ARTICLES' })
}
});
export default connect(mapStateToProps, mapDispatchToProps)(PostsList);
Though there is nothing going wrong with the rendering itself, something very awkful is happending with the myCustomForm.values prop when I change the form.
When I do that for the first time, the console.log(this.props.myCustomForm.values) call returns undefined, and the next calls return the previous value.
For example:
I load the page and select the draft option. undefined is printed.
I select published. { status: 'draft' } is printed.
I select draft again... { status: 'published' } is printed.
I inspected the redux store and the componend props. Both change according to the form interaction. But my function is returning the previous, not the new value sent by onChange.
This is clearly a problem with my code, most probably with the way I'm passing the function from parent to child component.
What am I doing wrong?
There is nothing wrong with your function. What I think is happening is that first time you select the option your callback is fired and is console logging current state for myCustomForm.values which haven't been yet changed by redux-form. So when the select changes:
your callback is fired...
...then redux-form is updating the state.
So. when your callback is making console.log it's printing not yet updated store.
do this, and you will see it's true:
onFormChange(e) {
// Here I need to make the HTTP Call.
console.info(e.currentTarget.value);
}
EDIT
My first question would be, do you really need to store this value in redux and use redux-form? It's a simple case, and you get current value in a way I showed you above.
However, if that's not the case, the callback is not required here, you just need to detect in your connected component (PostsList) that values have been changed in a form. You can achieve it with componentWillReceiveProps hook.
class PostsList extends Component {
constructor(props) {
super(props); // you should pass props to parent constructor
this.onFormChange = this.onFormChange.bind(this);
}
componentWillReceiveProps(nextProps) {
if(this.props.myCustomForm.values !== nextProps.myCustomForm.values) {
// do your ajax here....
}
}
componentWillMount(nextProps) {
this.props.actions.fetchArticles();
}
render() {
return (
<div>
<SearchForm />
<ul>
{ this.props.articles.map((article) => (<li>{article.title}</li>)) }
</ul>
</div>
);
}
}

React - How to print the value of an input onChange inside the component

This is my component:
export default function ImageUpload({ onSuccess, children}) {
let value = '';
const onUpload = async (element) => {
element = element.target.files;
let response;
// Call endpoint to upload the file
value = element[0].name;
return onSuccess(response.url);
};
return <div>
<div >
{children}
<input type="file" id="upload" onChange={onUpload.bind(this)} />
</div>
<span>{value}</span>
</div>;
}
I would like to print inside the span the name of the file chosen from the user.
I use redux for the state, but this information is not something that belongs to the state.
This way does not work, could someone explain me why?
UPDATE
Since looks like there is no way to achieve this without state my question is about the best approach:
Use Redux also if I don't need this value outside the component or use the internal state of the component but ending to have both Redux and this state?
I can think about changing the approach, and transform your component to a class type component, something like that:
export default class ImageUpload extends Component {
constructor() {
this.state = {
value: ''
}
this.onUpload = this.onUpload.bind(this)
}
onUpload() {
let value = '';
const onUpload = async (element) => {
element = element.target.files;
let response;
// Call endpoint to upload the file
value = element[0].name;
this.props.onSuccess(response.url);
this.setState({ value: value })
};
}
render () {
return <div>
<div >
{children}
<input type="file" id="upload" onChange={this.onUpload} />
</div>
<span>{this.state.value}</span>
</div>;
}
}

Editing a record: conflict between props and state

When editing a record, the data being displayed on the component belongs to the props. I get an error
Warning: Failed form propType: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly. Check the render method of TerritoryDetail.
I have a feeling I implemented my edit record component the wrong way based on what the docs say involving controlled components.
When editing a record, should you not use props for the field values? If that is the case, I have values of the record in my application state, but how do I sync my application state to my component state without using props?
In addition, the props say what value the select option should be on edit. But component state is used to monitor changes in the select option. How would component state update the props of the record, when the props are being set by application state and not component state?
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTerritory, getTerritoryMetaData, updateTerritory, modal } from '../actions/index';
import { Link } from 'react-router';
import { reduxForm } from 'redux-form';
import TerritoryTabs from './territory-tabs';
class TerritoryDetail extends Component {
constructor(props) {
super(props);
this.openSearchUserQueueModal = this.openSearchUserQueueModal.bind(this);
this.setAssignedToType = this.setAssignedToType.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
componentWillMount() {
// console.log(this.props);
this.props.getTerritory(this.props.params.id);
this.props.getTerritoryMetaData();
}
renderTerritoryPickList(fieldName) {
return this.props.territoryFields.map((territoryField) => {
const shouldRender = territoryField.name === fieldName;
if (shouldRender) {
return territoryField.picklistValues.map((option) => {
return<option value={option.value}>{option.label}</option>;
});
}
});
}
setAssignedToType(event) {
this.setState({ assignedToType : event.target.value });
}
openSearchUserQueueModal(searchType) {
this.props.modal({
type: 'SHOW_MODAL',
modalType: 'USER_QUEUE_SEARCH',
modalProps: {searchType}
})
}
onSubmit() {
console.log('Update button being clicked');
this.props.updateTerritory({
Name: this.refs[ `Name`].value,
tpslead__Type__c: this.refs[ `tpslead__Type__c`].value,
tpslead__Assigned_To_Type__c: this.refs[ `tpslead__Assigned_To_Type__c`].value,
tpslead__Assigned_To__c: this.refs['tpslead__Assigned_To__c'].value,
tpslead__Assigned_To_ID__c: this.refs['tpslead__Assigned_To_ID__c'].value
}, this.props.params.id);
}
onChangeTerritoryName(event) {
this.props.
}
render() {
if(!this.props.territory) {
return <div>Loading...</div>;
}
return(
<TerritoryTabs id={this.props.params.id} listTab="detail">
<div className="slds-form">
<div className="slds-form-element">
<div className="slds-form-element__label">
<label className="slds-align-middle" htmlFor="input1">Lead Territory Name</label>
</div>
<div className="slds-form-element__control">
<input type="text" ref="Name" className="slds-input" value={this.props.territory.Name}/>
</div>
</div>
<div className="slds-form-element">
<label className="slds-form-element__label" htmlFor="input2">Type</label>
<div className="slds-form-element__control">
<div className="slds-select_container">
<select ref="tpslead__Type__c" className="slds-select" value={this.props.territory.tpslead__Type__c}>
<option></option>
{this.renderTerritoryPickList('tpslead__Type__c')}
</select>
</div>
</div>
</div>
<div className="slds-form-element">
<label className="slds-form-element__label" htmlFor="input3">Assigned to Type</label>
<div className="slds-form-element__control">
<div className="slds-select_container">
<select ref="tpslead__Assigned_To_Type__c" onChange={ this.setAssignedToType } className="slds-select" value={this.props.territory.tpslead__Assigned_To_Type__c}>
<option></option>
{this.renderTerritoryPickList('tpslead__Assigned_To_Type__c')}
</select>
</div>
</div>
</div>
<div className="slds-form-element">
<label className="slds-form-element__label">Assigned To</label>
<div className="slds-form-element__control">
<section className="slds-clearfix">
<input ref="tpslead__Assigned_To__c" value={this.props.territory.tpslead__Assigned_To__c} className="slds-input slds-float--left" style={{maxWidth: '95%'}} disabled/>
<input ref="tpslead__Assigned_To_ID__c" value={this.props.territory.tpslead__Assigned_To_ID__c} type="hidden" />
<button onClick={this.openSearchUserQueueModal.bind(this, this.props.territory.tpslead__Assigned_To_Type__c)} className="slds-button slds-button--icon-border slds-float--right" aria-live="assertive" style={{display: 'inline'}}>
<svg className="slds-button__icon" aria-hidden="true">
<use xlinkHref={searchIcon}></use>
</svg>
</button>
</section>
</div>
</div>
<div className="slds-form-element slds-p-top--small">
<Link to="/" className="slds-button slds-button--neutral">
Cancel
</Link>
<button type="button" onClick={this.onSubmit} className="slds-button slds-button--brand">Update</button>
</div>
</div>
</TerritoryTabs>
);
}
}
function mapStateToProps(state) {
console.log(state);
return { territory: state.territories.single,
territoryFields: state.territories.fields
};
}
export default connect(mapStateToProps, { getTerritoryMetaData, getTerritory, updateTerritory, modal })(TerritoryDetail);
A controlled component means that you've provided both a value and an onChange handler. You have to have both, or React will complain. This is also true if you pass a null or undefined value, so you'll want to default to an empty string in those cases. Example:
export function TerritorySelect({ territory = '', options, onChange }) {
const choices = options.map((o, i) => (
<option key={i} value={o.value}>{o.label}</option>
));
const update = e => onChange(e.target.value);
return (
<select value={territory} onChange={update}>
{choices}
</select>
);
}
export default connect(
state => ({ territory: state.territory.get('territory') }),
{ onChange: actions.updateTerritory }
)(TerritorySelect)
Golden rule is that if the data will be changed by user's input then use the state, otherwise use props. To avoid confusion between application state and component state, I will call application state Redux store.
You can pass whatever is in your Redux store, with the function you already have mapStateToProps and on the constructor of your component simply setting them to the state. But the props are always needed, just not in the way you are using it. To do that on the constructor method, just add the following:
this.state = {
// whatever you want to define goes here
// if you want to pass props you don't need to call this, just props.foo
}
That means that you can handle the state of your select and the state of your inputs without an issue.
Your props will be updated whenever they receive new props as long as they are not received by user interaction. That means that you can use your Redux store to dispatch actions, what will fire an update to your props.

Categories