Using API data in multiple components to conditionally render - javascript

This is the same app/continuation of this previous question: Invalid hook call trying to make an axios get request in react
Currently, I'm trying to figure out the best way to conditionally render a component and pass values from my API calls to it. I'll try to explain the current set up of the app the best I can. Here is a link of the non-react version for some visuals: https://giovannimalcolm.github.io/weather-dashboard/
I plan to have a component for the page before any input is submitted. The component I'm working on now is for the current weather box that appears once a city is submitted. I will make a third component for the five forecast cards below the aforementioned.
Currently, in the Home component, I have a onClick on the search button to show the current weather component when clicked. I will probably change this to onSubmit and later add an autocomplete function to the search box for not only more precise results but to also prevent submissions of poor formatting or null submissions. I believe the weather will data will always return with something in this case. The Home component is shown below
import React, { Component } from 'react';
import {Weather} from '../components/TodaysWeather'
import { getWeatherData } from '../service/getWeather';
import { GetWeatherUrl } from '../service/getWeatherUrl';
export class Home extends Component {
constructor(props){
super(props);
this.state = {
location: "",
showWeather: false
};
this.locationChange = this.locationChange.bind(this);
}
onSubmit(e) {
e.preventDefault();
}
locationChange(e){
this.setState({
location: e.target.value
});
}
_showWeather = async (bool) => {
this.setState({
showWeather: bool
});
await getWeatherData(this.state.location)
console.log(await GetWeatherUrl(this.state.location))
}
componentDidUpdate(){
console.log(this.state)
}
render() {
return (
<div>
<header className="main-header">
<h1>Weather Dashboard</h1>
</header>
<div className="container-fluid" style={{ maxWidth: '1400px' }}>
<div className="row">
<aside className="col-lg-3 pb-3">
<h2 id="sidebar-title">Search for a City:</h2>
<form onSubmit={e => this.onSubmit(e)} id="citySearch">
<div className="input-group">
<input
className="form-control"
type="text"
placeholder="City Here"
id="city-input"
onChange={this.locationChange }
/>
<div className="input-group-append"></div>
</div>
<button
type="submit"
className="btn btn-primary btn-block"
id="sidebar-btn"
onClick={this._showWeather.bind(null,true)}
>
Search
</button>
</form>
<div id="history"></div>
</aside>
</div>
</div>
</div>
)
}
}
I have API calls to get the weather data in a separate "service" folder. These are called in the Home component in the _showweather function. What I need help with is figuring out the best way to capture the data from the API call in Home and send it over to the TodaysWeather component (and later the Forecast component) so it can be used for conditionally rendering via states and rendering in the virtual DOM.
I've considered doing the API call in the TodaysWeather component as shown below but this won't work as I need the data to be pulled before any rendering.
import { GetWeatherUrl } from "../service/getWeatherUrl";
import Axios from 'axios';
import React, { Component } from 'react';
export class Weather extends Component {
state = {
loading: true,
weather: []
}
async componentDidMount(){
const res = await Axios.get(GetWeatherUrl());
this.setState({weather: res.data, loading: false})
console.log(this.state.weather);
}
render(){
return(
<div>
<div className="col-lg-9 pb-3">
<section id="presentDay" className="todaysWeather">
<div className="todaysWeather-body">
<h2 className="h3 today-title"> San Diego <img className="weather-img" src="https://openweathermap.org/img/w/03d.png" alt="scattered clouds" /></h2>
<p className="today-txt">Temp: </p>
<p className="today-txt">Wind: 11.5 MPH</p>
<p className="today-txt">Humidity: 61 %</p>
<p>UV Index: <button className="uvi-btn wary-uvi">3.1</button>
</p></div>
</section>
</div>
</div>
)
}
}
Is there a better way to set this all up? Please ignore the strings in the render section, it only was there for debugging.

Related

React Custom Modal Component Problems With Updating Same Component Used Repeatedly

I built a custom Modal.
There is one particular function I would like it to do when opened. I would like a CSS class to be toggled when this modal is opened/closed.
This works just fine if I only insert this component once in a template. But in my case I am inserting it three times. By using the componentDidMount I insert some JS that should toggle the CSS class. It does not do it for the first or the second modal, it will only do it for the third.
CODE UPDATED!
This is the parent component:
import React from "react";
import ModalSmall from "./ModalSmall";
import ModalMedium from "./ModalMedium";
import ModalLarge from "./ModalLarge";
import "bootstrap/dist/css/bootstrap.css";
import "./styles.scss";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isModalSmallOpen: false,
isModalMediumOpen: false,
isModalLargeOpen: false
};
}
toggleModalSmall = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalSmallOpen: !prev.isModalSmallOpen
}));
};
toggleModalMedium = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalMediumOpen: !prev.isModalMediumOpen
}));
};
toggleModalLarge = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalLargeOpen: !prev.isModalLargeOpen
}));
};
render() {
return (
<div className="container">
<div className="row">
<div className="col">
<h1>Hello Y'all!</h1>
<p className="yo-green">My Modal Samples</p>
<div className="row mt-5">
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalSmall}
>
Modal Small
</button>
</div>
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalMedium}
>
Modal Medium
</button>
</div>
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalLarge}
>
Modal Large
</button>
</div>
</div>
</div>
</div>
<ModalSmall
modalName="smallModal"
modalTitle="Small Modal"
modalBody="This is the small modal!"
toggleModal={this.toggleModalSmall}
modalOpen={this.state.isModalSmallOpen}
/>
<ModalMedium
modalName="mediumModal"
modalTitle="Medium Modal"
modalBody="This is the medium modal!"
toggleModal={this.toggleModalMedium}
modalOpen={this.state.isModalMediumOpen}
/>
<ModalLarge
modalName="largeModal"
modalTitle="Large Modal"
modalBody="This is the LARGE modal!"
toggleModal={this.toggleModalLarge}
modalOpen={this.state.isModalLargeOpen}
/>
</div>
);
}
}
One of the in-between components:
import React from "react";
import Modal from "./Modal";
const ModalSmall = (props) => {
return (
<Modal
modalName={props.modalName}
modalTitle={props.modalTitle}
modalBody={props.modalBody}
toggleModal={props.toggleModal}
modalOpen={props.modalOpen}
/>
);
};
export default ModalSmall;
Here is my modal Component:
import React from "react";
export default class Modal extends React.Component {
componentDidUpdate() {
if (this.props.modalOpen) {
console.log("Open!", this.props.modalOpen);
document.body.classList.add("drawer-open");
} else {
console.log("Closed!", this.props.modalOpen);
document.body.classList.remove("drawer-open");
}
}
render() {
return (
<div className="mymodal" id={this.props.modalName}>
<div
onClick={this.props.toggleModal}
className={`mymodal-overlay ${this.props.modalOpen && "active"}`}
></div>
<div
className={`mymodal-content d-flex flex-column ${
this.props.modalOpen && "active"
}`}
>
<header className="p-2 border-bottom d-flex">
<span
className="material-icons clickable"
onClick={this.props.toggleModal}
>
close
</span>
<div className="flex-grow-1 ml-2">{this.props.modalTitle}</div>
</header>
<div className="p-2 flex-grow-1">{this.props.modalBody}</div>
<footer className="p-2 border-top">© ChidoPrime 2021</footer>
</div>
</div>
);
}
}
Working Sample Here with Solution Applied
UPDATE! -------------
There is a second approach I would like to include, different than the checked answer offered by #sanishJoseph. In which I add a constructor and declare a state within the modal controller. Without the need of using React.PureComponent. I use preProvs within the componentDidUpdate. Code for the modal follows:
constructor(props) {
super(props);
this.state = {
modalOpen: false
};
}
componentDidUpdate(prevProps) {
if (prevProps.modalOpen === this.props.modalOpen) return;
if (this.props.modalOpen) {
console.log("Open!", this.props.modalOpen);
document.body.classList.add("drawer-open");
} else {
console.log("Closed!", this.props.modalOpen);
document.body.classList.remove("drawer-open");
}
}
Second Sample using prevProps without using React.PureComponent
I think the biggest mistake is in your Parent component. Your initial state of the page is
this.state = {
isModalSmallOpen: false,
isModalMediumOpen: false,
isModalLargeOpen: false
}
But, when you open a Modal, you are setting your state to one item in the state, rest of the items are going null. Meaning, when you do
this.setState({
isModalSmallOpen: !this.state.isModalSmallOpen
})
You are setting isModalMediumOpen: null, isModalLargeOpen: null.
What you should be doing is,
this.setState((prev) => ({...prev,
isModalSmallOpen: !prev.isModalSmallOpen
}))
So all of your states will remain in your state. This change is needed in all the 3 modal opening functions.
Update :
Fix is petty easy. All you need to do is add a react.memo if it was a functional component. In your case make your Modal component as a PureComponent.
export default class Modal extends React.PureComponent
Pure Components in React are the components which do not re-renders
when the value of state and props has been updated with the same
values.
https://codesandbox.io/s/my-custom-modal-forked-yg4vo?file=/src/App.js
The code is a little complex to understand, but I think the main problem is with the logic used to implement it. If I understood correctly you are using the same Component more than once. So, each component executes componentDidUpdate method each time that is rerendered.
What this means is that if you are toggling one of your modals in the "parent" component with the methods "toggleModal..." then, the parent render method is executed and it executes each render children method. What happened there is that with your first modal you are adding o removing the body css, with the second you are doing the inverse and with the third, you are adding and removing again.
You have a lot of things to get better there, but the most simple is use the arguments you got in your componentDidUpdated method and make sure you only executed your code if the new props changes. This going to solve your problem.

ReactJS Component not displaying

I'am very new in the programming world and React (using the COVID-19 time to get better...). I'm trying to render a component when the user is clicking a register button. My goal is to display it as a pop-up in the middle of the screen for the user to fill a form. (I'm using Visual studio code and react app generator)
I can't make it happen, if I console.log the result true/false ( depending on a condition) it works correctly so I guess the problem is the way I " call" the component.
If anyone could point toward the good direction I would glady appreciate !
The App class where the handler function is calling the supposed popup div
import React from "react"
import Header from "./UI/Header";import RegisterWindow from "./UI/RegisterWindow"; import Footer from "./UI/Footer"; import MainSection from "./UI/MainSection";
import "./index.css"
class App extends React.Component{
constructor(){
super()
this.state ={
registerIsShowed: false
}
this.handleRegister = this.handleRegister.bind(this)
}
handleRegister(){
this.setState({
registerIsShowed: !this.state.registerIsShowed
})
const isShowed = this.state.registerIsShowed;
return isShowed ? <RegisterWindow /> : null
}
render(){
return (
<div>
<Header register={this.handleRegister} />
<MainSection />
</div>
)}
}
export default App
This is the Header code where the button that triggers the opening is located
import React from "react"
function Header(props) {
return (
<header>
<nav className="navbar-header">
<p className="header-data"></p>
<ul className="navbar-menu-header">
<li><button onClick={props.register}>Registrar</button></li>
<li><button>Entrar</button></li>
</ul>
</nav>
</header>
)
}
export default Header
and finally the Component that is supposed to show up
import React from "react"
class RegisterWindow extends React.Component{
render(){
return (
<div className="register-window">
<div>
<form>
<input name="firstName" placeholder="First name" type="text" />First Name
<input name="lasttName" placeholder="Last name" type="text" />Last Name
</form>
</div>
</div>
)
}
}
export default RegisterWindow
thank you,
The RegisterWindow component must be included in the Render lifecycle function in a class component, or within a return statement of a functional component. Your App component is class based so it must contain a render() method.
Setting the state is asynchronous, so even if you could render the component from the handleRegister() callback in a class component, the state update wouldn't be immediate so your synchronous logic to display the RegisterWindow component would fail.
Try something like this:
handleRegister() {
this.setState({
registerIsShowed: !this.state.registerIsShowed
});
}
render() {
return (
<>
{this.state.registerIsShowed && <RegisterWindow />}
<div>
<Header register={this.handleRegister} />
<MainSection />
</div>
</>
)
}
this.state.registerIsShowed && <RegisterWindow /> is an example of Conditional Rendering.
To make the RegisterWindow appear floating above the MainSection, you can style it with an absolute position.

Alter React parent component state

I am trying to create a simple SPA (without Router). It has also a simple structure: a component per section:
Home
Services
Products
Product
Modal
Contact us
As you can see the component Products has two sub-components Product and Modal. These are iterated so many times as JSON objects there are:
Products.js
import React, { Component } from "react";
import ReactHtmlParser from "react-html-parser";
import "./Products.css";
import { products } from "./products.json";
import Product from "./Product/Product";
import Modal from "./Modal/Modal";
class Products extends Component {
render() {
return (
<section id='products'>
<div className='container'>
<div className='row'>
{products.map(product => {
return (
<div>
<Product
image={"/img/" + product.image}
name={product.name}
target={product.target}
/>
<Modal
id={product.target}
title={product.name}
body={ReactHtmlParser(product.body)}
/>
</div>
);
})}
</div>
</div>
</section>
);
}
}
export default Products;
Each product has a More Info button what opens the modal and this has another button Budget ("Presupuestar"):
That function should "change the state" of Contact us component (a simple contact us form):
The component has the following code:
Contact.js
import React, { Component } from "react";
import "./Contact.css";
class Contact extends Component {
constructor() {
super();
this.state = { budget: "Contact" };
}
render() {
return (
<section id='contact'>
<div className='container'>
<div className='row'>
<div className='col-xs-12 col-md-6'>
<div className='contact-form'>
<form>
...
{/* Subject */}
<div className='form-group'>
<div className='input-group'>
<span className='input-group-addon' />
<input
type='text'
className='form-control'
id='subject'
aria-describedby='Subject'
placeholder='Subject'
readonly='readonly'
value={this.state.budget}
/>
</div>
{/* /form-group */}
</div>
{/* /Subject */}
...
</form>
</div>
</div>
</div>
</div>
</section>
);
}
}
I guess then I should create a function in the Modal component to trigger with an onClick="setSubject" in the Budget ("Presupuestar") button. What I don't know is how to alter the other component's state.
A quick summary: I have to make the following state update:
I was reading this similar question but I didn't get how to apply in my scenario. Any ideas?
I think you should either but the clickHandler function of the button in the App component that wrap the whole components and then pass it to the Products component then to Modal component but it's not a good practice,
Or you can use Redux a state management system that let you control your state through the whole app.
First of all, you don't need a function to change the state of another component. The smart way to do that is using an intermediary thing to connect 2 component together. There is two way to solve this problem.
The easiest way is you can transfer subject via URL (URL is "the intermediary thing"). When you click the button Presupuestar you can change URL to page contact like this:
/contact?subject=whatever you want
Then, at Contact component, you just need to parse URL to get subject (you can see this question to know how to parse from URL). You can see my example.
The second way is creating a service use singleton pattern to transfer subject from Modal to Contact form. You can see my example.
You can achieve this like this
Create a main app component which will contain all these these three comps
Add a function in app component "changeContacts"
Send it to both the product as well as contacts
Here is an explanation
class App extends Component {
render() {
return (
<div>
<Contact ref="contacts"/>
<Products changeContacts={this.changeContacts} />
</div>
);
}
changeContacts = (newState) => {
this.refs.contacts.changeState(newState)
};
}
class Contact extends Component {
state = { text:"Old Text" }
render() {
return ( <div style={{fontSize:50,backgroundColor:'red'}}>{this.state.text}</div> );
}
changeState = (newState) =>{
this.setState(newState);
}
}
class Modal extends Component {
render() {
return ( <div onClick={() => this.props.onClick({text:"New State Text"})}>This is a modal</div> );
}
}
class Products extends Component {
state = { }
render() {
return ( <div>
<h1>Products List</h1>
<Modal onClick={this.props.changeContacts} />
<Modal onClick={this.props.changeContacts}/>
<Modal onClick={this.props.changeContacts}/>
</div> );
}
}

Reactjs binding issue

How to request and receive data from signalR and bind data in ComponentDidMount in ReactJs
I am using react framework to design a view with SignalR. I am able to connect to SignalR server and get data but if I required data to load on view appear I am not able to load data on ComponentDidMount.
If I request data on ComponentDidMount view is displaying blank.
On view load, I mentioned an icon (i) and clicking after view load then the data is binding.
import React, { Component } from 'react';
import { Button, Card, CardBody, CardFooter, CardHeader, Col, Row, Collapse, Fade } from 'reactstrap';
import {Redirect} from 'react-router-dom';
import {hub} from '../../SignalRHub';
class Users extends Component {
constructor(props) {
super(props)
this.state = {
userId:0,
userName:'',
usersList:[]
};
}
LoadUsersList()
{
this.refs.child.userListRequest();
}
receiveUserList(userlist)
{
this.setState({usersList: userlist});
}
render(){
if(this.state.redirecttoUsers)
{
const route='/UsersInfo/'+this.state.userId;
return (<Redirect to={route} />)
}
return (
<form onSubmit={this.handleSubmit}>
<div className="animated fadeIn">
<SignalRHub ref="child" receiveUserList={this.receiveUserList.bind(this)} />
</i>
<Row>
{this.state.usersList.map((e, key) => {
return (
<Col className="col-3 .col-sm-3">
<div class="container">
<Card className="border-primary crd" onClick={(event)=>this.userFingerInfoHandle(event,e)}>
<CardHeader>
{e.userName}
<div className="card-header-actions middle upper">
</i>
<a className="card-header-action img btn btn-setting" onClick={(event)=>this.userRemovalHandle(event,e.id)}><i className="fa fa-trash"></i></a>
</div>
</CardHeader>
<CardBody align="center">
{e.userName}
</CardBody>
</Card>
</div>
</Col>
)})
}
</Row>
</div>
</form>
);
};
}
export default Users;
update state in componentDidMount and reference all necessary data in render using this.state
The reason is simple.
React decide to update the DOM tree when either the props or state is changed by default, so only by changing either of them can you update the rendering unless you implement your own shouldComponentUpdate

Filtering List in React

I am creating a basic blog in react using Flux + React Router + Firebase. I am having trouble trying to get a single blog post to render. When I click on the link to a single post, I try to filter out all of the other posts from a list of all posts and display only a single post from my firebase database.
I attempt to do this by matching the key of the firebase entry with the url params like so if (this.props.routeParams.key===key) . I really do not know what I have to do to make this happen. Any suggestions are welcome.
Below is Blogger.jsx, the page where I allow a user to create a blog post and then beneath the blog post, I display a list of the titles all blog posts.
import AltContainer from 'alt-container';
import React from 'react';
import { Link } from 'react-router';
import List from './List.jsx'
import Firebase from 'firebase'
import BlogStore from '../stores/BlogStore'
import BlogActions from '../actions/BlogActions';
const rootURL = 'https://incandescent-fire-6143.firebaseio.com/';
export default class Blogger extends React.Component {
constructor(props) {
super(props);
BlogStore.getState();
BlogStore.mountFirebase();
{console.log(this.props.location.query)}
};
componentDidMount() {
BlogStore.listen((state) => {
this.setState(state)
})
this.firebaseRef = new Firebase(rootURL + 'items/');
}
componentWillMount() {
BlogStore.unlisten((state) => {
this.setState(state)
})
}
renderList = (key) => {
return (
<Link to={`blogshow/${key}`}> <List key={key} blog={this.state.blog[key]} /> </Link>
)
}
handleInputChange = () => {
BlogStore.setState({
title: this.refs.title.value,
text: this.refs.text.value});
}
handleClick = () => {
BlogStore.handleClick();
}
render() {
return (
<div>
<div className="row panel panel-default">
<div className="col-md-8 col-md-offset-2">
<h2>
Create a New Blog Post
</h2>
</div>
</div>
<h2>Blog Title</h2>
<div className="input-group">
<input
ref="title"
value={BlogStore.state.title}
onChange = {this.handleInputChange}
type="text"
className="form-control"/>
<span className="input-group-btn">
</span>
</div>
<h2>Blog Entry</h2>
<div className="input-group">
<textarea
ref="text"
value={BlogStore.state.text}
onChange = {this.handleInputChange}
type="text"
className="form-control"/>
</div>
<div className="blog-submit input-group-btn">
<button onClick={this.handleClick}
className="btn btn-default" type="button">
Publish Blog Post
</button>
</div>
{/*<List blog={this.state.blog} />*/}
{Object.keys(BlogStore.state.blog)
.map(this.renderList)}
</div>
);
}
}
When a user clicks on a link to a single blog post, they should be transported to a page which shows only that single blog post. I have called this component BlogShow. I can't get BlogShow to render because I keep on getting the error
invariant.js?4599:45 Uncaught Invariant Violation: BlogShow.render(): A
valid React element (or null) must be returned. You may have returned
undefined, an array or some other invalid object.
This is BlogShow.jsx:
import AltContainer from 'alt-container';
import React from 'react';
import { Link } from 'react-router';
import Blogger from './Blogger'
import List from './List'
const rootURL = 'https://incandescent-fire-6143.firebaseio.com/';
import BlogStore from '../stores/BlogStore'
import BlogActions from '../actions/BlogActions';
export default class BlogShow extends React.Component {
constructor(props) {
super(props);
{console.log(this.props.routeParams.key)}
this.filterList = this.filterList.bind(this);
}
filterList(key) {
if (this.props.routeParams.key===key) {
return (<List key={key} blog={BlogStore.state.blog[key]} />)
}
}
render() {
<div> {Object.keys(BlogStore.state.blog).map(this.filterList)} </div>
}
}
You are getting that error because your Component BlogShow is not returning anything.
render() {
<div> {Object.keys(BlogStore.state.blog).map(this.filterList)} </div>
}
Should be:
render() {
return <div> {Object.keys(BlogStore.state.blog).map(this.filterList)} </div>
}
I'm not familiar with React.js at all, but I am familiar with pure JS arrays. To remove elements from an array, you should use .filter(), and then afterwards you can map the items.
Something like this:
filterList(key) {
return this.props.routeParams.key === key; // true if the item should stay in the list
}
mapList(key) {
return <List key={key} blog={BlogStore.state.blog[key]} />;
}
render() {
return <div> {Object.keys(BlogStore.state.blog).filter(this.filterList).map(this.mapList)} </div>;
}

Categories