React with Redux Popup or Modal syntax and setup - reactjs

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.

Related

React-toastify notification won't return in conditional

The notification works quite easily with a button. However, I'm trying to have it activate when a props passes through (true/false).
Basically, the user clicks on this tab, if they're not signed in, it'll pop up with the notification telling them to sign in.
However, I cannot make it work without being a button. The props passess through just fine, and I can console.log it. And the conditional returns... something, but it's like an odd bunch of letters, and each refresh it changes. And does not pop up like a notification. It's just obscure letters in the middle of the screen (because of {notify} placed above the form).
import React, {Component} from 'react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
class Alerts extends Component {
constructor(props){
super(props)
this.state = {
alertLogin: ''
}
}
render() {
const notify = () => toast("Please login before adding a recipe!");
// tried to make a conditional to check if prop alertLogin is true or false
// then calls notify function if false
if (!this.props.alertLogin) {
console.log('alert props received', this.props.alertLogin)
return notify()
}
return (
<div>
{/* <button onClick={notify}>Notify !</button> */}
{notify}
<ToastContainer />
</div>
);
}
}
export default Alerts;
import React, { Component } from "react";
import "./styles.css";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
class Alerts extends Component {
constructor(props){
super(props)
this.state = {
alertLogin: ''
}
}
componentDidMount() {
// tried to make a conditional to check if prop alertLogin is true or false
// then calls notify function if false
if (!this.props.alertLogin) {
console.log("alert props received", this.props.alertLogin);
this.notify();
}
}
notify = () => toast("Please login before adding a recipe!");
render() {
return (
<div>
<button onClick={(e) => this.notify()}>Notify !</button>
<ToastContainer />
</div>
);
}
}
export default Alerts;
Codepen for the solution
First you take the if statement with the function and put it in componentDidMount
cause i'm guessing is stopping the rendered elements themselves from rendering
second make the toast function accessible by component did mount and the button by declaring it before the render function hope i was clear enough

How to prevent parent component from re-rendering with React (next.js) SSR two-pass rendering?

So I have a SSR app using Next.js. I am using a 3rd party component that utilizes WEB API so it needs to be loaded on the client and not the server. I am doing this with 'two-pass' rendering which I read about here: https://itnext.io/tips-for-server-side-rendering-with-react-e42b1b7acd57
I'm trying to figure out why when 'ssrDone' state changes in the next.js page state the entire <Layout> component unnecessarily re-renders which includes the page's Header, Footer, etc.
I've read about React.memo() as well as leveraging shouldComponentUpdate() but I can't seem to prevent it from re-rendering the <Layout> component.
My console.log message for the <Layout> fires twice but the <ThirdPartyComponent> console message fires once as expected. Is this an issue or is React smart enough to not actually update the DOM so I shouldn't even worry about this. It seems silly to have it re-render my page header and footer for no reason.
In the console, the output is:
Layout rendered
Layout rendered
3rd party component rendered
index.js (next.js page)
import React from "react";
import Layout from "../components/Layout";
import ThirdPartyComponent from "../components/ThirdPartyComponent";
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
ssrDone: false
};
}
componentDidMount() {
this.setState({ ssrDone: true });
}
render() {
return (
<Layout>
{this.state.ssrDone ? <ThirdPartyComponent /> : <div> ...loading</div>}
</Layout>
);
}
}
export default Home;
ThirdPartyComponent.jsx
import React from "react";
export default function ThirdPartyComponent() {
console.log("3rd party component rendered");
return <div>3rd Party Component</div>;
}
Layout.jsx
import React from "react";
export default function Layout({ children }) {
return (
<div>
{console.log("Layout rendered")}
NavBar here
<div>Header</div>
{children}
<div>Footer</div>
</div>
);
}
What you could do, is define a new <ClientSideOnlyRenderer /> component, that would look like this:
const ClientSideOnlyRenderer = memo(function ClientSideOnlyRenderer({
initialSsrDone = false,
renderDone,
renderLoading,
}) {
const [ssrDone, setSsrDone] = useState(initialSsrDone);
useEffect(
function afterMount() {
setSsrDone(true);
},
[],
);
if (!ssrDone) {
return renderLoading();
}
return renderDone();
});
And you could use it like this:
class Home extends React.Component {
static async getInitialProps({ req }) {
return {
isServer: !!req,
};
};
renderDone() {
return (
<ThirdPartyComponent />
);
}
renderLoading() {
return (<div>Loading...</div>);
}
render() {
const { isServer } = this.props;
return (
<Layout>
<ClientSideOnlyRenderer
initialSsrDone={!isServer}
renderDone={this.renderDone}
renderLoading={this.renderLoading}
/>
</Layout>
);
}
}
This way, only the ClientSideOnlyRenderer component gets re-rendered after initial mount. 👍
The Layout component re-renders because its children prop changed. First it was <div> ...loading</div> (when ssrDone = false) then <ThirdPartyComponent /> (when ssrDone = true)
I had a similar issue recently, what you can do is to use redux to store the state that is causing the re-render of the component.
Then with useSelector and shallowEqual you can use it and change its value without having to re-render the component.
Here is an example
import styles from "./HamburgerButton.module.css";
import { useSelector, shallowEqual } from "react-redux";
const selectLayouts = (state) => state.allLayouts.layouts[1];
export default function HamburgerButton({ toggleNav }) {
let state = useSelector(selectLayouts, shallowEqual);
let navIsActive = state.active;
console.log("navIsActive", navIsActive); // true or false
const getBtnStyle = () => {
if (navIsActive) return styles["hamBtn-active"];
else return styles["hamBtn"];
};
return (
<div
id={styles["hamBtn"]}
className={getBtnStyle()}
onClick={toggleNav}
>
<div className={styles["stick"]}></div>
</div>
);
}
This is an animated button component that toggles a sidebar, all wrapped inside a header component (parent)
Before i was storing the sidebar state in the header, and on its change all the header has to re-render causing problems in the button animation.
Instead i needed all my header, the button state and the sidebar to stay persistent during the navigation, and to be able to interact with them without any re-render.
I guess now the state is not in the component anymore but "above" it, so next doesn't start a re-render. (i can be wrong about this part but it looks like it)
Note that toggleNav is defined in header and passed as prop because i needed to use it in other components as well. Here is what it looks like:
const toggleNav = () => {
dispatch(toggleLayout({ id: "nav", fn: "toggle" }));
}; //toggleLayout is my redux action
I'm using an id and fn because all my layouts are stored inside an array in redux, but you can use any logic or solution for this part.

How to make an API request In React on button click

I'm trying to build a random quote generator that loads A quote on componentDidMount with an axios api request , then loads new quotes on button click.
This is for A freecodecamp project. I have tried making the call again on button click, then adding the new response to state, but it will not work at all.
import React, { Component } from 'react'
import Button from './Button';
import axios from 'axios'
class QuoteBox extends Component{
constructor(props){
super(props)
this.state = {
quotes: []
}
}
componentDidMount(){
axios.get('http://quotesondesign.com/wp-json/posts?
filter[orderby]=rand&filter[posts_per_page]=1')
.then(res=> this.setState({quotes: res.data[0]}))
}
getNext = (ev) =>{
ev.preventDefault()
axios.get('http://quotesondesign.com/wp-json/posts?
filter[orderby]=rand&filter[posts_per_page]=2')
.then(res=> this.setState({quotes:[...this.state,res.data[0]]}))
}
render(){
const {content,title} = this.state.quotes
const filteredContent = String(content).replace(/(<\w>)|(<\/\w>)|
(&#\d{4})/gm, "").replace(/(;)/g,"'")
console.log(content)
return(
<React.Fragment>
<h2>A little inspiration for the day</h2>
<div className='outerQuoteBox'>
<div className='innerQuoteBox'>
<p>{filteredContent}</p><br/><br/>{title}
</div>
<Button getNext={this.getNext} />
</div>
</React.Fragment>)
}
}
export default QuoteBox
And this is my button component
import React, { Component } from 'react'
export class Button extends Component {
render() {
return (
<React.Fragment>
<button onClick={this.props.getNext} className='nextBtn'
type='button'>Next</button>
</React.Fragment>
)
}
}
export default Button
When I click the button, it seems like the request isn't going through at all. If i check State in the dev tools, only the first quote from componentDidMount is in the array. I don't understand where my mistake is.
Edit: I had used the wrong prop reference, so it wasn't making the call. I fixed this and it does make the call now, and it brings in one new quote, but that's it. And it doesn't add the new one to state, it just replaces it with the one new one. and that's all it will do. The api instructions say the end point i'm using should return a new random quote, but it does not.
It looks like you're referencing the wrong prop on the button.
Change getQuote to getNext and it should work...
import React, { Component } from 'react'
export class Button extends Component {
render() {
return (
<React.Fragment>
<button onClick={this.props.getNext} className='nextBtn'
type='button'>Next</button>
</React.Fragment>
)
}
}
export default Button

Reactjs refresh parent after react modal popup changes data

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.

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