How can I change the state of a React component from my old legacy jQuery soup
code?
I have a component like this:
var AComponent = React.createClass({
getInitialState: function() {
return { ids: [] }
},
render: function() {
...
},
onButtonClick: function() {
ids.splice(…); // remove the last id
}
});
When something special happens in the old jQuery soup code, I'd like to
push an id to AComponent.state.ids. How can I do that?
One "obvious" solution is an anti-pattern; here it is:
var componentInstance = AComtonent({});
React.renderComponent(componentInstance, document.getElementById(...));
// Somewhere else, in the jQuery soup. Something special happens:
componentIntance.state.ids.push(1234);
componentIntance.setState(componentInstance.state);
This is an antipattern, according to this email from a Facebook
developer,
because he writes that componentInstance might be destroyed by React.
I would make the component stateless. Store the ids array outside of your component and pass it as a prop with functions that will modify the array. See example on JSFiddle: http://jsfiddle.net/ohvco4o2/5/
Related
What i want to achieve
I have a store object and i want to populate one of it's properties (which is an empty array) with instances of another object. And i want one of my react component automatically updated when part of the mentioned array instance is changed.
What my problem is
By logging out the value of this constructed store object i can see the change in the browser console, but it doesn't get updated automatically in the react component when its value changed.
So i'd like to get hints, examples of how to implement a solution like this in general.
Details
My project
I want to create a MobX store called Session which would store everything my react webapp needs.
This webapp would be a document handling tool, so after creating new or loading existing documents i want to add the Document object to the Session (into an object array called documents).
Some more details: a Document consists of one or more section. So using a WYSIWYG editor i add its content to the given section every time it's content changes.
Problem
I can add a new Document to the Session, i can update section as well(I can log out the value of a section in console), but using the Session reference to this document and section in a react component it doesnt update its state when section value is changed.
To my understanding in my example the reference of a Document is not changed when the value of a section is changed and hence it doesn't trigger MobX to react.
What i found so far
I started to dig in the deep, dark web and found this article.
So i started getting excited since asStructure (or asMap) seemed to solve this issue, but it looks like asStructure in MobX is deprecated.
Then i found this issue thread, where a thing called observable.structurallyCompare is mentioned. But again i found nothing about this in MobX documentation so im puzzled how to implement it.
So im stuck again and have no idea how to solve this problem.
Code excerpts from my project
This is how i reference to the mentioned Session value in the main react component:
import userSession from '../client/Session';
import {observer} from 'mobx-react';
#observer class App extends React.Component {
...
render() {
return (
...
<div>{JSON.stringify(userSession.documents[0].content.sections)}</div>
...
This is how i update the section in the editor react component:
import userSession from '../../client/Session';
...
handleChange(value,arg2,arg3,arg4) {
this.setState({ content: value, delta: arg4.getHTML()});
userSession.documents[0].updateSectionContent(this.props.id, this.state.delta);
}
}
...
Session.js excerpt:
class Session {
constructor(){
extendObservable(this, {
user: {
},
layout: {
},
documents: []
})
//current hack to add an empty Document to Session
this.documents.push(new Document());
}
//yadayadayada...
#action addNewSection() {
userSession.documents[0].content.sections.push({
type: "editor",
id: userSession.documents[0].getNextSectionID(),
editable: true,
content: "",
placeholder: "type here to add new section..."
});
}
}
var userSession = window.userSession = new Session();
export default userSession;
Document.js
import {extendObservable, computed, action} from "mobx";
import userSession from "./Session";
class Document {
constructor(doc = null) {
if (doc == null) {
console.log("doc - no init value provided");
extendObservable(this,{
labels: {
title: "title",
description: "description",
owners: ["guest"]
},
content: {
sections: [
{
type: "editor",
id: "sec1",
editable: true,
placeholder: "this is where the magic happens"
},
]
}
})
} else {
console.log("doc - init values provided: ");
extendObservable(this,{
labels: doc.labels,
content: doc.content
});
}
}
getNextSectionID(){
return `sec${this.content.sections.length}`;
}
#action updateSectionContent(sectionID, delta) {
console.log("doc - looking for section " + sectionID + " to update with this: " + delta);
let index = this.content.sections.findIndex(
section => section.id === sectionID
);
userSession.documents[0].content.sections[index].content = delta;
}
}
export default Document;
Ps.: atm moment i don't remember why i made Document properties observable, but for some reason it was necessary.
Unfortunately, you are implementing mobx with React the incorrect way. To make it more comprehensible, I suggest you look into the implementation of the HOC observer that mobx-react provide.
Basically, what this HOC does is to wrap your component inside another React component that implement shouldComponentUpdate that check when the props referred inside render function change, and trigger the re-render. To make React component reactive to change in mobx store, you need to pass the store data to them as props, either in React traditional way, or via the inject HOC that mobx-react provide.
The problem while your code does not react to change in the store is because you import the store and use them directly inside your component, mobx-react cannot detect change that way.
To make it reactive, instead of import it and use it directly in your App component, you can pass the store as a prop as follow:
#observer
class App extends React.Component {
...
render() {
return (
...
<div>{this.props.sections}</div>
...);
}
}
Then pass the store to App when it's used:
<App sections={userSession.documents[0].content.sections} />
You can also have a look at how to use inject here: https://github.com/mobxjs/mobx-react.
An just a suggestion: before jumping directly on some best pattern, try to stick with the basic, standard way that library author recommend and understand how it works first, you can consider other option after you got the core idea.
I have two Vue components that use a common array set in a store like this:
var store = {
state: {
myArray: []
}
};
var FirstComp = Vue.extend({
template: '#first-template',
data: function () {
return {
arrayData: store.state.myArray
};
}
});
/* A second component quite identical */
I followed the example given in the Vue js guide.
I'm trying to update the data in the array in the store with new data from another array (after an ajax call), so that it impacts both components. I would like to have a nice way of replacing / concating the old array with a new one. I know I can't just replace the array like this store.state.myArray = newArrayData;because I would loose the Vue binding. But the method given in the docs (at least for concat) doesn't work in the case of the store (or maybe I'm missing something?).
Right now, the only way I've found is to use a foreach with push, $removeor $set depending on the operation and it is not that elegant and practical.
For example, for concat, I do this:
$.each(newArray, function (i, val) {
store.state.myArray.push(val);
});
But for replacing it gets uglier. What would be the proper way to this?
(For info, I'm not using Vuex for state management and I don't plan to at the moment, I'm keeping it very simple)
To make the assignment work you can just use "state" in your component like this:
var FirstComp = Vue.extend({
template: '#first-template',
data: function () {
return {
state: store.state
};
}
});
And then use state.myArray. This way if you will do store.state.myArray = newValue it won't break the binding.
I'll try my best to explain the scenario, as making a fiddle of the smallest possible case might be hard. Essentially, I have a SPA using React-Router. I'm currently getting a strange behavior in specifically one version of Firefox (31.4esr).
I have two sidebar icons which trigger a change in routes, navigating to a new page. On occasion when I switch quickly between them, I'm getting an error that this.state.list is undefined(this is a list that I populate a dropdown with).
The issue is, upon debugging, console.log(this.state) is returning an empty object just before the call (that errors) to this.state.list happens in my render method. However, I have list defined in getInitialState (along with a bunch of other state variables) and so this.state definitely shouldn't be empty.
The only thing I could think of that would be causing this is if due to the quick switching there is some confusion with mounting/unmounting of components and my component still thinks it is mounted, so skips the getInitialState and goes ahead and tries to render. Either that or some bug in React-Router.
Any thoughts? Thanks in advance for the help!
Nick
P.S I should reiterate this also only occurs very rarely during quick switching, and normally the componentDidMount -> getInitialState -> render occurs as expected, so it is not simply an error in how my getInitialState is written etc.
Edit: Using React 0.13.3 and React router 0.13.3
Edit 2: Here is the stripped down version of the lifecycle methods, very basic.
getInitialState: function() {
return { list: listStore.getList("myList") || [] }
},
render: function() {
var newList = [];
//this is the line that errors with this.state.list is undefined
this.state.list.forEach(function(listItem) {
...
}
return (
<div>
<OtherComponent newList={newList} />
</div>
)
};
When putting console.log in componentWillMount (just attaches store listeners), getInitialState, and render, I get output like this when the error occurs:
"Setting initial state"
"2,3" //This is this.state.list in componentWillMount
"2,3" //This is this.state.list in initial Render
Object { } //This is this.state the next time it gets called in render :S.
you will have to use mixin like this:
var state = {
getInitialState: function() {
return { list: listStore.getList("myList") || [] }
}
}
var nameRouter = React.createClass({
mixins : [state],
// more content
})
this is because react-router ignore the getInitialState that was definite
I'm trying to develop a React mixin to check the user access level before rendering the component.
If the user doesn't have the permission to see the component, I would like to disable the rendering of the component.
I've been looking for something build in react to handle this but found nothing, so I did that:
var AuthentLevelMixin = {
componentWillMount: function() {
if(!Auth.check()) {
// Disable component render method
this.render = function () {
return false;
}
}
}
}
It works as expected but I feel like it's the "dirty way".
So my question is: what is the "React way" for doing the same as this snippet ?
For a mixin this is about the best you can do. It's just a simple early return in render.
var AuthentLevelMixin {
isAuthenticated: function(){
return Auth.check();
}
};
var C = React.createClass({
mixins: [AuthentLevelMixin],
render: function(){
if (!this.isAuthenticated()) return <div />;
return (
<div>...</div>
);
}
});
If you decide to go with your initial strategy (I don't recommend it), it just needs to be modified slightly:
// more explicit names are important for dirty code
var PreventRenderUnlessAuthMixin = {
componentWillMount: function() {
this._originalRender = this.render;
this._setRenderMethod();
},
componentWillUpdate: function(){
this._setRenderMethod();
}.
_emptyRender: function () {
return <span />;
},
_setRenderMethod: function(){
this.render = Auth.check() ? this._originalRender : this._emptyRender;
}
}
If you want to handle the authorization inside your mixin without adding logic to your component you are doing it the right way. BUT: Every component implementing this mixin should then be aware of what happens within this mixin. If the result you expect is, that nothing is rendered, then you are perfectly right with what you are doing. So if your way is resulting in simplicity it is the React-Way. And in my Opinion this is the case.
In the componentWillMount lifecycle event you will capture the moment right before rendering - which is a great time to prevent rendering. So I really dont see anything speaking against your code.
EDIT:
aproach of defining: "react way"
Once you have the same input resulting in the same output every time your code becomes predictable. With your code being predictable you achieve simplicity. These are terms used by Pete Hunt to describe the intentions of React. So therefor if you stay predictable and in result achieving simplicity you are doing it the react way.
In case of the above mixin both these rules apply and is therefor the "react way" in the definition I have provided above.
My advice here would be to not use a mixin. The best way to clean up your component is to remove this logic from the component, and simply not render the component based on the result of checking Auth.
The problem with this is that you have a component that is no longer consistent, because it depends on something other than its props. This doesn't really do much other than push the problem upwards, but it does allow you to have one more pure component.
I can see why the mixin is attractive though, so here's a simpler way of doing what you need that doesn't involve dynamically swapping the render method:
var PreventRenderUnlessAuthMixin = {
componentWillMount: function () {
var oldRender = this.render;
this.render = function () {
return Auth.check() ? this.render() : <div />
}.bind(this);
}
}
I am trying to implement a dialog-heavy application. Often, a component needs to launch a dialog in a fire-and-forget fashion, so I have a function create_dialog() that takes a React.DOM element and appends it to the dialogs array in the state of the App component (the top-level component) which in its render() function renders all dialogs.
This is easy to use and works fine, but now I want to be able to minimize dialogs and show a little preview in a task bar. The way I tried to do this is to add a new array previews to the Apps state and when minimizing a dialog, I would remove it from dialogs and append it to previews which is rendered into a different element on the page.
The problem is that React.DOM.* elements are just lightweight references that don't store state, so when minimizing dialogs all state is lost because the dialog components have to be remounted (and as far as I know there is no way to really move a DOM element in ReactJS).
Is there an easy way to move/copy the state of a component without making changes to it?
Or is there a different approach?
var Application = React.createClass(
{
getInitialState: function()
{
return {dialogs: [], previews: []};
},
render: function()
{
return (
<div>
<div id="previews">{this.state.previews}</div>
{this.state.dialogs}
</div>
);
},
...
});
var g_app = React.renderComponent(<Application />, ...);
function create_dialog(header, content)
{
var new_dialog_state = g_app.state.dialogs.slice();
new_dialog_state.push(<Dialog header={header}>{content}</Dialog>);
g_app.setState({dialogs: new_dialog_state});
}
var Item = React.createClass(
{
onButtonClick: function()
{
create_dialog('Item info', <ItemInfo data={this.state.item_info} />);
},
...
});
As mentioned in the comments:
Okay, I solved the problem by storing the state outside components. Components now only user their props which are set by a separate Store object.
- DaviD.