I have my React app defined as such:
function App() {
const [clickedNode, setClickedNode] = useState({});
const [hoveredLink, setHoveredLink] = useState({});
const updateNode = useCallback((node) => { setClickedNode(node); });
return (
<Container fluid>
<Row>
<Col sm={8} >
<MemoGraph setClickedNode={updateNode} />
</Col>
<Col sm={4}>
<InfoSection node={clickedNode}
link={hoveredLink} />
</Col>
</Row>
</Container >
);
}
in MemoGraph, there is a callback in a graph vizualization that updates the ClickedNode state variable.
This is really just to rerender the InfoSection object, but it causes the whole component to rerender.
The MemoGraph reRendering is really expensive, and needs to stop. In DevTools, the App is said to rerender because of the Hooks changing, which I do not understand. Is there someone that can explain the best way to change parent state in a child component, but so only InfoSection renders?
Any help would be really appreciated!
Related
I have two different react components placed one after the other in my app named SearchBar and InfiniteScroller;
function App() {
const [searchTerm, setSearchTerm] = useState("");
return (
<div className="App">
<SNavbar></SNavbar>
<MainLogo></MainLogo>
<SearchBar search={setSearchTerm}></SearchBar>
<hr/>
<InfiniteScroller term={searchTerm}/>
<Footer/>
</div>
);
}
The search bar component has its own state where it updates a search term as its input is being edited and it calls the setSearch function of its parent when the button is clicked (the function is passed as a prop in the parent)
function SearchBar(props)
{
const [search,setSearch] = useState("");
return(
<Container className="Search-Bar">
<Row>
<Col>
<InputGroup >
<FormControl
placeholder="What are we making today?"
onChange={event => setSearch(event.target.value)}
/>
<Button onClick={() => props.search(search)}>
Go!
</Button>
</InputGroup>
</Col>
</Row>
</Container>)
}
The search term that is updated by the SearchBar component is passed onto the InfiniteScroller component as a property and is set as the searchTerm field in its state object.
class InfiniteScroller extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
items:[],
page:1,
hasMore:true,
searchTerm:props.term
};
}
render(){
return(
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchData}
hasMore={this.state.hasMore}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
<Row>
{this.state.items.map((i, index) => (
<Col key={index} lg="2" md="4" sm="6" xs="12">
<ImageCell className="ImageCell" link = {this.state.items[index].link}> - #{index}</ImageCell>
</Col>
))}
</Row>
</InfiniteScroll>
)
}
}
However when the setSearchTerm function of App.js is triggered by pressing the button on the SearchBar component, the InfiniteScroller does not seem to get updated. As the SearchTerm field of its state still comes up as "undefined" and the component itself does not re-render to represent the change in property.
I want the InfiniteScroller to completely re-render itself and make some API calls to populate itself with content, How can I achieve this?
So far I've tried adding in HTML tags that have the SearchTerm property in them to check if react skips re-rendering components that don't "use" any properties but that has not worked.
The props' change does not make the UI re-rendering but the states' change does.
It has 2 potential ways to fix have a proper UI re-rendering.
For the first one, you can add key attribute to your component that will help you do a trick for re-rendering whenever key gets changed
<InfiniteScroller term={searchTerm} key={searchTerm}/>
The second way, you can update your local states of that component by componentDidUpdate (useEffect in function-based components)
class InfiniteScroller extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
items:[],
page:1,
hasMore:true,
searchTerm:props.term
};
}
//update states according to props change
componentDidUpdate(prevProps) {
if(this.props.searchTerm !== prevProps.searchTerm) {
setState({ searchTerm: this.props.searchTerm })
}
}
render(){
return(
<InfiniteScroll
dataLength={this.state.items.length}
next={this.fetchData}
hasMore={this.state.hasMore}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
>
<Row>
{this.state.items.map((i, index) => (
<Col key={index} lg="2" md="4" sm="6" xs="12">
<ImageCell className="ImageCell" link = {this.state.items[index].link}> - #{index}</ImageCell>
</Col>
))}
</Row>
</InfiniteScroll>
)
}
}
On page load I'm getting the weather data for an api and then displaying it on sidebar then when you click on a city it shows the the city's weather in more detail. So basically I've been passing the data around from parent to child with props. I need to fill the detailed component with some initial data so I'm trying to send the first object in the data array to the child component through props and set it to state but when I try to render it is undefined and I'm not sure why.
It actually seems to be coming back undefined a couple times before setting it but when I try to render it on the page {weather.data.temp} I get 'Cannot read property 'temp' of undefined'.
Parent:
const fetchCity = async (city) => {
const res = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`);
return {
description: res.data.weather[0].description,
icon: res.data.weather[0].icon,
temp: res.data.main.temp,
city: res.data.name,
country: res.data.sys.country,
id: res.data.id,
};
};
function App() {
const [data, setData] = useState([]);
const [activeWeather, setActiveWeather] = useState([]);
useEffect(() => {
const fetchCities = async () => {
const citiesData = await Promise.all(["Ottawa", "Toronto", "Vancouver", "California", "London"].map(fetchCity)).catch((err) => {
console.log(err);
});
setData((prevState) => prevState.concat(citiesData));
};
fetchCities();
}, []);
const handleClick = (event) => {
const weather = JSON.parse(event.target.dataset.value);
setActiveWeather(weather);
};
return (
<div className="App">
<Header />
<Container>
<Row>
<Col>
<WeatherPanel data={data} handleClick={handleClick} />
</Col>
<Col>
<ActiveWeather activeWeather={activeWeather} data={data[0]} />
</Col>
</Row>
</Container>
</div>
);
}
export default App;
Child
import React, { useEffect, useState } from "react";
import { Container, Card } from "react-bootstrap";
const ActiveWeather = (props) => {
const [weather, setWeather] = useState();
useEffect(() => {
setWeather(props.data);
}, [props]);
console.log(weather);
return (
<Container>
<Card>
<Card.Header> </Card.Header>
{weather.temp}
</Card>
</Container>
);
};
export default ActiveWeather;
Other child
const WeatherPanel = (props) => {
return (
<div>
<Container fluid>
<Card style={{ boxShadow: "0 0 10px 2px lightgrey" }}>
<Card.Header> Favorite Location</Card.Header>
<ListGroup variant="flush">
<ListGroup.Item>
{props.data.map((item) => (
<ListGroup.Item key={item.id} data-value={JSON.stringify(item)} onClick={props.handleClick}>
<img src={`http://openweathermap.org/img/wn/${item.icon}#2x.png`} alt="Weather Icon" />
{item.city + ", " + item.country}
</ListGroup.Item>
))}
</ListGroup.Item>
</ListGroup>
</Card>
</Container>
</div>
);
};
export default WeatherPanel;
Issue
Calling setState does not update the state variable immediately. Why?
Solution
Check if weather is defined before accessing it.
Option 1: Use weather?.temp
Option 2: Use an if statement: if (!weather)
I think the issue is with your useEffect in the ActiveWeather component.
Because you are passing just props as the dependancy of your useEffect, any prop change will trigger it. So in your example activeWeather is probably triggering it, and you are then setting the state of weather to undefined because at mount stage, there isn't any data to pass in.
If you limit the dependence to only props.data. Then that useEffect will only run if something gets passed though the data prop. You can even use an if statement in there to double check that there is actually some data.
useEffect(() => {
if(props.data){
setWeather(props.data);
}
}, [props.data]);
In your parent component you are also passing in data[0] as the data prop. On mount, data is an empty array so if you say data[0] it will be undefined. Maybe wrap the rendering of your ActiveWeather component in an if to check if there is any data
<Col>
{ data.length > 0 && <ActiveWeather activeWeather={activeWeather} data={data[0]} /> }
</Col>
when i'm updating my state using setState from parent component my child component get rendering(because props getting change)
Parent component
addonsHandler =(addons) =>{
this.setState({addons:addons}, () => {
// console.log(this.state.addons);
});
};
render() {
return (
<div>
<Row>
<Col span={15} offset={2}>
<AntForm pickupHandler= {this.pickupHandler} dropHandler={this.dropHandler} addonsHandler={this.addonsHandler} ambulanceTypeHandler={this.ambulanceTypeHandler}/>
<Button type="primary" onClick={this.drop} >Drop</Button>
<Button type="primary" onClick={this.calculateRoute}>Direction</Button>
{/*<div id="map" style={{height: "600px"}}></div>*/}
<Map onRef={ref => (this.MapRef = ref)} />
</Col>
<Col span={6} offset={1}>
<BookingDetails addons={this.state.addons} price={this.addonObj} ambulaceType={this.state.AmbulanceType} VehiclePrice={this.ambulacneTypeObj} />
</Col>
</Row>
<Row>
<Col span={15} offset={2}>
</Col>
</Row>
</div>
);
}
so i want to stop rendering only Map component when addons state get change in parent component
so i used shouldComponentUpdate in Map component but it's not stoping rendering to component
shouldComponentUpdate(nextProps, nextState) {
return false;
}
shouldComponentUpdate() affect on parent component. If it returns true, parent component will be rerender.
So, I think you should move shouldComponentUpdate() into BookingDetails component instead.
I've written a container component in ReactJS and am passing in a prop to that component which is to be rendered as the 'main' content, like so:
class RegistrationContainer extends Component {
render() {
const MainContent = this.props.mainContent;
return (
<Row>
<Col offset="lg-3" lg={6}>
<MainContent />
</Col>
</Row>
);
}
}
export default RegistrationContainer;
And I'm passsing to it a mainContent prop like so:
import RegistrationContainer from './RegistrationContainer';
import RegistrationEntryView from './RegistrationEntryView';
class RegistrationCodeEntry extends Component {
render() {
return (
<RegistrationContainer mainContent={RegistrationEntryView} />
);
}
}
export default RegistrationCodeEntry;
My issue is that I would like RegistrationEntryView to have props, but can't seem to figure out how to define/pass in props on it. If I do the following I get an error:
class RegistrationCodeEntry extends Component {
render() {
const RegistrationView = <RegistrationEntryView someProp="blah" /> ;
return (
<RegistrationContainer mainContent={RegistrationView} />
);
}
}
export default RegistrationCodeEntry;
Error is as follows:
invariant.js?7313:42 Uncaught Error: Element type is invalid: expected
a string (for built-in components) or a class/function (for composite
components) but got: object. Check the render method of
RegistrationContainer.
Is this something that this.props.children could solve? I've been struggling to get my head around the concept of that, so any advice on where I'm going wrong would be appreciated.
You can solve this with this.props.children like this
class RegistrationCodeEntry extends Component {
render() {
return (
<RegistrationContainer>
// Render it as children
<RegistrationEntryView someProp="blah" />
</RegistrationContainer>
);
}
}
then in your container
class RegistrationContainer extends Component {
render() {
const MainContent = this.props.mainContent;
return (
<Row>
<Col offset="lg-3" lg={6}>
// Render the passed children
{this.props.children}
</Col>
</Row>
);
}
}
Your approach is correct. You just went wrong here:
<Row>
<Col offset="lg-3" lg={6}>
<MainContent />
</Col>
</Row>
Instead do this:
<Row>
<Col offset="lg-3" lg={6}>
{ MainContent }
</Col>
</Row>
I personally think, this approach is better than using children.
When you did this - const RegistrationView = <RegistrationEntryView someProp="blah" /> ; The component was already rendered and converted to appropriate format. Hence you cannot re-render it with <MainContent />.
So using {} is correct in this case.
Good Luck!
I am attempting to pass an argument, setting 'fooId' to a temporary state, allowing to me set the default filter for the user to all 'fooId's within a react-data-grid when they clicked on the row(a cell value used for filtering). I thought I could make a function to set the state and pass that through the filter. The backend and supporting Sagas, Reducer, ect are built to support passing data. I just need to figure out this change in default filtering to set the filter to slice 'filteredData'. Hope it makes sense...Please see applicable code below, thanks:
function FooBar({fooBar, filteredData, fooBarSearch, toDate, fromDate, searchText, foobar}) {
let filterData = fooBar.slice();
function onRowClickFoo(event) { let fooBarId = set.state.fooId return
selectRoute('bar/whee/' + fooId); }
return (
<div>
<Grid fluid>
<Row className={styles.fooPage}>
<Col xs={12} md={3}>
<AutoComplete
floatingLabelText="Search Foo"
filter={AutoComplete.caseInsensitiveFilter}
openOnFocus={true}
dataSource={foobar}
searchText={searchText}
dataSourceConfig={{text: 'fooId', value: 'fooId'}}
onUpdateInput={searchOnUpdateHandler}
onNewRequest={searchOnNewRequest}
maxSearchResults={8}
/>
</Col>
<Col xs={12} md={3}>
<DatePicker
onChange={fromDateOnChangeHandler}
floatingLabelText="Filter Start Date"
autoOk={true}
value={fromDate}
mode="landscape"
firstDayOfWeek={0}
shouldDisableDate={disableStartDays}
/>
</Col>
<Col xs={12} md={3}>
<DatePicker
onChange={toDateOnChangeHandler}
floatingLabelText="Filter End Date"
autoOk={true}
value={toDate}
mode="landscape"
firstDayOfWeek={0}
shouldDisableDate={disableEndDays}
/>
</Col>
<Col xs={12} md={3} className={styles.resetButton}>
<RaisedButton
label="Reset"
secondary={true}
onTouchTap={handleResetFilter}
/>
</Col>
</Row>
<Row>
<Col xs={12}>
<fooBarGrid className={styles.fooBarGrid}
columnHeaders={columnHeaders}
rows={filteredData}
enableRowSelect={true}
onRowSelect={onRowClickFoo}
/>
It's kind of hard to understand what you're going for, but ... I think I understand.
If you want to use component-local state, you'll have to either use a Component class, or use an HOC to store the state for you (the recompose package has withState, which I find very very useful).
example; vanilla React
class Foo extends React.Component {
state = {
filter: null
}
render() {
const { data } = this.props
const { filter } = this.state
const mData = data.filter((dataRow) => dataRow.id === filter)
return (
/* use mData */
)
}
}
using recompose
const Foo = ({ data, filter, setFilter }) => {
const mData = data.filter((dataRow) => dataRow.id === filter)
const onRowClick = (event) => setFilter(/* whatever you want to filter to */)
return (
/* use mData */
)
}
export default withState("filter", "setFilter", null)(Foo)