A better way to write this React Class Component with Hooks? - javascript

I have a section with a fixed height. I don't know when the component mounts (first renders) whether the content coming in will fit or not. If it does NOT fit, then I need to render a 'Read More' button.
It looks like this:
I wrote this originally as a Class component using the lifecycle methods DidMount/DidUpdate:
Class Component
import React, { createRef } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
class ParagraphList extends React.Component {
state = {
overflowActive: false,
}
wrapper = createRef() // so we can get a ref to the height container
isOverflowing(el) {
if (el) return el.offsetHeight < el.scrollHeight
}
componentDidMount() {
this.setState({ overflowActive: this.isOverflowing(this.wrapper.current) })
}
componentDidUpdate() {
if (this.wrapper.current && !this.state.overflowActive) {
this.setState({
overflowActive: this.isOverflowing(this.wrapper.current),
})
}
}
handleClick() {
this.setState({ overflowActive: false })
}
render() {
const { moreButtonText, titleText, paragraphs, theme } = this.props
return (
<>
<Section overflowActive={this.state.overflowActive}>
{this.state.overflowActive || !this.wrapper.current ? (
<StyledHeightContainer ref={this.wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(this)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
}
export default ParagraphList
My best way to explain the flow:
When the component mounts, the flag is false and we have no reference to the div so the StyledHeightContainer will try to render and thus provide a ref to it
In componentDidMount -> try to set the overflow flag (which will be false because at this point we do not yet have rendering completed so the ref will be null). But by setting the flag anyway, we queue an additional render pass
1st INITIAL rendering completes -> we have a ref to the div now
The 2nd (queued) render occurs, firing the componentDidUpdate -> we can calculate the overflow and set the flag to true when the content overflows
When the user clicks the button -> set the flag to false, which will trigger a re-render and hence the StyledHeightContainer will be removed from the DOM.
Functional Component With Hooks
Sandbox of the code
When I re-wrote this as a functional component using Hooks, I ended up with this:
import React, { createRef, useEffect, useState } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
const ParagraphList = ({ moreButtonText, titleText, paragraphs, theme }) => {
const [overflowActive, setOverflowActive] = useState(false)
const [userClicked, setUserClicked] = useState(false)
const wrapper = createRef(false) // so we can get a ref to the height container
const isOverflowing = el => {
if (el) return el.offsetHeight < el.scrollHeight
}
useEffect(() => {
if (!userClicked && !overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
}, [userClicked]) // note: we only care about state change if user clicks 'Read More' button
const handleClick = () => {
setOverflowActive(false)
setUserClicked(true)
}
return (
<>
<Section theme={theme} overflowActive={overflowActive}>
{!userClicked && (overflowActive || !wrapper.current) ? (
<StyledHeightContainer ref={wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(null)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
export default ParagraphList
I was surprised that I needed to add another state (userClicked), which is how I force the 2nd render to occur (ie. the equivalent to the componentDidUpdate in the class solution).
Is this correct or can someone see a more concise way to write the 2nd solution?
NOTE
One of the reasons I ask is because in the console I get this warning:
48:6 warning React Hook useEffect has missing dependencies:
'overflowActive' and 'wrapper'. Either include them or remove the
dependency array react-hooks/exhaustive-deps
and I don't THINK I want to add them to the dependency array, as I don't want to trigger rendering when they change...?

I really enjoyed while solving the query.
Here is the implementation: https://codesandbox.io/s/react-using-hooks-in-section-component-5gibi?file=/src/ParagraphList.js
First of all, I was thinking of
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, [wrapper]);
But if we do this, it will again call the useEffect as when we'll click on the Read more button. Because it was comparing the reference of the wrapper and not it's value.
So, to avoid the reference comparison we have to use the useCallback hook.
const isOverflowingNode = node => {
return node.offsetHeight < node.scrollHeight;
};
const wrapper = useCallback(node => {
if (node !== null) {
setOverflowActive(isOverflowingNode(node));
}
}, []);
I came across the beautiful discussion: https://github.com/facebook/react/issues/14387
For more information:
https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
Thanks for the question :)

You could add an extra useEffect(() => (...),[]) that acts like componentDidMount(). And another useEffect(() => (...)) that acts like componentDidUpdate(). Then you should be able to get rid of userClicked.
This is a good link on how the lifestyle methods work with hooks. https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, []);
useEffect(() => {
if (!overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
});
The second one might need to be useLayoutEffect if you are wanting the update to happen after the layout.

Related

Toggle component without changing state

I have a component Scroller which I don't control which takes in data as a prop.
This data is a list of objects. Within object, one of the keys takes in a function.
This component has ability where upon clicking on the square, I am meant to show a new component (like a pop up).
The component Scroller which I don't control taking in the data prop.
<Scroller
data={getData(allData)}
/>
This is the data being passed in. content is a list of objects.
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: (() => {
setClicked(true); // this is the line which resets the scroll
}),
}
));
So this works as intended. Upon clicking, the new pop up content shows. This is due to state change via the setClicked function.
The issue is that this Scroller component has a scroll option. So user could have scrolled pass a a block (0) like following image.
But the moment I click the button to show the popup, it resets the scroll position back to 0 like following. Instead of remaining in position as above.
This scroll reset is the issue.
This is being caused by the call to setClicked function. It doesn't matter if I do anything with it. As long as I call it, it resets.
Showing the popup component is not the issue. The mere call to setClicked is the issue.
Thus wondering if there a way I could toggle showing the pop up component without having to set state?
Or a way to maintain the scroll position without resetting the scroll.
Note that in this instance I am using hooks. It is the same outcome if I use Redux. Please advice.
This is my component which I can control.
import React, { Fragment } from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const { allData } = data;
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
return (
<Fragment>
<Scroller
data={getData(allData)}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</Fragment>
);
};
export default MyComponent;
Explanation:
Each time setClick is called, the value of isClicked is changed, which causes MyComponent to be reevaluated. Since allData is initialized inside MyComponent, it will be reinitialized each time MyComponent is reevaluated. Another issue is that the data being sent to Scroller is the result of a function that takes in allData. Each time MyComponent is reevaluated, that function will run again and return a new array instance given the new allData instance. This means that every time MyComponent reevaluates, Scrollbar gets a new instance of data, causing anything that consumes data inside of Scrollbar to also be reevaluated.
Solution:
My suggestion would be to utilize react's useMemo hook (docs: https://reactjs.org/docs/hooks-reference.html#usememo) to 'memoize' the data going into Scroller:
import React from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const scrollerData = React.useMemo(()=> {
return data.allData.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
},[data])
return (
<>
<Scroller
data={scrollerData}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</>
);
};
export default MyComponent;
Also fun fact, <> is shorthand for React's Fragment
The problem could be that each time you click the component your Scroller gets a different reference of the data and because of that, it calls lifecycle methods that cause your performance issue.
If you will send the same props ( same reference ) to Scroller it should not call any lifecycle method which propably causes your problems.
import React, { Fragment, useMemo, useState } from 'react'
import Scroller from 'comp-external-lib'
import PopUpComponent from './PopUpComponent'
const MyComponent = props => {
const [isClicked, setClicked] = useState(false)
const { allData } = props.data
const getData = content =>
content.map((c, i) => ({
header: c.header,
customOnClick:
c.customOnClick &&
(() => {
setClicked(true)
})
}))
const scrollerData = useMemo(() => getData(allData), [allData])
return (
<Fragment>
<Scroller data={scrollerData} />
{isClicked && <PopUpComponent />}
</Fragment>
)
}
export default MyComponent
You are calling getData on every render cycle and thus causing a reset of the state:
data={getData(allData)}
The solution will be to wrap the getData function with a useCallback hook:
const getData = useCallback((content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
)),[]);

How to synchronize data in a React app? The data rendered doesn't match the data accessed in the function call

I'm making a React app that requests data from an API and renders the results as cards. The issue I'm having is that I added a 'favorite' star icon to each card, that when clicked, will become highlighted gold, and an additional click will revert it to grey. This is controlled by a Boolean value stored in a React Hook for the local component. The color change works properly, however when I want to access the boolean state variable to continue the rest of the operation, the local state value that I keep accessing seems to be the 'previous' state. I can't figure out how to fix this.
Reading up on the documentation leads me to think that I'm accessing stale state data or stale closures. I'm still having trouble fixing this so I would appreciate some assistance, here's what I have.
import React, { useState } from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faStar} from '#fortawesome/free-solid-svg-icons'
export const Card = ({ item, addOn, onDelete }) => {
//flags if the star was clicked or not
const [favored, setFavored] = useState(false);
//update favorite component
const updateFavorites = (item) => {
//here thet favored state is toggled
setFavored(!favored);
//further attempts to access favored results in the wrong favored being accessed
if(favored){
console.log(`value of favored is: ${favored}, the start should be gold`);
}else{
console.log(`value of favored is: ${favored}, the star should be grey`);
}
}
return (
<div className = 'card' key = {item.id} >
<FontAwesomeIcon
icon = {faStar}
className = {favored ? 'star favored' : 'star'}
onClick = {(e) => updateFavorites(item)}
/>
<div className = 'card-info'>
<img
src = {item.image_url}
alt = ''
style = {{height: '15rem', width: '5rem'}}
/>
<div>
<h3>{item.name}</h3>
<p className = "description">{item.description}</p>
</div>
</div>
</div>
)
}
Edit: I solved my issue by doing the following:
const updateFavorites = (item) => {
//here thet favored state is toggled
setFavored(favored => {
favored = !favored;
if(favored){
console.log('adding a favorite');
addOn(item);
}else{
console.log('removing a favorite');
onDelete(item);
};
return favored;
});
}
However, I replaced it by using useEffect as per the answers.
Fact is that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
You can use following solutions.
useEffect(() => {
if (favored) {
console.log(`value of favored is: ${favored}, the start should be gold`);
} else {
console.log(`value of favored is: ${favored}, the star should be grey`);
}
}, [favored,item]);
You should import useEffect and move the logic from the event handler to the useEffect hook:
import React, { useState, useEffect } from 'react'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faStar} from '#fortawesome/free-solid-svg-icons'
export const Card = ({ item, addOn, onDelete }) => {
const [favored, setFavored] = useState(false);
const updateFavorites = () => {
setFavored(!favored);
}
// Move the desired logic here and add 'favored' and 'item' to your dependency array:
useEffect(() => {
if(favored){
console.log(`value of favored is: ${favored}, the start should be gold`);
}else{
console.log(`value of favored is: ${favored}, the star should be grey`);
}
}, [favored, item])
return (
<div className = 'card' key = {item.id} >
<FontAwesomeIcon
icon = {faStar}
className = {favored ? 'star favored' : 'star'}
onClick = {() => updateFavorites()}
/>
<div className = 'card-info'>
<img
src = {item.image_url}
alt = ''
style = {{height: '15rem', width: '5rem'}}
/>
<div>
<h3>{item.name}</h3>
<p className = "description">{item.description}</p>
</div>
</div>
</div>
)
}

How to change background based on page url react js?

I have a simple app with a few pages, now I would like to change the background color based on page URL
using react js,
What is expected?:
When a pathname is /movies I want to change the background to red
Here is what I have so far
import React from 'react'
function Testing() {
const[moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState('green');
const getMoviesUrl = window.location.pathname;
if(getMoviesUrl == '/movies'){
setMoviesUrlBackgroundColor('red');
}else{
setMoviesUrlBackgroundColor('green');
}
return (
<div>
<Container style={{backgroundColor:moviesUrlBackgroundColor}}>
Testing
</Container>
</div>
)
}
export default Testing
const Container = styled.div`
background-color:green
`;
Unfortunately, I am getting the following URL
app.js:38323 Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
What do I need to do this working?
You should have an extra check to make sure you have set background color or not.Your current code is causing rerender infinte times
import React from 'react'
function Testing() {
const[moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState('green');
const [bgFlag, setbgFlag] = useState(false);
const getMoviesUrl = window.location.pathname;
if(!bgFlag){
setMoviesUrlBackgroundColor(getMoviesUrl == '/movies' ? 'red' : 'green')
setbgFlag(true)
}
return (
<div>
<Container style={{backgroundColor:moviesUrlBackgroundColor}}>
Testing
</Container>
</div>
)
}
export default Testing
const Container = styled.div`
background-color:green
`;
Use an useEffect block so you can perform side-effects effectively.
useEffect(() => {
if(getMoviesUrl === '/movies'){
console.log("running")
setMoviesUrlBackgroundColor('red');
}else{
setMoviesUrlBackgroundColor('green');
}
},[getMoviesUrl]);
The problem is that you call setMoviesUrlBackgroundColor without wrapping in an effect which results in getting called recursively.
In order to fix this, you just simple set state as needed in this case is your pathname has been changed:
React.useEffect(() => {
if (getMoviesUrl == '/movies'){
setMoviesUrlBackgroundColor('red');
} else{
setMoviesUrlBackgroundColor('green');
}
}, [getMoviesUrl])
Ok, I think I got it, you're creating an infinite loop on your first if statement:
if (getMoviesUrl == '/movies') {
// every time you change state, that causes your component
// to re-render, and when it re-renders again you're checking
// changing your state AGAIN, so it's an infinite loop
setMoviesUrlBackgroundColor('red');
}
I'll would recommend using react-route for this and getting the url from params, and then update the background color on componentDidMount or useEffect hook when component mounts for the first time, to prevent infinite loops.
Window.location.pathname was resetting state each render, so it needs to be placed within the useEffect hook to prevent re-renders.
Also, this would be a good use-case to pass props to your styled component.
Also included this in the code below.
Here's a link to a codesandbox I made with the solution.
https://codesandbox.io/s/musing-mirzakhani-njmsh?file=/src/random.js:0-620
import React, { useState, useEffect } from "react";
import styled from "styled-components";
const Container = styled.div`
background: ${(props) => props.backgroundColor || "green"};
`;
const Testing = () => {
const [moviesUrlBackgroundColor, setMoviesUrlBackgroundColor] = useState(
"green"
);
useEffect(() => {
const getMoviesUrl = window.location.pathname;
if (getMoviesUrl === "/movies") {
setMoviesUrlBackgroundColor("yellow");
}
}, [moviesUrlBackgroundColor]);
return (
<div>
<Container backgroundColor={moviesUrlBackgroundColor}>Test</Container>
</div>
);
};
export default Testing;
Cheers!

React Warning: Cannot update a component from inside the function body of a different component

I am using Redux with Class Components in React. Having the below two states in Redux store.
{ spinner: false, refresh: false }
In Parent Components, I have a dispatch function to change this states.
class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
this.props.onShowSpinner();
this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
In Child Component, I am trying to reload the parent component like below.
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
if (somecondition) {
// doing some redux store update
props.reloadApp();
}
}
render() {
return <button />;
}
}
I am getting error as below.
Warning: Cannot update a component from inside the function body of a
different component.
How to remove this warning? What I am doing wrong here?
For me I was dispatching to my redux store in a React Hook. I had to dispatch in a useEffect to properly sync with the React render cycle:
export const useOrderbookSubscription = marketId => {
const { data, error, loading } = useSubscription(ORDERBOOK_SUBSCRIPTION, {
variables: {
marketId,
},
})
const formattedData = useMemo(() => {
// DISPATCHING HERE CAUSED THE WARNING
}, [data])
// DISPATCHING HERE CAUSED THE WARNING TOO
// Note: Dispatching to the store has to be done in a useEffect so that React
// can sync the update with the render cycle otherwise it causes the message:
// `Warning: Cannot update a component from inside the function body of a different component.`
useEffect(() => {
orderbookStore.dispatch(setOrderbookData(formattedData))
}, [formattedData])
return { data: formattedData, error, loading }
}
If your code calls a function in a parent component upon a condition being met like this:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
Try wrapping the condition in a useEffect:
const ListOfUsersComponent = ({ handleNoUsersLoaded }) => {
const { data, loading, error } = useQuery(QUERY);
useEffect(() => {
if (data && data.users.length === 0) {
return handleNoUsersLoaded();
}
}, [data, handleNoUsersLoaded]);
return (
<div>
<p>Users are loaded.</p>
</div>
);
};
It seems that you have latest build of React#16.13.x. You can find more details about it here. It is specified that you should not setState of another component from other component.
from the docs:
It is supported to call setState during render, but only for the same component. If you call setState during a render on a different component, you will now see a warning:
Warning: Cannot update a component from inside the function body of a different component.
This warning will help you find application bugs caused by unintentional state changes. In the rare case that you intentionally want to change the state of another component as a result of rendering, you can wrap the setState call into useEffect.
Coming to the actual question.
I think there is no need of getDerivedStateFromProps in the child component body. If you want to trigger the bound event. Then you can call it via the onClick of the Child component as i can see it is a <button/>.
class Child extends React.Component {
constructor(props){
super(props);
this.updateState = this.updateState.bind(this);
}
updateState() { // call this onClick to trigger the update
if (somecondition) {
// doing some redux store update
this.props.reloadApp();
}
}
render() {
return <button onClick={this.updateState} />;
}
}
Same error but different scenario
tl;dr wrapping state update in setTimeout fixes it.
This scenarios was causing the issue which IMO is a valid use case.
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setSomeState(someNewValue);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
fix
const [someState, setSomeState] = useState(someValue);
const doUpdate = useRef((someNewValue) => {
setTimeout(() => {
setSomeState(someNewValue);
}, 0);
}).current;
return (
<SomeComponent onSomeUpdate={doUpdate} />
);
In my case I had missed the arrow function ()=>{}
Instead of onDismiss={()=>{/*do something*/}}
I had it as onDismiss={/*do something*/}
I had same issue after upgrading react and react native, i just solved that issue by putting my props.navigation.setOptions to in useEffect. If someone is facing same problen that i had i just want to suggest him put your state changing or whatever inside useEffect
Commented some lines of code, but this issue is solvable :) This warnings occur because you are synchronously calling reloadApp inside other class, defer the call to componentDidMount().
import React from "react";
export default class App extends React.Component {
reloadHandler = () => {
console.log("[App] reloadComponent");
// this.props.onShowSpinner();
// this.props.onRefresh();
};
render() {
return <Child reloadApp={this.reloadHandler} />;
}
}
class Child extends React.Component {
static getDerivedStateFromProps(props, state) {
// if (somecondition) {
// doing some redux store update
props.reloadApp();
// }
}
componentDidMount(props) {
if (props) {
props.reloadApp();
}
}
render() {
return <h1>This is a child.</h1>;
}
}
I got this error using redux to hold swiperIndex with react-native-swiper
Fixed it by putting changeSwiperIndex into a timeout
I got the following for a react native project while calling navigation between screens.
Warning: Cannot update a component from inside the function body of a different component.
I thought it was because I was using TouchableOpacity. This is not an issue of using Pressable, Button, or TouchableOpacity. When I got the error message my code for calling the ChatRoom screen from the home screen was the following:
const HomeScreen = ({navigation}) => {
return (<View> <Button title = {'Chats'} onPress = { navigation.navigate('ChatRoom')} <View>) }
The resulting behavior was that the code gave out that warning and I couldn't go back to the previous HomeScreen and reuse the button to navigate to the ChatRoom. The solution to that was doing the onPress in an inline anonymous function.
onPress{ () => navigation.navigate('ChatRoom')}
instead of the previous
onPress{ navigation.navigate('ChatRoom')}
so now as expected behavior, I can go from Home to ChatRoom and back again with a reusable button.
PS: 1st answer ever in StackOverflow. Still learning community etiquette. Let me know what I can improve in answering better. Thanx
If you want to invoke some function passed as props automatically from child component then best place is componentDidMount lifecycle methods in case of class components or useEffect hooks in case of functional components as at this point component is fully created and also mounted.
I was running into this problem writing a filter component with a few text boxes that allows the user to limit the items in a list within another component. I was tracking my filtered items in Redux state. This solution is essentially that of #Rajnikant; with some sample code.
I received the warning because of following. Note the props.setFilteredItems in the render function.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- Danger! Updates Redux during a render!
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I ran this code with React 16.12.0, I received the warning listed in the topic of this thread in my browser console. Based on the stack trace, the offending line was my props.setFilteredItems invocation within the render function. So I simply enclosed the filter invocations and state change in a useEffect as below.
import {setFilteredItems} from './myActions';
const myFilters = props => {
const [nameFilter, setNameFilter] = useState('');
const [cityFilter, setCityFilter] = useState('');
useEffect(() => {
const filterName = record => record.name.startsWith(nameFilter);
const filterCity = record => record.city.startsWith(cityFilter);
const selectedRecords = props.records.filter(rec => filterName(rec) && filterCity(rec));
props.setFilteredItems(selectedRecords); // <-- OK now; effect runs outside of render.
}, [nameFilter, cityFilter]);
return <div>
<input type="text" value={nameFilter} onChange={e => setNameFilter(e.target.value)} />
<input type="text" value={cityFilter} onChange={e => setCityFilter(e.target.value)} />
</div>
};
const mapStateToProps = state => ({
records: state.stuff.items,
filteredItems: state.stuff.filteredItems
});
const mapDispatchToProps = { setFilteredItems };
export default connect(mapStateToProps, mapDispatchToProps)(myFilters);
When I first added the useEffect I blew the top off the stack since every invocation of useEffect caused state change. I had to add an array of skipping effects so that the effect only ran when the filter fields themselves changed.
I suggest looking at video below. As the warning in the OP's question suggests, there's a change detection issue with the parent (Parent) attempting to update one child's (Child 2) attribute prematurely as the result of another sibling child's (Child 1) callback to the parent. For me, Child 2 was prematurely/incorrectly calling the passed in Parent callback thus throwing the warning.
Note, this commuincation workflow is only an option. I personally prefer exchange and update of data between components via a shared Redux store. However, sometimes it's overkill. The video suggests a clean alternative where the children are 'dumb' and only converse via props mand callbacks.
Also note, If the callback is invoked on an Child 1 'event' like a button click it'll work since, by then, the children have been updated. No need for timeouts, useEffects, etc. UseState will suffice for this narrow scenario.
Here's the link (thanks Masoud):
https://www.youtube.com/watch?v=Qf68sssXPtM
In react native, if you change the state yourself in the code using a hot-reload I found out I get this error, but using a button to change the state made the error go away.
However wrapping my useEffect content in a :
setTimeout(() => {
//....
}, 0);
Worked even for hot-reloading but I don't want a stupid setTimeout for no reason so I removed it and found out changing it via code works just fine!
I was updating state in multiple child components simultaneously which was causing unexpected behavior. replacing useState with useRef hook worked for me.
Try to use setTimeout,when I call props.showNotification without setTimeout, this error appear, maybe everything run inTime in life circle, UI cannot update.
const showNotifyTimeout = setTimeout(() => {
this.props.showNotification();
clearTimeout(showNotifyTimeout);
}, 100);

Why isn't `useContext` re-rendering my component?

As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode

Categories

Resources