I am learning Flux in ReactJS. I have written a simple code in ReactJS using Flux pattern. In this code some records are being displayed and there is an option to add a new record. The problem is that when I fire an event i.e. when I click the add button, the callback function is not being called from this.on(CHANGE_EVENT, callback); and as a result of that, the new record is not being displayed on screen. Therefore please tell, why this callback function is not being called? And how to resolve this issue?
Store.js
import React from 'react';
import Dispatcher from './Dispatcher.js';
import { EventEmitter } from "events";
class Store extends EventEmitter {
constructor() {
super();
this.records = [
{
id: 1,
name: 'First Record'
},
{
id: 2,
name: 'Second Record'
},
{
id: 3,
name: 'Third Record'
},
{
id: 4,
name: 'Fourth Record'
},
{
id: 5,
name: 'Fifth Record'
}
]
};
createRecord(name, id) {
this.records.push({
id: id,
name: name
});
this.emit("change");
}
addChangeListener(CHANGE_EVENT, callback) {
this.on(CHANGE_EVENT, callback);
}
handleActions(action) {
switch(action.type) {
case "ADD_RECORD": {
this.createRecord(action.name, action.id);
break;
}
}
}
getRecords() {
return this.records;
}
};
const recordsStore = new Store();
Dispatcher.register(recordsStore.handleActions.bind(recordsStore));
export default Store;
View.jsx
import React from 'react';
import Store from './Store.js';
import {addRecord} from "./Action.js";
class View extends React.Component {
constructor() {
super();
this.Store = new Store();
this.state = {records: this.Store.getRecords()};
}
render() {
return (
<div className="container" style={{marginTop:'25px'}}>
<ul className="list-group">
<li style={{backgroundColor:'#696969', color:'#f5f5f5', textAlign:'center', padding:'5px', fontSize:'16px', borderRadius:'5px 5px 0px 0px'}}><b>Records</b></li>
{this.state.records.map((eachRecord,index) =>
<ListItem key={index} singleRecord={eachRecord} />
)}
</ul>
<input type="text" ref="input"/>
<button onClick={()=>addRecord(this.refs.input.value)}>Add</button>
</div>
);
}
componentWillMount() {
this.Store.addChangeListener("change", this.updateStore);
}
updateStore() {
this.setState({
records: this.Store.getRecords()
});
}
}
class ListItem extends React.Component {
render() {
return (
<li className="list-group-item" style={{cursor:'pointer'}}>
<b>{this.props.singleRecord.name}</b>
<button style={{float:'right'}}>Delete</button>
</li>
);
}
}
export default View;
This is how I would set up a flux store -- the key points are:
register the dispatch callback in the constructor
It makes it easier to have a single CHANGE_EVENT constant and hide it inside the store.
Typically you'll want your stores to only have one instance. So when you export it you would export default new Store(). That way all components will be able to use the same store.
Store
const CHANGE_EVENT = 'change';
class Store extends EventEmitter {
constructor() {
super();
Dispatcher.register(this.handleActions.bind(this));
}
createRecord(name, id) {
// code..
this.emitChange();
}
emitChange() {
this.emit(CHANGE_EVENT);
}
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
handleActions(action) {
switch (action.type) {
case 'ADD_RECORD': {
this.createRecord(action.name, action.id);
break;
}
}
}
getRecords() {
return this.records;
}
}
export default new Store();
--
In the view, you should bind your listeners in componentDidMount and remember to remove the listeners in componentWillUnmount.
View
import Store from './Store';
class View extends React.Component {
constructor() {
super();
this.state = {records: Store.getRecords()};
}
render() {
// code..
}
componentDidMount() {
Store.addChangeListener(this.updateStore);
}
componentWillUnmount() {
Store.removeChangeListener(this.updateStore);
}
// use this syntax to keep context or use .bind in the constructor
updateStore = () => {
this.setState({
records: Store.getRecords()
});
}
}
Related
So when I run this and check the checkbox, I can see the values changing in the state, but why is the checkbox control not changing its status from check/uncheck? I know the render() method is being hit as well. Why, oh why, Gods of code? Lost in hours of figuring out what's wrong and I'm lost!
bob-Todos.js FILE
class Todo extends React.Component {
constructor(param) {
super();
this.state = {
id: param.data.id,
text: param.data.text,
completed: param.data.completed,
onMyChange: param.OnChange,
};
}
render() {
console.log("In TODO Render");
return (
<div>
<p>
<input
type="checkbox"
onChange={() => {
this.state.onMyChange(this.state.id);
}}
checked={this.state.completed}
/>
{this.state.text}
</p>
</div>
);
}
}
export default Todo;
Bob-App.js FILE
import React, { Component } from "react";
import Todo from "./bob-Todo";
import todoData from "../data/bob-todosData";
class App extends Component {
constructor() {
super();
this.state = { data: todoData };
this.OnChange = this.OnChange.bind(this);
}
OnChange(myId) {
this.setState((prev) => {
let updatedTodos = prev.data.map((todo) => {
if (todo.id === myId) {
todo.completed = !todo.completed;
}
return todo;
});
return { data: updatedTodos };
});
console.log(this.state.data);
}
render() {
return this.state.data.map((item) => {
return <Todo key={item.id} data={item} OnChange={this.OnChange} />;
});
}
}
export default App;
bob-todosData.js FILE
const todosData = [
{
id: 1,
text: "take out the trash",
completed: true
},
{
id: 2,
text: "rest for a while and relax",
completed: false
},
{
id: 3,
text: "watch an online movie",
completed: true
}
]
export default todosData
index.js FILE
import React from 'react';
import ReactDOM from 'react-dom';
import AppBob from "./bobComponents/Bob-App";
ReactDOM.render(
<AppBob />, document.getElementById('root')
);
You don't need to assign your props to state in your Todo component
Just remove them and invoke the function also use those variables directly:
Then your component will be:
class Todo extends React.Component {
render() {
const {
data: {
id,
text,
completed,
},
OnChange, // <-- Should rename this to "onChange"
} = this.props;
console.log('In TODO Render');
return (
<div>
<p>
<input
type="checkbox"
onChange={() => {
OnChange(id);
}}
checked={completed}
/>
{text}
</p>
</div>
);
}
}
export default Todo;
Also, rename your OnChange function to onChange to enable js convention
I have the following:
import React from 'react';
import ReactDOM from 'react-dom'
import {render} from 'react-dom';
import Forms from './forms/forms.jsx';
class Option1 extends React.Component {
render () {
return (
<p>Icon 1</p>
)
}
}
class TShirt extends React.Component {
render () {
console.log(this.props.currentState);
return <div className="thsirt">
<h1>{this.props.name}</h1>
<p>{this.props.iconID}</p>
{this.props.optionA ? <Option1 /> : ''}
</div>;
}
}
class Link extends React.Component {
render () {
return (
<li
data-id={this.props.el}
onClick={this.props.onClick}
className={this.props.activeClass}>{this.props.el}
</li>
);
}
}
class Nav extends React.Component {
getComponentID (id) {
switch(id) {
case 'name':
return 1;
break;
case 'color':
return 2;
break;
case 'design':
return 3;
break;
case 'share':
return 4;
break;
}
}
handleClick (event) {
// setting active class
var id = event.target.getAttribute("data-id");
this.props.action(id);
// switching coomponent based on active class
var component = this.getComponentID(id);
this.props.switchComponent(component);
}
render () {
var links = ['name', 'color', 'design', 'share'],
newLinks = [],
that = this;
links.forEach(function(el){
newLinks.push(<Link
onClick={that.handleClick.bind(that)}
activeClass={that.props.active == el ? 'active': ''}
key={el}
el={el}
/>
);
});
return (
<ol>
{newLinks}
</ol>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
color: '',
active: '',
formId: 1,
optionA: {
on: false,
icon_id: '',
option_id: '',
name: ''
}
};
this.setName = this.setName.bind(this);
this.setColor = this.setColor.bind(this);
this.setAtciveNavEl = this.setAtciveNavEl.bind(this);
this.setFormId = this.setFormId.bind(this);
this.setOptionA = this.setOptionA.bind(this);
this.setOptionAVisibility = this.setOptionAVisibility.bind(this);
}
setName (tshirt) {
this.setState({ name:tshirt })
}
setColor (color) {
this.setState({ color:color })
}
setAtciveNavEl (el) {
this.setState({ active:el })
}
setFormId (id) {
this.setState({ formId:id })
}
setOptionA (iconID, iconName) {
this.setState({
optionA:
{
icon_id: iconID,
name: iconName
}
})
}
setOptionAVisibility (onOff, optionID) {
this.setState({
optionA:
{
option_id: optionID,
on: onOff
}
})
}
render () {
return (
<section className={this.state.color}>
<Nav
active={this.state.active}
action={this.setAtciveNavEl}
switchComponent={this.setFormId}
/>
<TShirt
name={this.state.name}
icons={this.state.options}
optionA={this.state.optionA.on}
currentState={this.state}
/>
<Forms
name={this.state.name}
action={this.setName}
colorVal={this.setColor}
activeNav={this.setAtciveNavEl}
switchComponent={this.setFormId}
formID={this.state.formId}
setOptionA={this.setOptionA}
setOptionAVisibility={this.setOptionAVisibility}
/>
</section>
);
}
}
render(<App/>, document.getElementById('app'));
I need to populate this object at different times like this:
setOptionA (iconID, iconName) {
this.setState({
optionA:
{
icon_id: iconID,
name: iconName
}
})
}
setOptionAVisibility (onOff, optionID) {
this.setState({
optionA:
{
option_id: optionID,
on: onOff
}
})
}
The problem I have is taht when I console.log my state at:
class TShirt extends React.Component {
render () {
console.log(this.props.currentState);
return <div className="thsirt">
<h1>{this.props.name}</h1>
<p>{this.props.iconID}</p>
{this.props.optionA ? <Option1 /> : ''}
</div>;
}
}
after all my click events it seems like I loose the "on" and "option_id" from the optionA object.
Does calling setState on the same object override the previous setState?
If you are writing ES2015, you can use the spread operator to copy the whole object and just modify one of it's properties:
setOptionAVisibility (onOff, optionID) {
this.setState({
optionA:
{
...this.state.optionA,
option_id: optionID,
on: onOff
}
})
}
Can be very useful when modifying single properties of complex objects on the state tree.
I have a container component that connects to the state which I made with immutable.js. When I update the state, my redux inspector tells me that the state is updated, but my component doesn't get the new updates and doesn't re-render.
My container component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { setCategoryActive } from '../actions'
import Category from '../components/Category'
class MenuList extends Component {
constructor(props) {
super(props)
this.categories = this.props.categories
this.subreddits = this.props.subreddits
this.allCategories = this.categories.get("allCategories")
this.byName = this.categories.get("byName")
}
printSubreddits(category) {
const subreddits = this.byName.get(category)
const subredditsByName = subreddits.get("subredditsByName")
const list = subredditsByName.map((subreddit, i) => {
return <p className="swag" key={i}>{subreddit}</p>
})
return list
}
isCategoryActive(category) {
const cat = this.byName.get(category)
return cat.get("active")
}
printCategory(category, i) {
console.log(this.isCategoryActive(category))
return (
<div className="category-container" key={i}>
<Category name={category}
active={this.isCategoryActive(category)}
setActive={this.props.setCategoryActive.bind(this, category)} />
{this.isCategoryActive(category) ? this.printSubreddits(category) : null}
</div>
)
}
render() {
return (
<div>
{this.allCategories.map((category, i) => {
const x = this.printCategory(category, i)
return x
}, this)}
</div>
)
}
}
const mapStateToProps = (state) => ({
categories: state.subredditSelector.get('categories'),
subreddits: state.subredditSelector.get('subreddits')
})
export default connect(mapStateToProps, {
setCategoryActive
})(MenuList);
My Category component
class Category extends Component {
printToggle(active) {
if (active) {
return <span> [-]</span>
} else {
return <span> [+]</span>
}
}
componentWillReceiveProps(nextProps) {
this.printToggle(nextProps.active)
}
render() {
const { setActive, active, name } = this.props
return (
<div className="category-container">
<a onClick={setActive}
href="#"
className="category-title">
{name}
{this.printToggle(active)}
</a>
</div>
)
}
}
export default Category
And my reducer
import { fromJS } from 'immutable'
import {
SET_SUBREDDIT_ACTIVE,
SET_CATEGORY_ACTIVE
} from '../actions'
import List from '../data/nsfw_subreddits.js'
const initState = fromJS(List)
const subredditSelector = (state = initState, action) => {
switch (action.type) {
case SET_SUBREDDIT_ACTIVE:
return state
case SET_CATEGORY_ACTIVE:
return state.updateIn(['categories', 'byName', action.payload.name],
x => x.set('active', !x.get('active')))
default:
return state
}
}
export default subredditSelector
A piece of my state that I have coded as a JSON object
const list = {
categories: {
byName: {
"example": {
name: "example",
id: 1,
active: true,
subredditsByName: ["example", "example"]
},
"example": {
name: "example",
id: 2,
active: true,
subredditsByName: ["example"]
},
"example": {
name: "example",
id: 3,
active: true,
subredditsByName: ["example", "example", "example"]
}
},
allCategories: ["example", "example", "example"]
},
subreddits: {
My guess is that my reducer is mutating the state? Though I am not sure how, since I am using the immutable js functions?
So I fixed this issue by changing the constructor from a constructor to a regular function and called it at the top of my render method!
You should still keep your constructor, but only have in it what needs to be done when the object is first created:
constructor(props) {
super(props)
}
And yes, having
cleanPropData(){
this.categories = this.props.categories
this.subreddits = this.props.subreddits
this.allCategories = this.categories.get("allCategories")
this.byName = this.categories.get("byName")
}
is fine. I haven't heard of someone invoking it at the top of a render function. Nice to know that works.
From 2 weeks ago I'm facing a problem in my React/Flux app. It's done in ES6 and using webpack and babel.
It actually doesn't go inside the _onChange method ones the store emit the change event. So the component itself doesn't render again with the modified state.
Here you can take a look to my component:
import React from 'react';
import Item from 'components/item/item';
import Actions from './item-list.actions';
import Store from './item-list.store';
const StoreInstance = new Store();
class ItemList extends React.Component {
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
this.state = this.getItemListState();
}
componentWillMount() {
StoreInstance.addChangeListener(this._onChange);
Actions.requestFlats(Actions.setFlats);
}
componentWillUnmount() {
StoreInstance.removeChangeListener(this._onChange);
}
_onChange() {
this.setState(this.getItemListState);
}
getItemListState() {
return {
flats: StoreInstance.getFlats()
}
}
render() {
return(
<ul className="item__list">{
this.state.flats.map((flat, index) => {
<li className="col-xs-12 col-sm-12 col-md-6 col-lg-6">
<Item key={index} flat={flat}></Item>
</li>
})
}</ul>
);
}
}
export default ItemList;
My actions:
import AppDispatcher from 'services/dispacher/dispacher';
import Constants from './item-list.constants';
let ItemListActions = {
getFlats: () => {
AppDispatcher.handleAction({
type: Constants.GET_FLATS,
data: {}
});
},
setFlats: (flats) => {
AppDispatcher.handleAction({
type: Constants.SET_FLATS,
data: {
flats
}
});
},
requestFlats: (callback) => {
AppDispatcher.handleAction({
type: Constants.REQUEST_FLATS,
data: {
callback
}
});
}
};
export default ItemListActions;
And store:
import AppDispatcher from 'services/dispacher/dispacher';
import AppStore from 'services/store/store';
import Api from './item-list.api';
import Constants from './item-list.constants';
class ItemListStore extends AppStore {
constructor() {
super();
this.flats = [];
}
requestFlats(callback) {
Api.getFlats(callback);
}
getFlats() {
return this.flats;
}
setFlats(flats) {
this.flats = flats;
}
}
const ItemListStoreInstance = new ItemListStore();
AppDispatcher.register((payload) => {
let action = payload.action;
switch (action.type) {
case Constants.GET_FLATS:
ItemListStoreInstance.getFlats(action.data);
break;
case Constants.SET_FLATS:
ItemListStoreInstance.setFlats(action.data.flats);
break;
case Constants.REQUEST_FLATS:
ItemListStoreInstance.requestFlats(action.data.callback);
break;
default:
return true;
}
ItemListStoreInstance.emitChange();
});
export default ItemListStore;
which extends of AppStore
import EventEmitter from 'events';
const CHANGE_EVENT = 'change';
class Store extends EventEmitter {
constructor() {
super();
}
emitChange() {
this.emit(CHANGE_EVENT);
}
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
}
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
}
Store.dispatchToken = null;
export default Store;
I have check this code many times and looking at examples over the whole Internet and I got no success.
I's supposed that when I do:
StoreInstance.addChangeListener(this._onChange);
the store will listen for my change event, but looks like it doesn't.
When I got the new data from the API, I execute setFlats and _onChange is not executed, so no changes on the UI are shown.
Do you see any issue in this code? Anything that could help me to solve it?
Thanks in advance.
I don't see any usage of you ItemListStore anywhere. Your component is using the "Store" class, which only extends EventEmitter. The connection to the ItemListStore is nowhere to be found.
This line (In your ItemListStore):
ItemListStoreInstance.emitChange();
will not trigger the emitChange() method in your Store.
The problem was actually in the store which was returning the ItemListStore instead of an instance of ItemListStore and then in the component I was having another instance, that's why it wasn't communication with each other.
Here is the fixed code for the ItemListStore:
import AppDispatcher from 'services/dispacher/dispacher';
import AppStore from 'services/store/store';
import Api from './item-list.api';
import Constants from './item-list.constants';
class ItemListStore extends AppStore {
constructor() {
super();
this.flats = [];
}
requestFlats(callback) {
Api.getFlats(callback);
}
getFlats() {
return this.flats;
}
setFlats(flats) {
this.flats = flats;
}
}
const ItemListStoreInstance = new ItemListStore();
AppDispatcher.register((payload) => {
let action = payload.action;
switch (action.type) {
case Constants.GET_FLATS:
ItemListStoreInstance.getFlats(action.data);
break;
case Constants.SET_FLATS:
ItemListStoreInstance.setFlats(action.data.flats);
break;
case Constants.REQUEST_FLATS:
ItemListStoreInstance.requestFlats(action.data.callback);
break;
default:
return true;
}
ItemListStoreInstance.emitChange();
});
export default ItemListStoreInstance;
I have a Flux problem that's been killing me. I'm calling an action on page load, but for some reason it doesn't update the state in the component. In this example, I have this.props.count set to 5 (the default in TestStore). I then call an action to increase it in componentDidmount to 6, but it doesn't update the component's state. It stays at 5. Then if I click the link to manually update it, it goes from 5 to 7.
I think it has something to do with the Flux changeListener being added to the top-level component after the action is dispatched?
If I put the changeListener in componentWillMount instead of componentDidMount in the top-level component, then everything works. But that doesn't seem like the proper way? I feel like I'm missing something.
Here's a console.log and the components...
< Tester />
import React from 'react';
import TestActions from '../actions/TestActions';
export default class Tester extends React.Component {
componentDidMount() {
// this.props.count defaults to 5
// This brings it to 6
TestActions.increaseCount();
}
render() {
return (
<div>
// Count should display 6, but shows 5
Count: {this.props.count}
<br />
<a href="#" onClick={this._handleClick}>Increase</a>
</div>
);
}
_handleClick(e) {
e.preventDefault();
TestActions.increaseCount();
}
}
< Application />
import React from 'react';
import {RouteHandler} from 'react-router';
import TestStore from '../stores/TestStore';
export default class Application extends React.Component {
constructor() {
super();
this._onChange = this._onChange.bind(this);
this.state = this.getStateFromStores();
}
getStateFromStores() {
return {
count: TestStore.getCount()
};
}
componentDidMount() {
TestStore.addChangeListener(this._onChange);
}
_onChange() {
this.setState(this.getStateFromStores());
}
componentWillUnmount() {
TestStore.removeChangeListener(this._onChange);
}
render() {
return (
<RouteHandler {...this.state} {...this.props}/>
);
}
}
TestStore
var AppDispatcher = require('../dispatchers/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TestConstants = require('../constants/TestConstants');
var assign = require('object-assign');
var CHANGE_EVENT = 'change';
var _count = 5;
function increaseCount() {
_count = _count + 1;
}
var TestStore = assign({}, EventEmitter.prototype, {
getCount: function() {
return _count;
},
emitChange: function() {
console.log('TestStore.emitChange');
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
console.log('TestStore.addChangeListener');
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
AppDispatcher.register(function(action) {
var text;
switch(action.actionType) {
case TestConstants.INCREASE_COUNT:
increaseCount();
TestStore.emitChange();
break;
default:
// no op
}
});
module.exports = TestStore;
As you said, the issue is in <Application />: You start listening to the store in componentDidMount, whereas you should do that in componentWillMount, otherwise you start listening to changes after all the components are mounted, therefore you lose the initial increment.
componentWillMount() {
TestStore.addChangeListener(this._onChange);
}
Anyway, I would suggest to perform the action in the top component:
In <Application />
componentDidMount() {
TestActions.increaseCount();
},
_handleClick() {
TestActions.increaseCount();
},
render() {
return <Tester callback={this._handleClick} count={this.state.count} />
}
In <Tester/>
<a href="#" onClick={this.props.callback}>Increase</a>