ReactJS: Move component state - javascript

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.

Related

VueJS XHR inside reusable component

Asking for best practice or suggestion how to do it better:
I have 1 global reusable component <MainMenu> inside that component I'm doing XHR request to get menu items.
So if I place <MainMenu> in header and footer XHR will be sent 2 times.
I can also go with props to get menu items in main parent component and pass menu items to <MainMenu> like:
<MainMenu :items="items">
Bet that means I cant quickly reuse it in another project, I will need pass props to it.
And another way is to use state, thats basically same as props.
What will be best option for such use case?
If you don't want to instantiate a new component, but have your main menu in many places you can use ref="menu" which will allow you to access it's innerHTML or outerHTML. I've created an example here to which you can refer.
<div id="app">
<main-menu ref="menu" />
<div v-html="menuHTML"></div>
</div>
refs aren't reactive so if you used v-html="$refs.menu.$el.outerHTML" it wouldn't work since refs are still undefined when the component is created. In order to display it properly you would have to create a property that keeps main menu's HTML and set it in mounted hook:
data() {
return {
menuHTML: ''
}
},
mounted() {
this.menuHTML = this.$refs.menu.$el.outerHTML;
}
This lets you display the menu multiple times without creating new components but it still doesn't change the fact that it's not reactive.
In the example, menu elements are kept in items array. If the objects in items array were to be changed, those changes would be reflected in the main component, but it's clones would remain unchanged. In the example I add class "red" to items after two seconds pass.
To make it work so that changes are reflected in cloned elements you need to add a watcher that observes the changes in items array and updates menuHTML when any change is noticed:
mounted() {
this.menuHTML = this.$refs.menu.$el.outerHTML;
this.$watch(
() => {
return this.$refs.menu.items
},
(val) => {
this.menuHTML = this.$refs.menu.$el.outerHTML;
}, {
deep: true
}
)
}
You can also watch for changes in any data property with:
this.$refs.menu._data
With this you don't need to pass props to your main menu component nor implement any changes to it, but this solution still requires some additional logic to be implemented in it's parent component.

React: Programmatically opening modals (and cleaning up automatically)

I have a site with lots of modals which can be opened from anywhere (for example LoginModal). The challenge I'm running into is if I open one programmatically with something like ReactDOM.render, how do I clean it up automatically when the parent component is unmounted without putting it (and all possible modals) in the template.
For example, something like this to open it:
openLoginModal() {
ReactDOM.render(<LoginModal />, document.body);
}
LoginModal can clean itself up when closed. However, if the DOM from the component which opened it is unmounted, how do I let LoginModal know to unmount as well.
One thought I've had is to use an Rx.Subject to notify it when to unmount, but this also sounds like a bit of a wrong approach and a possible anti-pattern.
For example:
// modules/User.js
openLoginModal(unmountSubj) {
const container = document.createElement('div');
ReactDOM.render(<LoginModal />, container);
unmountSubj.subscribe(() => {
ReactDOM.unmountComponentAtNode(container);
});
}
// components/RandomView.jsx
unmountSubject = new Rx.Subject();
componentWillUnmount() {
this.unmountSubject.next();
}
login() {
User.openLoginModal(this.unmountSubject);
}
I'd like to avoid having all the possible modal components in each JSX template they might be used in.
How would you approach this?
Here's the solution I've come up with so far: There's a modal manager module, which will render a modal into the DOM (via ReactDOM.render) and return a function which will unmount it.
Here's a simplified version:
// modalManager.js
export default modalManager = {
openModal(modalClass, props) {
// Create container for the modal to be rendered into
const renderContainer = document.createElement('div');
document.body.appendChild(renderContainer);
// Create & render component
const modalInst = React.createElement(modalClass, { ...props, renderContainer });
ReactDOM.render(modalInst, renderContainer);
// Return unmounting function
return () => this.unmountModal(renderContainer);
},
unmountModal(renderContainer) {
ReactDOM.unmountComponentAtNode(renderContainer);
renderContainer.parentNode.removeChild(renderContainer);
},
}
// TestThing.jsx
class TestThing extends React.Component {
unmountLogin = null;
componentWillUnmount() {
this.unmountLogin();
}
login() {
this.unmountLogin = modalManager.openModal(Login, {});
}
}
You'll also notice that renderContainer is passed to the modal component. This way the modal can call modalManager.unmountModal itself when closed.
Let me know what you think.
In my current React project, I've addressed this by having a multilayered component architecture.
<App>
// the standard starting point for React apps
<DataLayer>
// this is where I make API calls for data that is shared between all components
<DisplayLayer>
// this is where I put the methods to launch display elements that are shared
// by all components (e.g., modals, alerts, notifications, etc.)
<Template>
// this is the first layer that is actually outputting HTML content
<ModuleX>
<ModuleY>
<ModuleZ>
// these modules control the main display area of the screen, they encompass
// major UI functions (e.g., UsersModule, TeamsModule, etc.)
// when one of these modules needs to launch a shared UI element (like a
// modal), they call a method in the <DisplayLayer> template - this means
// that if I have a commonly-used modal (like, LoginModal), it doesn't need
// to be included in every one of the core modules where someone might need
// to initiate a login; these modules are mounted-and-unmounted as the user
// navigates through the app
So when the app loads, <App>, <DataLayer>, <DisplayLayer>, and <Template> all load up (and they will only load one time). As the user navigates around, the <ModuleX/Y/Z/etc> components are mounted-and-unmounted, but all of the "common stuff" stays in place that was mounted/loaded in the higher layers of the app.

How to allow child component to react to a routing event before the parent component?

I am using react, react-router & redux. The structure of my app is such:
CoreLayout
-> <MaterialToolbar /> (contains back button)
-> {children} (react-router)
When the user presses the back button, which is normally handled by the CoreLayout, I would like the current child component to handle the back button instead of the parent. (In my case, I would like the current view to check if its data has been modified, and pop up an 'Are you sure you wish to cancel?' box before actually going back.) If the child does not wish to handle this, the parent will do it's thing.
Another example would be allowing a childview to set the title in the toolbar.
My reading has told me that accessing a component through a ref and calling a method on it is not the react way -- this is also made a bit more difficult since I am using redux-connect. What is the correct way to implement this behavior?
This is how I would do it, assuming you mean your navigation back button (and not the browser back button):
class CoreLayout extends Component {
handleBack () {
//... use router to go back
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, child => React.cloneElement(child, { onBack: this.handleBack }))}
</div>
}
}
class Child extends Component {
handleBackButtonClick () {
// Here perform the logic to decide what to do
if (dataHasBeenModifiedAndConfirmed) {
// Yes, user wants to go back, call function passed by the parent
this.props.onBack()
} else {
// User didn't confirm, decide what to do
}
}
render () {
return <div onClick={this.handleBackButtonClick.bind(this)}>
Go Back
</div>
}
}
You simply pass a function from the parent to the child via props. Then in the child you can implement the logic to check if you really want to delegate the work to the parent component.
Since you use react-router and your children are passed to your parent component through this.props.children, to pass the onBack function you need to map the children and use React.cloneElement to pass your props (see this answer if you need more details on that: React.cloneElement: pass new children or copy props.children?).
Edit:
Since it seems you want to let the children decide, you can do it this way (using refs):
class CoreLayout extends Component {
constructor () {
super()
this.childRefs = {};
}
handleBack () {
for (let refKey in Object.keys(this.childRefs) {
const refCmp = this.childRefs[refKey];
// You can also pass extra args to refCmp.shouldGoBack if you need to
if (typeof refCmp.shouldGoBack === 'function' && !refCmp.shouldGoBack()) {
return false;
}
}
// No child requested to handle the back button, continue here...
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, (child, n) => React.cloneElement(child, {
ref: cmp => { this.childRefs[n] = cmp; }
}))}
</div>
}
}
class Child extends Component {
shouldGoBack () {
// Return true/false if you do/don't want to actually go back
return true
}
render () {
return <div>
Some content here
</div>
}
}
This is a bit more convoluted as normally with React it's easier/more idiomatic to have a "smart" parent that decides based on the state, but given your specific case (back button in the parent and the logic in the children) and without reimplementing a few other things, I think using refs this way is fine.
Alternatively (with Redux) as the other answer suggested, you would need to set something in the Redux state from the children that you can use in the parent to decide what to do.
Hope it's helpful.
I don't think there is a correct way to solve this problem, but there are many ways. If I understand your problem correctly, most of the time the back button onClick handler will be handled within CoreLayout, but when a particular child is rendered that child will handle the onClick event. This is an interesting problem, because the ability to change the functionality of the back button needs to be globally available, or at very least available in CoreLayout and the particular child component.
I have not used redux, but I have used Fluxible and am familar with the Flux architecture and the pub/sub pattern.
Perhaps you can utilize your redux store to determine the functionality of your back button. And your CoreLayout component would handle rendering the prompt. There is a bug with the following code, but I thought I would not delete my answer for the sake of giving you an idea of what I am talking about and hopefully the following code does that. You would need to think through the logic to get this working correctly, but the idea is there. Use the store to determine what the back button will do.
//Core Layout
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
// backFunction is a string correlating to name of function in Core Layout Component
if(state.backFunction) {
// lets assume backFunction is 'showModal'. Execute this.showModal()
// and let it handle the rest.
this[state.backFunction]();
// set function to false so its not called everytime the store updates.
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: false})
}
})
}
showModal() {
// update state, show modal in Core Layout
if(userWantsToGoBack) {
this.onBack();
// update store backFunction to be the default onBack
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'onBack'})
// if they don't want to go back, hide the modal
} else {
// hide modal
}
}
onBack() {
// handle going back when modal doesn't need to be shown
}
The next step is to update your store when the child component mounts
// Child component
componentDidMount(){
// update backFunction so when back button is clicked the appropriate function will be called from CoreLayout
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'showModal'});
}
This way you don't need to worry about passing any function to your child component you let the state of the store determine which function CoreLayout will call.

Reusability/Scalability issues with react-flux app

The question:
Is there any way to have a standard flux workflow - using Actions and Stores inside of a component and still be able to use this component for multiple different purposes, or if not is there any way to have complex nested structure in flux-react app without propagating every change trough a huge callback pipe-line?
The example (If the question is not clear enough):
Lets say I have a couple of super simple custom components like ToggleButton, Slider, DatePicker and more. They need to be reusable, so i can't use any actions inside of them, instead i've defined callback functions. For example onChange on the DatePicker fires like this:
this.props.onChange(data);
I have a custom component lets call it InfoBox that contains a couple of the simple components described above. This component listens for changes for every of its children like this:
<DatePicker ref='startDate' onChange={this.startDate_changeHandler} />
The InfoBox is used for different purposes so i guess it can not be binded to a specific store as well.
I also have a custom Grid component that render many instances of the InfoBox. This grid is used to show different data on different pages and each page can have multiple grids - so i think i can not bind it with Actions and Stores.
Now here is where it all gets crazy, bear with me - I have couple of pages - Clients, Products, Articles, etc.. each of them have at least one Grid and every grid have some filters (like search).
The pages definitely can use actions and store but there are big similarities between the pages and I don't want to have to duplicate that much code (not only methods, but markup as well).
As you may see it's quite complex structure and it seems to me that is not right to implement pipe-line of callback methods for each change in the nested components going like DataPicker > InfoBox > Grid > Page > Something else.
You're absolutely right in that changing the date in a DatePicker component should not trigger a Flux action. Flux actions are for changing application state, and almost never view state where view state means "input box X contains the value Z", or "the list Y is collapsed".
It's great that you're creating reusable components like Grid etc, it'll help you make the application more maintainable.
The way to handle your problem is to pass in components from the top level down to the bottom. This can either be done with child components or with simple props.
Say you have a page, which shows two Grids, one grid of - let's say - meeting appointments and one grid with todo notes. Now the page itself is too high up in the hierarchy to know when to trigger actions, and your Grid and InfoBox are too general to know which actions to trigger. You can use callbacks like you said, but that can be a bit too limited.
So you have a page, and you have an array of appointments and an array of todo items. To render that and wire it up, you might have something like this:
var TodoActions = {
markAsComplete: function (todo) {
alert('Completed: ' + todo.text);
}
};
var InfoBox = React.createClass({
render: function() {
return (
<div className="infobox">
{React.createElement(this.props.component, this.props)}
</div>
);
}
});
var Grid = React.createClass({
render: function() {
var that = this;
return (
<div className="grid">
{this.props.items.map(function (item) {
return <InfoBox component={that.props.component} item={item} />;
})}
</div>
);
}
});
var Todo = React.createClass({
render: function() {
var that = this;
return (
<div>
Todo: {this.props.item.text}
<button onClick={function () { TodoActions.markAsComplete(that.props.item); }}>Mark as complete</button>
</div>
);
}
});
var MyPage = React.createClass({
getInitialState: function () {
return {
todos: [{text: 'A todo'}]
};
},
render: function() {
return (
<Grid items={this.state.todos} component={Todo} />
);
}
});
React.render(<MyPage />, document.getElementById('app'));
As you see, both Grid and InfoBox knows very little, except that some data is passed to them, and that they should render a component at the bottom which knows how to trigger an action. InfoBox also passes on all its props to Todo, which gives Todo the todo object passed to InfoBox.
So this is one way to deal with these things, but it still means that you're propagating props down from component to component. In some cases where you have deep nesting, propagating that becomes tedious and it's easy to forget to add it which breaks the components further down. For those cases, I'd recommend that you look into contexts in React, which are pretty awesome. Here's a good introduction to contexts: https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html
EDIT
Update with answer to your comment. In order to generalize Todo in the example so that it doesn't know which action to call explicitly, you can wrap it in a new component that knows.
Something like this:
var Todo = React.createClass({
render: function() {
var that = this;
return (
<div>
Todo: {this.props.item.text}
<button onClick={function () { this.props.onCompleted(that.props.item); }}>Mark as complete</button>
</div>
);
}
});
var AppointmentTodo = React.createClass({
render: function() {
return <Todo {...this.props} onCompleted={function (todo) { TodoActions.markAsComplete(todo); }} />;
}
});
var MyPage = React.createClass({
getInitialState: function () {
return {
todos: [{text: 'A todo'}]
};
},
render: function() {
return (
<Grid items={this.state.todos} component={AppointmentTodo} />
);
}
});
So instead of having MyPage pass Todo to Grid, it now passes AppointmentTodo which only acts as a wrapper component that knows about a specific action, freeing Todo to only care about rendering it. This is a very common pattern in React, where you have components that just delegate the rendering to another component, and passes in props to it.

How to trigger re-render on model change in ReactJS?

I am creating a react component using
React.render(<ReactComponent data="myData">, document.body);
Once the data model changes, I call render again using
React.render(<ReactComponent data="myData">, document.body);
Is this the right/recommended way to update my HTML?
Will this utilize the advantages of the React virtual DOM (i.e. rendering only the elements that have actually changed).
Also, should I be using state or properties when passing in myData?
You should be rendering only one main App component which does AJAX requests etc and uses the data model inside its render function to update sub components.
When creating React components you should always keep the use of state minimal and move it up to the top level component, Instead you should use props to render child components.
This article helped me a lot when i was first getting started with React: https://github.com/uberVU/react-guide/blob/master/props-vs-state.md
so something like:
var App = React.createClass({
render: function(){
return (
<div>
<input type="button" onClick={this.handleClick}/>
<Dropdown items={this.state.countries}/>
</div>
)
},
getInitialState: function(){
return {countries: {}};
},
componentDidMount: function(){
var self = this;
$.getJSON("countries", function(err, countries){
self.setState({countries: countries});
});
},
handleClick: function(){
// every time the user does something,
// all you need to do is to update the state of the App
// which is passed as props to sub components
}
})
React.render(React.createElement(App, {}), document.body);

Categories

Resources