React: Programmatically opening modals (and cleaning up automatically) - javascript

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.

Related

Reactjs render and state change dependancies

I'm learning ReactJS at the moment and struggling to understand how to render/update content based on changes elsewhere.
Example:
I have a timer app, which includes pause/restart functionality. It contains a Start/Pause button.
Timer.js
export class Timer {
constructor(parentApp) {
this.app = app;
this.playing = false;
}
start() {
this.playing = true;
}
pause() {
this.playing = false;
}
}
Button.js
export class IconButtonBar extends Component {
constructor(props) {
super(props);
this.state = {label: 'Start'};
}
render() {
return (
<div className="IconButtonBar">
<Button label={this.state.label} />
</div>
);
}
}
I want to update the label:
Depending whether the timer is started/stopped at initial render
When the timer is manually started/stopped by pressing the button
When the timer is manually started/stopped by another means
When the timer is programatically started/stopped (e.g. limit reached)
In jQuery I'd probably fire a custom event trgger to the body tag:
$('body').on('start_playing', function() {
$('#playpause_button).text('Pause');
}
But there are probably much more 'native' ways to do this in ReactJS.
I hope this makes sense, and you can help!
You need to change your vision of how react app is structurized. Just forget about jQuery. React is declarative, you need to say it what should be rendered (surely it can be done another way, but why to use React then?).
You can use stateful component if you have no third-party store library.
You can use conditional redering for showing different elements depending on different state.
Here is an example stateful component of what you wanted to achieve: https://codesandbox.io/s/14vokm9q14
Just remember, it is not a good practice to keep state in a view layer. Consider using Redux/Mobx:
Redux - it will require a lot of boilerplate: creation of reducers, action-creators, action-types, handling side-effects (with redux-thunk/redux-saga/redux-observable), etc. But it is stable, reliable and easy-testable.
Mobx - something like you have in your example. It is MVVM, where you have model in its classical meaning, decorate properties as observable and just inject this model into your react component. After that you can just mutate properties of your model and these changes will reflect onto your view.

Is componentDidMount causing html.script to disappear?

I am having issues mounting an external script into a component of my React/Gatsby App. The script below is called into a component that is used in two places throughout app.
First being pages/index.js and loads fine with zero issue, yet when called to use within a gatsby created page (exports.createPages = ({ graphql, boundActionCreators }) => {) from a template the script will load, show content and then go.
Here is the code for the script being mounted into the component -
componentDidMount () {
const tripadvisor = document.createElement("script");
tripadvisorLeft.src = "https://www.jscache.com/wejs?wtype=selfserveprop&uniq=789&locationId=10467767&lang=en_NZ&rating=true&nreviews=0&writereviewlink=true&popIdx=true&iswide=true&border=false&display_version=2";
tripadvisorLeft.async = true;
document.body.appendChild(tripadvisor);
}
I am not getting any errors from the console.
NOTE: Incase of relation to the error? I also have this code using componentDidMount and componentWillUnmount in the /layout/index.js file that handles a body class for navigation elements.
componentDidMount () {
this.timeoutId = setTimeout(() => {
this.setState({loading: ''});
}, 100);
this.innerContainer.addEventListener("scroll", this.handleHeaderStuck), 100;
this.innerContainer.addEventListener("scroll", this.handleSubNavStuck), 200;
}
componentWillUnmount () {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.innerContainer.removeEventListener("scroll", this.handleHeaderStuck);
this.innerContainer.removeEventListener("scroll", this.handleSubNavStuck);
}
UPDATE: All code
import React from 'react';
import Link from 'gatsby-link'
import styled from 'styled-components'
const Wrapper = styled.section`
display:block;
`
class ReviewsPage extends React.Component {
componentDidMount () {
const tripadvisorLeft = document.createElement("script");
tripadvisorLeft.src = "https://www.jscache.com/wejs?wtype=selfserveprop&uniq=789&locationId=10467767&lang=en_NZ&rating=true&nreviews=0&writereviewlink=true&popIdx=true&iswide=true&border=false&display_version=2";
tripadvisorLeft.async = true;
document.body.appendChild(tripadvisorLeft);
}
render() {
return (
<Wrapper id="tripAdvisor">
<div id="TA_selfserveprop789" className="TA_selfserveprop">
<ul id="3LacWzULQY9" className="TA_links 2JjshLk6wRNW">
<li id="odY7zRWG5" className="QzealNl"></li>
</ul>
</div>
</Wrapper>
)
}
}
export default ReviewsPage
So, all your componentDidMount() is doing is adding a <script> tag which references a third party script. I am assuming that third party script tries to add some information or thing to the DOM (something you can see visually).
However, the DOM only exists between component updates. React will completely redraw the DOM (the HTML inside your component) any time it detects a change to State or Props. I'm assuming in this case that Wrapper is what is resetting each time.
I'm not sure how to help with this, mainly because React's entire role in an application is really just managing the state of the DOM, and that script is trying to edit the DOM, but without telling React. React might be sensing an invalid change to the DOM then trying to correct it, but I really don't think React does that. At any rate, the issue is that React is trying to manage the DOM while another thing is trying to edit the DOM, and that's not gonna end well.
It would be better if you could have a script that asynchronously calls to the other service and receives data, then let React apply that data to the DOM, instead of letting the script edit the DOM itself. Granted, you probably don't have control over how that external script actually works, which is why I say I'm not sure how to help.

How to get React setState to re-render for Foundation 6 Reveal (Modal)?

My issue has been touched in a few questions around the web but I don't think it's been holistically asked.
I am using Foundation 6 with React. Everything works by using
import $ from 'jquery';
window.jQuery = $;
let foundation = require('path/to/foundation.js');
then in componentDidUpdate(), I call $(document).foundation(). Also, the CSS is being called somewhere.
My problem is once I get the modal to open, I can't populate it with data using setState(). I think I understand that the DOM changes when the modal opens, thus causing issues but I was wondering if anyone has had success with the Reveal plugin? My code is like this:
getData() {
Facebook.get('/me/taggable_friends', function(error, response) {
$('#modal').foundation('open');
//setTimeout is just for testing sanity
let _this = this;
setTimeout(function() {
_this.setState({ friends: response.data });
}, 3000);
})
}
Again, everything works. I'm getting data back from Facebook, the state is updating, the modal is opening, I'm just not able to populate the modal and I have this as my markup:
<div id="modal" className="modal-classes-from-foundation" data-reveal>
{
this.state.friends.length > 0 &&
this.state.friends.map((friend, i) => {
return(
<div>{ friend.name }</div>
)
})
}
</div>
Also to note, this.state.friends is being set in the constructor as an empty array.
All the code is valid on my server (no errors), but I wrote this from memory so I didn't remember small details like class/path names
Things I tried
Using componentWillReceiveProps to force update
Setting state before calling modal open
I dont think your answer is so smart. AFAIK the current state of the art is to set a nodeReference using a setNode method and then have
ref={this.setNode}
in your element, so then you can call whatever you are calling with jQuery, passing it via argument the node so you can play with it, and use the React lifecycle hooks to sync your react world with your jQuery playground
componentWillMount() {
window.addEventListener('resize', this.handleWindowResize, false);
}
componentDidMount() {
//initialize your jQuery dom manipulation
}
shouldComponentUpdate(nextProps) {
if (!this.props.store.equals(nextProps)) {
//to update stuff
}
return false; //> dont update
}
componentWillUnmount() {
//
}
setNode(ref) {
this.nodeReference = ref;
}
After banging my head against a virtual desk for a day, I understand why it's tough getting Reveal to work with React, especially compared to the other Foundation elements.
Reveal is an overlay that lives inside of <body></body> but not inside <div id="app"></div>, or whatever you name the root div that your React app renders to. Since the overlay is outside of the "app", React has no control over it and sending states/props to it won't do anything.
What I ended up doing is a bit clever and could be looked down upon, but seemed very necessary. I took inspiration from BlackMutt in 2015 where he basically created a function to use jQuery for appending the modal's code and initializing it. Unfortunately, this means every other thing you do with the modal will need to use jQuery as well but the good news is that it's separated from the rest of the app. Here's a sample of what it looks like:
createListModal(items) {
let content = items.map((item, i) => {
return $('<div class="list-item"><div class="item-name">'+ item.name +'</div></div>');
});
let close = $('<button class="close-button" data-close aria-label="Close modal" type="button"><span aria-hidden="true">×</span></button>');
let modal = $('<div class="reveal" id="list-popup" data-reveal>').append(content).append(pagination).append(close);
$(modal).foundation();
$('#list-popup').foundation('open');
}
So all I did was call that method when I got my data from Facebook. It's pretty simple but if you've been in React for a while, you have to switch your brain back into thinking in jQuery again.

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.

ReactJS: Move component state

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.

Categories

Resources