Why do multiple instances of a component share the same state? - javascript

I am building a todo list, and one of the functionalities is to display a list of completed tasks when the "completed" text is clicked and hide the list when it is clicked again. However, I can't get the first step - saving the state of the list, whether shown or hidden - to work.
There are several of these "CompletedRow" components (for different categories of tasks), and while clicking the text does toggle the state successfully, it seems that all these "CompletedRow" components share the same state - they are not independent. Why is this the case when I have already assigned keys to these components, and how can I solve this issue?
var CompletedRow = React.createClass({
getInitialState: function () {
return {
show: false
};
},
handleChange: function() {
this.setState.show = !this.setState.show;
console.log(this.state.show);
},
render: function() {
return (<tr>
<td
className="completed" colSpan="3" onClick={this.handleChange}
> {this.props.count} tasks completed
</td>
</tr>);
}
});
var TaskTable = React.createClass({
// other code omitted for simplicity
render: function() {
var rows = [];
var completedTasks = 0;
this.state.taskList.forEach(function(task, index) {
// do something
if (completedTasks > 0) {
rows.push(<CompletedRow
count={completedTasks}
key={getKey()}
taskList={this.state.taskList}
/>);
completedTasks = 0;
}
}.bind(this));
return (
<div>
<table>
<tbody>{rows}</tbody>
</table>
</div>
);
}
});

This may be a typo, but you have a line in handleChange that is toggling a property attached to the setState function. This is shared by all the components
this.setState.show = !this.setState.show;
That line should be
this.setState({show: !this.state.show});
Also, the console.log on the following line will reflect the old state because setState is asynchronous to allow batching of state changes.

Related

Calling a function in another component based on the condition, whether the tab is active or not

Example 1:
click on tab1. Tab 1 is active tab. Click again on tab1(tab1 is active tab) I don't want to call function start() in component 'Details'. Click on tab 2 or tab 3 I want to call function start() in another component 'Details'.
Example 2:
click on tab2. Tab 2 is active tab. Click again on tab2(tab 2 is active tab) I don't want to call function start() in component 'Details'. Click on tab 1 or tab 3 I want to call function start() in another components 'Details'.
Tries to create a condition in the 'Details' component. If the tab is not active, call the start () function. Move the function isActive () and the variable selectedTabId from the component 'Tabs' to 'Details' and call the start function there if the tab is not active. Is my reasoning correct? Is this a good approach?
Code here: https://codepen.io/kaka-milan/pen/oKXJma?editors=1111
Drawing: https://imgur.com/VZ3N5lM
Tabs
var Tabs = React.createClass({
getInitialState: function() {
return { selectedTabId: 1 }
},
isActive: function (id) {
return this.state.selectedTabId === id;
},
setActiveTab: function (selectedTabId) {
this.setState({ selectedTabId });
},
render: function() {
var total = this.props.data.points.total,
tabs = total.map(function (el, i) {
return <Tab
key={ i }
content={ el.name }
id={el.id}
isActive={ this.isActive(el.id) }
onActiveTab={ this.setActiveTab.bind(this, el.id) }
/>
}, this);
return
<div>
<ul className="navigation">
{ tabs }
</ul>
<Details
isActive={ this.isActive}
selectedTabId={this.state.selectedTabId}
/>
</div>
}
});
Details
var Details = React.createClass({
componentDidMount() {
if(!this.props.isActive(this.props.selectedTabId)){
this.start();
}
},
start: function() {
return console.log('aaa')
},
render: function() {
return
<p></p>
}
});
Currently the problem is that Details is rendered when Tabs is rendered so start is only called on first render. Also Details has no way of knowing when state is changed.
One solution is to move start into Tabs and call it in setActiveTab if needed, like below. This allows us to remove the Details component altogether:
var Tabs = React.createClass({
...
setActiveTab: function (selectedTabId) {
if(selectedTabId !==
this.setState({ selectedTabId });
},
start: function () {
console.log('aaa')
},
...
})
Updated codepen: https://codepen.io/sentaipadas/pen/JgYPmq
I haven't worked with react classes for a while, but if you have access to React 16.8 and want to separate state handling logic from rendering (it looks like that is the aim of Details component) you can use React hooks (with classes you could possibly use refs).
Example codepen: https://codepen.io/sentaipadas/pen/mNeboE

Initialise an element with a function dynamically - jQuery

I'm currently using react router to build my site, but I have .dropdowns on several pages which I initialise using $('.dropdown').dropdown();
How can I keep initialising my dropdowns on every page I visit using react router? The onhashchange doesn't seem to register the URL changing.
This is what my dropdown function looks like.
jQuery
$(function() {
$('.dropdown').dropdown();
$(window).on('hashchange', function(e){
$('.dropdown').dropdown();
});
});
$.fn.extend({
dropdown: function(o){
var o = $.extend({ maxHeight: 600, buffer: 100, delay: 500 }, o);
return this.each(function(){
var dropdown = $(this),
toggle = dropdown.find('.toggle'),
menu = dropdown.find('.menu'),
a = dropdown.find('a'),
liheight = dropdown.height();
if(!dropdown.length){ return; }
toggle.click(function(e){
if(!menu.is(':visible')) {
$('.menu').parent().removeClass('open');
}
menu.parent().addClass('open');
});
$(document).bind('click', function(e){
if (! $(e.target).parents().hasClass('dropdown'))
menu.parent().removeClass('open');
});
});
}
});
I have also tried this, but still no luck:
$('body').on('focus', '.dropdown' function(e){
$('.dropdown').dropdown();
});
Edit: The solution
Used the componentDidMount() function in react router to initialise the dropdown every time the dropdown was rendered. This is what I added:
export const elDropdown = React.createClass({
componentDidMount: function() {
$(document).ready(function() {
$('.dropdown').dropdown();
})
},
render() {
return(
<div>...
you need to initialise the dropdowns after they become available on the page. The right place to do that is in React's lifecycle methods. In that case, componentDidMount - https://facebook.github.io/react/docs/component-specs.html#mounting-componentdidmount
You can create a component that wraps the dropdown and initialises it. Here is a simple example of how you could achieve that:
class DropdownWrapper extends React.Component {
constructor(){
super()
this.id = 'someUniqueID'
}
componentDidMount(){
$('#' + this.id).dropdown();
}
render(){
return <select id={this.id} otherProps={this.otherProps} ></select>
}
}
Notice that you will need to use unique identifiers. You can pass it to the component manually or create it randomly.

Dynamic Component onClick not working

I have a JS Fiddle with the below code.
I have a dynamic component setup, which does work. If I do var Component = Switch or set Component to some other React component, it works. But I want to be able to have it switch when I click on it.
So I set up an onClick event. Yet its not firing. I get no log statements or any change. Any ideas why?
var Other = React.createClass({
render: function () {
return <h2>Test this craziness</h2>;
}
});
var Switch = React.createClass({
render: function () {
return <h2>A different one to switch with</h2>;
}
});
var Hello = React.createClass({
getInitialState: function() {
return {on: true};
},
handleClick: function() {
console.log('handleclick');
this.setState({on: ! this.state.on});
},
render: function() {
console.log(this.state);
var Component = this.state.on ? this.props.component.name : Switch;
return <Component onClick={this.handleClick} />;
}
});
ReactDOM.render(
<Hello component={{name: Other}} />,
document.getElementById('container')
);
Easy! When onClick is put directly on a Component, like you have done, it is NOT set as the onClick function. It is instead placed in this.props.onClick. You still need to add the onClick to your actual DOM Elements. See the attached JSFiddle!
You need to add the onClick attribute to an actual HTML element, i.e.
var Other = React.createClass({
render: function () {
return <h2 onClick={this.props.onClick}>Test this craziness</h2>;
}
});

Vue DevTools updating correctly but not browser window

I'm having a strange issue where the value found in Vue DevTools is correct. It's declared in my data as expected. The first time I click on "Edit" an item, the correct value shows up in my browser window as well.
However, if I click on "Edit" an item that has a different quantity, the same value shows up again even if it is incorrect (it should be prepopulating from the database).
Then, if I click back on the first "Edit" item again that value will get updated with the previous value!
The craziest part is that while my browser window is not showing the correct value, the correct result is showing up in Vue DevTools at all times! The circled item in the image below is the UUID for the "Quantity" of 100, which is the correct value. Yet 700 is showing up (the previous Edit item's value). Anybody ever had this happen before and know what gives?
Here's some snippets of relevant code (it's from a Vue component using vue-resource, and this is taking place in a bootstrap modal in a Laravel project):
Vue JS
data() {
return {
selected_options: {},
attributes: [],
}
},
methods: {
editLineItem: function (line_item) {
this.getProductOptionsWithAttributes(line_item.product_id);
this.getPrepopulatedOptionsForLineItem(line_item.id);
},
getProductOptionsWithAttributes: function (product_id) {
var local_this = this;
var url = '/api/v1/products/' + product_id + '/options';
this.$http.get(url).then(function (response) {
local_this.attributes.$set(0, response.data);
}, function (response) {
// error handling
});
},
getPrepopulatedOptionsForLineItem: function (id) {
var local_this = this;
var url = '/api/v1/line_items/' + id + '/options';
this.$http.get(url).then(function (response) {
Object.keys(response.data).forEach(function (key) {
Vue.set(local_this.selected_options, key, response.data[key]);
});
}, function (response) {
//#TODO Implement error handling.
});
},
}
HTML
<div v-for="(key, attribute) in attributes[0]" class="col-md-12 selectbox_spacing">
<label for="option_{{$index}}">{{key}}</label><br/>
<select class="chosen-select form-control" v-model="selected_options[key]" v-chosen="selected_options[key]" id="option_{{$index}}">
<option v-for="option in attribute" value="{{option.id}}">{{option.name}}</option>
</select>
</div>
<button v-on:click="editLineItem(line_item)">
Main.js vue-directive:
Vue.directive('chosen', {
twoWay: true, // note the two-way binding
bind: function () {
$(this.el)
.change(function(ev) {
// two-way set
//this.set(this.el.value);
var i, len, option, ref;
var values = [];
ref = this.el.selectedOptions;
if(this.el.multiple){
for (i = 0, len = ref.length; i < len; i++) {
option = ref[i];
values.push(option.value)
}
this.set(values);
} else {
this.set(ref[0].value);
}
}.bind(this));
},
update: function(nv, ov) {
// note that we have to notify chosen about update
$(this.el).trigger("chosen:updated");
}
});
var vm = new Vue({
el : '#wrapper',
components: {
LineItemComponent
}
});
Script in edit.blade.php file:
<script>
$(document).ready(function() {
$('#lineItemModal').on('shown.bs.modal', function () {
$('.chosen-select', this).chosen('destroy').chosen();
});
}
</script>
by default, custom directives have a priority of 1000. v-model has a priority of 800 meaning it's evaluated after v-chosen when the template is compiled.
My Assumption is now: this is also affecting the update.
What I mean by that: I think $(this.el).trigger("chosen:updated"); in the v-chosen update method is called before v-model did refresh the selected attribute on the list of <option> elements - and that's where chosen checks for the new selected value.
Long story short: try this:
Vue.directive('chosen', {
priority: 700, // Priority lower than v-model
twoWay: true, // note the two-way binding
bind: function () {
....

ReactJS remove component

I'm trying to learn the basics of facebook's react.js library and I've been lately thinking about a few stuff I can do with it just to get used to the way it works . I'm trying to make a div that contains 2 buttons one is OPEN and the other is CLOSE, when you click the OPEN the react will render a div containing anything (Like a msg saying "You clicked"), this is fine up to now but I cannot figure out how to make it disappear once clicked on the CLOSE button, does anyone know how to do that ? Thanks ^^
There are at least four ways, that depends on the real problem you need to solve:
1) Add #your-div-id.hidden { display:none } styles and add/remove hidden class on click (maybe not React way)
2) Change view state (i.e. opened flag). That's a React way and maybe the simplest choice
onOpen() {
this.setState({ opened: true });
}
onClose() {
this.setState({ opened: false });
}
render() {
var div = (this.state.opened) ? (<div>Your Div Content</div>) : undefined;
return (
//some your view content besides div
{div}
);
}
3) If you use Flux. Move state to Store and subscribe to changes. That maybe useful if you gonna show your div at many parts of your app (i.e. implement error popups which may be shown at any part of an application).
So, first of all let's keep warnings at the store:
var CHANGE_EVENT = 'change';
const warnings = [];
var WarningsStore = assign({}, EventEmitter.prototype, {
getWarnings: () => return warnings,
emitChange: () => this.emit(CHANGE_EVENT),
addChangeListener: callback => this.on(CHANGE_EVENT, callback),
removeChangeListener: callback => this.removeListener(CHANGE_EVENT, callback)
});
WarningsStore.dispatchToken = AppDispatcher.register(action => {
switch(action.type) {
case ActionTypes.ADD_WARNING:
warnings.push(action.warning);
WarningsStore.emitChange();
break;
case ActionTypes.DISMISS_WARNING:
_.remove(warnings, {type: action.warningType}); //that's lodash or underscore method
WarningsStore.emitChange();
break;
}
});
After we have a warnings store, you may subscribe to it from YourView and show popup on each AppDispatcher.dispatch({type: ADD_WARNING, warningType: 'Error'});
var YourView = React.createClass({
componentDidMount: function() {
WarningsStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
WarningsStore.removeChangeListener(this._onChange);
},
_onChange() {
this.setState({ warnings: WarningsStore.getWarnings() });
},
render() {
//if we have warnigns - show popup
const warnings = this.state.warnings,
onCloseCallback = () => AppDispatcher.dispatch({type: DISSMISS_WARNING, warningType: warnings[0].warningType});
popup = (warnings.length) ? <YourPopupComponent warning={warnings[0]} onClose={onCloseCallback}> : undefined;
return (
//here the main content of your view
{popup}
);
}
})
4) If you simplified your example, but actually instead of div you need to show/hide another page - you should use react-router
Here's how I would do this:
var foo = React.CreateClass({
getInitialState: function() {
return {
showDiv: false, //Or true if you need it displayed immediately on open
}
},
showIt: function() {
this.setState({showDiv: true});
},
hideIt: function() {
this.setState({showDiv: false});
},
render: function() {
return (<div style={{display: this.state.showDiv ? 'block' : 'none'}}>Your content here</div>);
}
});
What this will do is on state change, the style block of the div will be re-evaluated. If the showDiv state variable is true, it'll display as a block element. Otherwise it'll display none. You could, in theory, do this with CSS as well.
Here's a jsFiddle showing this being done with both CSS classes AND the style attribute on the div element.

Categories

Resources