Reactjs refresh parent after react modal popup changes data - reactjs

I am using react modal to set up a pop up in on one of my components. I have a component that renders a div wrapped with react modal. My parent component renders the modal component on load with the isOpen set to false. Clicking a link on the parent sets isOpen to true and causes the modal popup to open.
I make changes to my data within the open popup and then save the changes and close the model when the close button is clicked.
I am using a redux set up with actions and reducers handling the data changes and state changes.
Conceptually, how would I update the parent to show the changes made from the popup? Shouldn't changes to my data using an action cause the store to regenerate and hence update the data in my components or do I have to explicitly "refresh" my store after saving? My issue is that right now when the popup closes it is not tirggering any kind of "refresh" on the DataView component.
Components below:
DataView component
import React from 'react';
import DataView from './MonthView.js';
import DataViewPopup from './MonthViewPopup.js';
import { connect } from 'react-redux';
import { getAction } from '../actions/actions.js';
import { getActionAll } from '../actions/actions.js';
import { getPopupData } from '../actions/actions.js';
class DataViewContainer extends React.Component {
constructor() {
super();
this.popupCategory = undefined;
this.popupMonth = undefined;
this.state = {
detailPopup : false,
refreshView: false
}
this.handleAddYear = this.handleAddYear.bind(this);
this.handleSubtractYear = this.handleSubtractYear.bind(this);
this.handleGetDetail = this.handleGetDetail.bind(this);
}
componentDidMount() {
this.props.getAction(2016);
this.props.getActionAll(2016);
}
render() {
return (
<div className="row">
<div className="col-sm-8">
<MonthView transactions={this.props.storeData.storeData} selectedYear={this.props.storeData.selectedYear} onAddYear={this.handleAddYear} onSubtractYear={this.handleSubtractYear} onHandleGetDetail={this.handleGetDetail} />
</div>
<div className="col-sm-4">
<MonthViewPopup modalActive={this.state.detailPopup} transactions={this.props.storePopupData.getPopupData} selectedYear={this.props.storeTransactions.selectedYear} category={this.popupCategory} month={this.popupMonth}/>
</div>
</div>
)
}
handleGetDetail(category,month) {
console.log("props inside handleGetDetail: ", this.props);
this.popupCategory = category;
this.popupMonth = month;
let popupYear = this.props.storeTransactions.selectedYear
this.props.getPopupData(popupYear, month, category);
this.setState({ detailPopup: true}, function () {});
}
}
const mapStateToProps = (state) => ({
storeData: state.storeData,
storePopupData: state.storePopupData,
storeDataAll: state.storeDataAll
});
export default connect(mapStateToProps, {getAction,getActionAll,getPopupData})(DataViewContainer);
DataViewPopup component
import React from 'react';
import Modal from 'react-modal';
import { connect } from 'react-redux';
import { saveAction } from '../actions/actions.js';
import { getActionAll } from '../actions/actions.js';
class DataViewPopup extends React.Component {
constructor (props) {
super(props)
this.editedData = new Map();
this.state = {
modalIsOpen: false
};
this.openModal = this.openModal.bind(this);
this.afterOpenModal = this.afterOpenModal.bind(this);
this.closeModal = this.closeModal.bind(this);
this.renderFilteredTransactions = this.renderFilteredTransactions.bind(this);
}
openModal() {
this.setState({modalIsOpen: true});
}
afterOpenModal () {
console.log("inside afterOpenModal");
}
closeModal () {
this.props.saveTransactions(this.editedData);
this.editedData = new Map();
this.setState({ modalIsOpen: false }, function () {});
}
componentWillUnmount() {
this.props.getDataAll(); // i tried this but it does not work because the component (modal popup) is still mounted, but just not visible
//this.props.refreshParent();
}
componentWillReceiveProps(nextProps){
if (nextProps.modalActive === true) {
this.openModal();
return;
}
}
render () {
return <div>
<Modal
isOpen={this.state.modalIsOpen}
//isOpen={this.modalActive}//not needed as is currently setup
onAfterOpen={this.afterOpenModal}
onRequestClose={this.closeModal}
//style={customStyles}
contentLabel="Example Modal"
>
<button onClick={this.closeModal}>close</button>
{this.renderFilteredTransactions(this.props.data,this.props.category,this.props.month)};
</Modal>
</div>
}
renderFilteredTransactions(trans,category,month){
return <div>
<table>
<tbody className="mo-tblBody">
{trans && trans.map((data,index) =>
<tr key={data.transid}>
<td>{data.transid}</td>
<td>
{this.renderCategory(data.category,index,data.transid)}
</td>
</tr>
)}
</tbody>
</table>
</div>
}
handleCategoryChange(value,transIndex,transId){
let trans = JSON.parse(JSON.stringify(this.props.transactions));
//add or updated transaction to this.editedTransactions based on whether or not the transid already exists
trans.filter(item => item.transid === transId)
.map(item => this.editedData.set(transId,
Object.assign({}, item, { category: value })));
}
}
const mapStateToProps = (state) => {
return {storeDataAll: state.storeDataAll,
storeSaveData: state.storeSaveData
}
};
export default connect(mapStateToProps, {getDataAll,saveData})(DataViewPopup);

I covered this kind of question in my recent post Practical Redux, Part 10: Managing Modals and Context Menus.
The basic approaches are:
Pass a callback function as a prop to the modal component (which will work, but if you're driving the modal display from Redux state, putting functions into the Redux state is not encouraged)
Create a "pre-built" action, pass that as a prop to the modal, and have the modal dispatch that action as a "return value" when it closes
Use a middleware like redux-promising-modals to track actions for opening and closing modals, and use the promises it returns to handle "return values" when modals are closed.

Related

React.js Dropdown component doesn't hide on button click

I'm facing a problem in the making of clickable Dropdown component. My task is to show a menu when a button is clicked and hide the menu when the user clicks anywhere in the document or whether a click on the same button, also all components should be functional components.
I'm using 3rd party package named classnames which help to conditionally join CSS classes, also using a React ContextAPI to pass props to Dropdown child components.
Dropdown component depends upon 2 child components.
DropdownToggle -
(Renders a clickable button)
DropdownMenu -
(Renders a div with menu items)
Problem:
Whenever I open a menu and click anywhere in the document menu works perfectly, but when I open a menu and want to hide with a button click it didn't work. I think the problem is inside the useEffect hook of the Dropdown component.
Codesandbox
Demo:
Here is the main App component which renders all components.
App.js
import React, { Component } from "react";
import Dropdown from "./Dropdown";
import DropdownToggle from "./DropdownToggle";
import DropdownMenu from "./DropdownMenu";
import "./dropdown.css";
// App component
class App extends Component {
state = {
isOpen: false
};
toggle = () => {
alert("Button is clicked");
this.setState({
isOpen: !this.state.isOpen
});
};
render() {
return (
<div className="app">
<Dropdown isOpen={this.state.isOpen} toggle={this.toggle}>
<DropdownToggle>Dropdown</DropdownToggle>
<DropdownMenu>
<div>Item 1</div>
<div>Item 2</div>
</DropdownMenu>
</Dropdown>
</div>
);
}
}
export default App;
Main src code:
DropdownContext.js
import {createContext} from 'react';
// It is used on child components.
export const DropdownContext = createContext({});
// Wrap Dropdown with this Provider.
export const DropdownProvider = DropdownContext.Provider;
Dropdown.js
import React, { useEffect } from "react";
import classNames from "classnames";
import { DropdownProvider } from "./DropdownContext";
/**
* Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`.
* #param obj
* #param omitKeys
*/
const omit = (obj, omitKeys) => {
const result = {};
// Get object properties as an array
const propsArray = Object.keys(obj);
propsArray.forEach(key => {
// Searches the array for the specified item, if the item is not found it returns -1 then
// construct a new object and return it.
if (omitKeys.indexOf(key) === -1) {
result[key] = obj[key];
}
});
return result;
};
// Dropdown component
const Dropdown = props => {
// Populate context value based on the props
const getContextValue = () => {
return {
toggle: props.toggle,
isOpen: props.isOpen
};
};
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
return props.toggle(e);
};
// handle click for the document object
const handleDocumentClick = e => {
// Execute toggle function of the parent
toggle(e);
};
// Remove event listeners
const removeEvents = () => {
["click", "touchstart"].forEach(event =>
document.removeEventListener(event, handleDocumentClick, true)
);
};
// Add event listeners
const addEvents = () => {
["click", "touchstart"].forEach(event =>
document.addEventListener(event, handleDocumentClick, true)
);
};
useEffect(() => {
const handleProps = () => {
if (props.isOpen) {
addEvents();
} else {
removeEvents();
}
};
// mount
handleProps();
// unmount
return () => {
removeEvents();
};
}, [props.isOpen]);
// Condense all other attributes except toggle `prop`.
const { className, isOpen, ...attrs } = omit(props, ["toggle"]);
// Conditionally join all classes
const classes = classNames(className, "dropdown", { show: isOpen });
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} />
</DropdownProvider>
);
};
export default Dropdown;
Dropdown component has a parent i.e. a Provider whenever Provider values will change child components will access those values.
Secondly, on the DOM it will render a div which consists of Dropdown markup structure.
DropdownToggle.js
import React, {useContext} from 'react';
import classNames from 'classnames';
import {DropdownContext} from './DropdownContext';
// DropdownToggle component
const DropdownToggle = (props) => {
const {toggle} = useContext(DropdownContext);
const onClick = (e) => {
// If props onClick is not undefined
if (props.onClick) {
// execute the function
props.onClick(e);
}
toggle(e);
};
const {className, ...attrs} = props;
const classes = classNames(className);
return (
// All children would be render inside this. e.g. `svg` & `text`
<button type="button" className={classes} onClick={onClick} {...attrs}/>
);
};
export default DropdownToggle;
DropdownMenu.js
import React, { useContext } from "react";
import classNames from "classnames";
import { DropdownContext } from "./DropdownContext";
// DropdownMenu component
const DropdownMenu = props => {
const { isOpen } = useContext(DropdownContext);
const { className, ...attrs } = props;
// add show class if isOpen is true
const classes = classNames(className, "dropdown-menu", { show: isOpen });
return (
// All children would be render inside this `div`
<div className={classes} {...attrs} />
);
};
export default DropdownMenu;
Jayce444 answer is correct. When you click the button, it fires once, then the event bubbles up to the document and fires again.
I just want to add another alternative solution for you. You can use useRef hook to create a reference of Dropdown node and check if the current event target is button element or not. Add this code to your Dropdown.js file.
import React, { useRef } from "react";
const Dropdown = props => {
const containerRef = useRef(null);
// get reference of the current div
const getReferenceDomNode = () => {
return containerRef.current;
};
// handle click for the document object
const handleDocumentClick = e => {
const container = getReferenceDomNode();
if (container.contains(e.target) && container !== e.target) {
return;
}
toggle(e);
};
//....
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} ref={containerRef} />
</DropdownProvider>
);
};
export default Dropdown;
The toggling function is linked to both the document, and the button itself. So when you click the button, it fires once, then the event bubbles up to the document and fires again. Gotta be careful attaching event listeners to the entire document object. Add a line to stop the event propagation in your Dropdown.js file:
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
e.stopPropagation(); // this stops it bubbling up to the document and firing again
return props.toggle(e);
};

React component not updating from props

I have simplified the code to isolate and reproduce the issue, so it may not make sense in real implementations:
import React, { Component } from 'react'
const obj = {
objProp: true
};
export default class MyButtonContainer extends Component {
render() {
return (
<MyButton
onClick={() => {obj.objProp = !obj.objProp;}}
text={obj.objProp.toString()}
/>
);
}
}
class MyButton extends Component {
render() {
return (
<button
onClick={this.props.onClick}
>
{this.props.text}
</button>
)
}
}
You can see that obj.objProp is assigned into MyButton.props.text, and it's value gets toggled when you click on an instance of MyButton. The value of obj.objProp does change as expected, but MyButton doesn't update and rerender.
My question is why is MyButton is not updating, and what is the proper way to implement such logic?
In addition, if the solution is to push obj into MyButtonContainer.state, why MyButton would of update if I have used Redux, which injects data only into props without changing the state?
Thanks :)
What you need is a state, You should not use variable this way, it needs to be on state and changing that state asynchronously.
Change your button container to this.
export default class MyButtonContainer extends Component {
constructor() {
this.state = {
objProp: true
}
this.onclick = this.onclick.bind(this);
}
onclick() {
this.setState({ objProp: !this.state.objProp })
}
render() {
return (
<MyButton
onClick={() => { this.onclick() }}
text={this.state.objProp.toString()}
/>
);
}
}
Demo
Use state to hold your objProp
React will rerender when there is setstate is called, it won't get rerendered automatically.
export default class MyButtonContainer extends Component {
state = {
objProp: true
}
onclick = () => {
this.setState({ objProp: !this.state.objProp })
}
render() {
return (
<MyButton
onClick={() => { this.onclick() }}
text={this.state.objProp.toString()}
/>
);
}
}
}
Whenever there is something where you want the UI to change it should be either through its State or by props passed to it.
Both the given answers are right, if you want to re-render your component you must use this.setState. so there is two way to get your updated data in React Component.
1) put your object in to state and setState.
2) if you really dont want to use your object in state, you can do a workaround like take a variable i in your state and when assigning the data in your object just do this.setState({i+1}), so due to change in state will re-render your component although this is not good way to resolve it, because to re-render you must setState.
import React, { Component } from 'react'
constructor(props){
super(props);
this.state = {i:0}
}
const obj = {
objProp: true
};
export default class MyButtonContainer extends Component {
render() {
return (
<MyButton
onClick={() => {
obj.objProp = !obj.objProp ;
let {i} = this.state ;
i = i + 1;
this.setState(i)}}
text={obj.objProp.toString()}
/>
);
}
}
class MyButton extends Component {
render() {
return (
<button
onClick={this.props.onClick}
>
{this.props.text}
</button>
)
}
}

Sharing store change event between same hierarchical level child components

I am developing a simple React JS application for learning purpose. I just started learning React JS a few days ago. Now, I am having a problem with Flux Store. I need to share the change event across two child components on the same hierarchical level.
I have the parent component, called TodoComponent with the following definition
//Create
class TodoComponent extends React.Component{
constructor(props){
super(props)
}
render(){
return (
<div>
<div>
<ListComponent />
</div>
<AddItemComponent />
</div>
)
}
}
It has two child components called, ListComponent and the AddItemComponent. Moreover, I have a store with this definition.
import { EventEmitter } from 'events';
class DataStore extends EventEmitter{
constructor()
{
super();
this.todos = [
"Eat",
"Sleep",
"Die",
"Shower"
];
}
getAll(){
return this.todos;
}
addItem(newItem)
{
this.todos.push(newItem);
this.emit("change")
}
}
const dataStore = new DataStore;
export default dataStore;
It has a function for adding new item into the array and a function for fetching the array.
This is the ListComponent that is displaying the array of items from the DataStore flux store.
import React from 'react';
import TodoItem from './TodoItem';
import DataStore from './data.store';
class ListComponent extends React.Component{
constructor(props)
{
super(props)
this.state = { todos : DataStore.getAll() };
}
componentWillMount(){
DataStore.on('change', () => {
//do somethif
this.state = { todos : DataStore.getAll() };
})
}
render()
{
var deleteItem = (item) => {
this.deleteItem(item);
}
var editItem = (item) => {
this.editItem(item);
}
var addItem = (newItem) => {
this.addItem(newItem);
}
var todos = this.state.todos.map((item, index) => {
return (
<TodoItem item={item} addItem={addItem.bind(this)} deleteItem={deleteItem} editItem={editItem} />
)
});
return (
<ul>
{todos}
</ul>
)
}
deleteItem(item)
{
this.setState({ todos: this.state.todos.filter((listItem, index) => {
return listItem !== item;
}) });
}
editItem(item)
{
alert(item)
}
addItem(newItem)
{
DataStore.addItem(newItem);
}
}
module.exports = ListComponent;
It is updating the items in the change event of the DataStore store. But I am not calling the addItem function in the ListComponent. I am calling it in the AddItemComponent.
This is the definition of the AddItemComponent.
import React from 'react';
import DataStore from './data.store';
class AddItemComponent extends React.Component{
constructor(props)
{
super(props)
}
render()
{
return (
<form id="form-todo" onSubmit={this.addItem.bind(this)} action="post">
<input type='text' ref="newItem" />
<button>ADD</button>
</form>
);
}
addItem(e)
{
e.preventDefault();
DataStore.addItem(this.refs.newItem.value);
}
}
module.exports = AddItemComponent;
But when I trigger the addItem function in the AddItemComponent, the change event of the DataStore in the ListComponent is not triggered. Therefore, how can I synchronize the change event of Flux Store between two components exist on the same hierarchical level?
The solution I can think of is having the DataStore in the TodoComponent (parent component) and send the data and functions as props to the child component. I think, the code will become a bit messy in that way. Is that the only solution to do that?
Welcome to React! I recreated your example and your "change" event is firing in ListComponent, but to update the state in a component you should use this.setState(changes) rather than this.state = {changes}. Only use this.state = {} in the constructor to set the initial state. The setState method properly flows through the React lifecycle and causes the component to re-render using the new state. There is an official guide on React's state and lifecycle hooks here.

React with Redux Popup or Modal syntax and setup

I have a MonthContainer component that renders a MonthView component. The container passes data to the view and the view does the formatting and displays a table with link buttons corresponding to month and category.
What I want to do is when a link is clicked in MonthView, a pop up displays on the page with another set of data that is based on the year, category and month for the link that was clicked. I am using react-modal to accomplish this.
In my initial setup, MonthViewContainer was rendering MonthView and PopupContainer which in turn rendered PopupView. PopupContainer was passed a full list of transaction data (unfiltered) which it then passed to PopupView. When I clicked on a link in MonthView, it would set the displayModal flag to true and my PopupView component which was wrapped in react-modal would show up with the transactions filtered based on year, month, category. This worked fine except for my challenges with updating the parent component after saving and closing the modal. However, I don't like the idea of loading all my state into the PopupView and then filtering. Ideally, I would want to get the data when the PopView loads. I'm having trouble doing this.
I have several issues with my setup. Below is my code with comments in each section I'm having trouble with.
MonthViewContainer
import React from 'react';
import MonthView from './MonthView.js';
import { connect } from 'react-redux';
import { getTransactions } from '../actions/actions.js';
import TransactionsPopupContainer from './TransactionsPopupContainer.js';
MonthViewContainer Component
class MonthViewContainer extends React.Component {
constructor() {
super();
this.popupCategory;
this.popupMonth;
this.state = {
detailPopup : false
}
this.handleGetDetail = this.handleGetDetail.bind(this);
this.handleRefresh = this.handleRefresh.bind(this);
}
componentDidMount() {
this.props.getTransactions(2016);
// this.props.getTransactionsAll(2016);
}
render() {
return (
<div className="row">
<div className="col-md-12">
<MonthView
transactions={this.props.storeTransactions.transactions}
selectedYear={this.props.storeTransactions.selectedYear}
onHandleGetDetail={this.handleGetDetail}
/>
</div>
<div>
<PopupContainer
modalActive={this.state.detailPopup}
selectedYear={this.props.storeTransactions.selectedYear}
category={this.popupCategory}
month={this.popupMonth}
onRefresh={this.handleRefresh}
/>
</div>
</div>
)
}
handleRefresh() {
console.log("handle refresh entered")
this.props.getTransactions(this.props.storeTransactions.selectedYear);
}
handleGetDetail(year,category,month) {
this.popupCategory = category;
this.popupMonth = month;
this.setState({ detailPopup: true}, function () {});
}
}
const mapStateToProps = (state) => ({
storeTransactions: state.storeTransactions
});
export default connect(mapStateToProps, {getTransactions})(MonthViewContainer);
PopupContainer
import React from 'react';
import { connect } from 'react-redux';
import PopupView from './PopupView.js';
import { getPopupTransactions } from '../actions/actions.js';
import { saveTransactions } from '../actions/actions.js';
class PopupContainer extends React.Component {
constructor() {
super();
this.editedTrans = undefined;
this.handleSave = this.handleSave.bind(this);
}
componentWillUnmount(){
//when modalActive is true, I would like to get the popup data with params coming from props
//doing something like getPopupTransactions
//the problem is I can't do it here because the component is mounted when parent loads and
//is set to active/visible when a button is clicked on the parent
}
handleSave(transToSave) {
this.props.saveTransactions(transToSave);//use the action in redux store to save these transactions
//refresh the parent (MonthViewContainer/MonthView) after saving //not sure how to do this after closing the modal
//I would like the transactions that are saved after closing the modal to be reflected in the parent component
//what I attempted is to pass in a handler what will trigger set state and case the MonthViewContainer to rerender
this.props.handleRefresh();
}
render() {
return (
<PopupView
modalActive={this.props.modalActive}
transactions={this.props.storePopupTransactions.popuptransactions}
savePopupView={this.handleSave}
/>
);
}
}
const mapStateToProps = (state) => {
return {storePopupTransactions: state.storePopupTransactions//,
}
};
export default connect(mapStateToProps, {getPopupTransactions,saveTransactions})(PopupContainer);
Been working on it and it turns out that I was indeed able to call my parent component (MonthViewContainer) after calling closeModal and before setting the modalIsOpen to false for the react-modal component (PopupView in my case). Therefore, I was able to save the data from PopupView and then refresh my state in MonthViewContainer.

Call external Api on button click and then render a child component with the api result data

<!--Weather.js-->
<!-- begin snippet: js hide: false console: true babel: false -->
import React from 'react'
import MyApi from '../api/MyApi';
import InputCity from './InputCity'
import WeatherData from './WeatherData'
export default class Weather extends React.Component{
constructor(props){
super(props);
this.state = {
weather:[],
city: ''
}
}
makeRequest = (city) => {
MyApi.getWeather(city)
.then(function (res) {
this.setState(function () {
return{
weather:res
}
})
}.bind(this));
}
componentDidMount(){
this.makeRequest(this.state.city)
}
setCity = (mycity) =>{
this.setState(function () {
return{
city:mycity
}
})
}
render(){
const showWeatherData = this.state.weather;
return(
<div>
<InputCity setCity={this.setCity}/>
{showWeatherData && <WeatherData city={this.state.city}/>}
{console.log(this.state.weather)}
</div>
);
}
}
I have three components:
Weather
InputText
WeatherData
Now the InputText Component is rendered when the main Weather component is rendered, the InputText component contains a textfield and a button.
So now when i click the button need to call an openweathermap api and then display the result in WeatherData Component.
The WeatherData component must be rendered only after the button click.
How can i achieve this??
add some state to the Weather component, call it showWeatherData for example, set it to null in the beginning. Give it a value after you receive back the data from your api.
in your JSX inside Weather component, use the && to short circuit the WeatherData component (just a short way instead of using an if or a tertiary operator)
render(){
<InputText />
{ ShowWeatherData && <WeatherData /> }
}

Resources