React component is rendering old state before getting new data - javascript

When the component first renders it gets from the switchcase (enters ALL case because its the default value) on the usePositions hook, the value for positions and I set them there, and return them to the Positions controller.
The problem comes when the selectedClient changes from ALL to TODAY (its a context value, I change it in a Sidebar component somewhere else) and before entering the switch case in TODAY value to get the positions of today, I noticed the Positions component already rendered the old state of ALL positions again! Then a second later it renders correctly the todays positions.
I noticed this because my browser on my network tab shows some calls to the server that IndividualPosition makes it means that it rendered
This is my component where it calls the usePositions hook
export const Positions = () => {
const { selectedClient } = useSelectedClientValue();
const { loading, setLoading } = useLoadingValue();
const { positions } = usePositions(selectedClient);
const clientName =
typeof selectedClient === "string" ? selectedClient : selectedClient.name;
useEffect(() => {
setLoading(false);
}, [positions]);
let positionsView = (
<ul className="positions__list">
{positions.map((position) => (
<IndividualPosition position={position} key={position.positionId} />
))}
</ul>
);
return (
<div className="positions" data-testid="positions">
{!loading ? (
<>
<div className="positions__header">
<h2 data-testid="client-name">{clientName}</h2>
</div>
{positionsView}
</>
) : (
<div className="loading-main-window">
<Spinner />
</div>
)}
</div>
);
};
This is the hook where I fetch the data
export const usePositions = selectedClient => {
const [positions, setPositions] = useState([]);
const {setLoading} = useLoadingValue()
useEffect(() => {
setLoading(true);
switch (selectedClient) {
case 'ALL':
getPositions().then(pos => {
setPositions(pos);
});
break;
case 'TODAY':
getTodayPositions().then(pos => {
setPositions(pos);
});
break;
default:
break;
}
}, [selectedClient]);
return {positions, setPositions};
};
The useEffect runs each time the selectedClient change
It looks like the component renders again before getting the todays data and thats why it shows the old state before getting the new data, but I thought that could be avoided with the loading flag
Basically:
-Positions renders, the hook fetches allPositions, its fine
-If I change in the sidebar the value of the selectedClient context value, the Positions components renders again, rendering the IndividualComponent but with the state of allPositions.
- Instead it should wait till todaysPositions fetches to show the new state (loading should do that)
I already tried having a loading local state (my loading is a context value)
Moving the loading in the useEffects on my local component instead of my hook
Setting loading to false inside the hook after fetching the data
Any ideas?

Related

React Native + Context + FlashList wont re-render with Context update + extraData updating

The problem: I have a FlashList that uses React Context to fill in the data (the data is an array of objects that renders a View) but when I update the context and the extraData prop for FlashList, the list does not re-render, or re-renders sometimes, or takes multiple events to actually re-render.
The Code:
// Many imports, they are all fine though
export default () => {
// Relevant context.
const {
cardsArray,
cardsArrayFiltered,
updateCardsArray,
updateCardsArrayFiltered
} = useContext(AppContext);
// Relevant state.
const [didUpdateCards, setDidUpdateCards] = useState(false);
const [cardsFilters, setCardsFilters] = useState([]);
// Relevant refs.
const flatListRef = useRef(null);
// Example effect on mount
useEffect(() => {
setInitialAppState();
}, []);
// Effect that listen to changing on some data that update the context again
useEffect(() => {
const newCardsArray = doSomeFiltering(cardsArray, cardsFilters);
updateCardsArrayFiltered(newCardsArray);
setDidUpdateCards(!didUpdateCards);
}, [cardsFilters]);
// Example of promisey function that sets the initial context.
const setInitialAppState = async () => {
try {
const newCardsArray = await getPromiseyCards();
updateCardsArrayFiltered(newCardsArray);
updateCardsArray(newCardsArray);
} catch ( err ) {
console.debug( err );
}
}
// Renderer for the list item.
const renderListItem = useCallback((list) => <Card key={list.index} card={list.item} />, []);
// List key extractor.
const listKeyExtractor = useCallback((item) => item.id, []);
return (
<FlashList
ref={flatListRef}
data={cardsArrayFiltered}
extraData={didUpdateCards}
keyExtractor={listKeyExtractor}
renderItem={renderListItem}
showsVerticalScrollIndicator={false}
estimatedItemSize={Layout.window.height}
/>
);
}
Notes:
What I did not write all out is the function, logic, view to update cardsFilters however the above effect IS running when it changes.
Moreover, this line here, const newCardsArray = doSomeFiltering(cardsArray, cardsFilters); does indeed return the proper updated data.
What's going on here? I am updating the extraData prop with that didUpdateCards state when the context changes which I thought was the requirement to re-render a FlatList/FlashList.
It looks like object being passed as extraData is a boolean. This means that if the previous value was true, setting it as true again wouldn't count as a change. Instead use an object and update it when you want list to update.
To try just set extraData={{}}. if everything works as expected it means that your update logic has some problem.

setState inside useEffect not working on First Render in React JS

I'm building a ReactJS Component that uses React Awesome Slider.
What I'm trying to create is a slider with a description div under it, which changes the text then I change the Slide.
Now, I found a way to make it work but I have a problem with the setState of an object, here is the code.
SLIDER:
const AutoplaySlider = withAutoplay(AwesomeSlider);
const StaticSlider = ({slider}) => {
var images = "";
var length=0;
const [current, setCurrent] = useState(0);
const [title, setTitle] = useState([]);
const [description, setDescription] = useState([]);
switch (slider) {
case 'portfolio_sviluppo_software':
images = portfolio_description.sviluppo_software;
length= portfolio_description.sviluppo_software.length;
break;
case 'portfolio_domotica':
images = portfolio_description.domotica;
length= portfolio_description.domotica.length;
break;
case 'portfolio_digital_signage':
images = portfolio_description.digital_signage;
length= portfolio_description.digital_signage.length;
break;
case 'portfolio_ricerca_e_sviluppo':
images = portfolio_description.ricerca_e_sviluppo;
length= portfolio_description.ricerca_e_sviluppo.length;
break;
}
useEffect(
() => {
setTitle(
images.map(
(slide) => (slide.title)
)
);
setDescription(
images.map(
(desc) => (desc.data)
)
);
}, [images]
);
return(
<div>
<AutoplaySlider
play={true}
cancelOnInteraction={true}
interval={0}
onTransitionStart={slide => setCurrent(slide.nextIndex)}
className="sliderHome"
>
{images.map((image, index) => {
let src = "/image/slider/portfolio/"+image.image;
//console.log(src);
return (
<div key={index} data-src={src}>
</div>
);
})}
</AutoplaySlider>
<GalleryCaption selected={current} title={title} description={description} area={slider}/>
</div>
)
};
export default StaticSlider;
DESCRIPTION GENERATOR
const GalleryCaption = ({ selected = 0, title = [], description= [], area = 0 }) => {
const formattedIndex = selected + 1;
var title = title[selected];
var data = description[selected];
return (
<div className="containerDivDescriptionPortflio">
<div className="DivDescriptionPortflio">
<p id ={"description_portfolio_"+area} className="paragDescriptionPortflio" >
<h4>{title}</h4>
<hr></hr>
{
data.map((val) => (
<div className="rowDescriptionPortfolio">
<div className="divIndexPortfolio" dangerouslySetInnerHTML={{ __html: val.index }} >
</div>
<div className="divTextPortfolio" dangerouslySetInnerHTML={{ __html: val.text }} >
</div>
</div>
))}
</p>
</div>
</div>
);
};
export default GalleryCaption;
OBJECT EXAMPLE
{
"title":"text",
"data":[
{
"index":"text",
"text": "text"
},
{
"index":"text",
"text": "text"
}
],
"image": "folder/image.jpg"
},
(This is an element of an array of this kind of object)
Now the main problem is that if inside the use effect I only call the setTitle function all works as it should, but if I use also the setDescription all just stop working. I didn't get a specific error, but I get a white screen.
ERROR THAT I GET
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `PortfolioArea`. See https://reactjs.org/link/warning-keys for more information.
at div
at PortfolioArea (http://localhost:3000/static/js/bundle.js:1618:5)
at http://localhost:3000/static/js/bundle.js:4509:78
at Routes (http://localhost:3000/static/js/bundle.js:177110:5)
at Router (http://localhost:3000/static/js/bundle.js:177043:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:176523:5)
at App
at AppProvider (http://localhost:3000/static/js/bundle.js:3289:5)
Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details.
* Move data fetching code or side effects to componentDidUpdate.
* If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://reactjs.org/link/derived-state
Please update the following components: AwesomeSlider
The above error occurred in the <GalleryCaption> component:
at GalleryCaption (http://localhost:3000/static/js/bundle.js:973:5)
at div
at StaticSlider (http://localhost:3000/static/js/bundle.js:3049:5)
at div
at PortfolioArea (http://localhost:3000/static/js/bundle.js:1618:5)
at http://localhost:3000/static/js/bundle.js:4510:78
at Routes (http://localhost:3000/static/js/bundle.js:177111:5)
at Router (http://localhost:3000/static/js/bundle.js:177044:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:176524:5)
at App
at AppProvider (http://localhost:3000/static/js/bundle.js:3290:5)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.`
I've tried to change the useEffect second parameters to null and also to use a unique state for every parameter, but the problem seems to be that every time I try to set a state with an object inside the useEffect, on the first render I always get a null value inside that state.
Any tips?
I think that in StaticSlider, since images and length are calculated based on slider prop, I suggest using useMemo() to calculate them per slider change, instead of reassigning the values to the var variables, which is not how it should be done in React and invites bugs to come.
const StaticSlider = ({slider}) => {
const images = useMemo(
() => {
// calculate and return images value
// with `switch`
switch (slider) {
// ...
default:
return [];
}
},
[slider]
);
const length = useMemo(
() => {
// calculate and return length value
// with `switch`
switch (slider) {
// ...
default:
return 0;
}
},
[slider]
);
Please note that your current switch block does not have a default case, you should consider returning a default case with initial values for images and length.
Also note that the initial assignment of images is images = "" which would make it a string, but inside setDescription() you are calling images.map() which is an array method, so it won't work at the initial render of the component when images is an empty string. I think this is what causes the bug.
images = [] or default: return [] for images inside switch statement should be better.
Lastly I think you can consider using a condition check inside useEffect() to only setState on title and description when images is already populated (has length).
useEffect(
() => {
if (images.length) {
setTitle(
images.map(
(slide) => (slide.title)
)
);
setDescription(
images.map(
(desc) => (desc.data)
)
);
}
},
[images]
);

How to stop making api call on re-rendering in React?

In my homepage, I have code something like this
{selectedTab===0 && <XList allItemList={some_list/>}
{selectedTab===1 && <YList allItemList={some_list2/>}
Now, In XList, I have something like this:
{props.allItemList.map(item => <XItem item={item}/>)}
Now, Inside XItem, I am calling an api to get the image of XItem.
Now my problem is When In homepage, I switched the tab from 0 to 1 or 1 to 0, It is calling all the API's in XItem again. Whenever I switched tab it calls api again. I don't want that. I already used useEffect inside XItem with [] array as second parameter.
I have my backend code where I made an api to get the image of XItem. The API is returning the image directly and not the url, so I can't call all api once.
I need some solution so that I can minimize api call.
Thanks for help.
The basic issue is that with the way you select the selected tab you are mounting and unmounting the components. Remounting the components necessarily re-runs any mounting useEffect callbacks that make network requests and stores any results in local component state. Unmounting the component necessarily disposes the component state.
{selectedTab === 0 && <XList allItemList={some_list} />}
{selectedTab === 1 && <YList allItemList={some_list2} />}
One solution could be to pass an isActive prop to both XList and YList and set the value based on the selectedTab value. Each component conditionally renders its content based on the isActive prop. The idea being to keep the components mounted so they only fetch the data once when they initially mounted.
<XList allItemList={some_list} isActive={selectedTab === 0} />
<YList allItemList={some_list2} isActive={selectedTab === 1} />
Example XList
const XList = ({ allItemList, isActive }) => {
useEffect(() => {
// expensive network call
}, []);
return isActive
? props.allItemList.map(item => <XItem item={item}/>)
: null;
};
Alternative means include lifting the API requests and state to the parent component and passing down as props. Or using a React context to do the same and provide out the state via the context. Or implement/add to a global state management like Redux/Thunks.
Just to quickly expand on Drew Reese's answer, consider having your tabs be children of a Tabs-component. That way, your components are (slightly) more decoupled.
const MyTabulator = ({ children }) => {
const kids = React.useMemo(() => React.Children.toArray(children), [children]);
const [state, setState] = React.useState(0);
return (
<div>
{kids.map((k, i) => (
<button key={k.props.name} onClick={() => setState(i)}>
{k.props.name}
</button>
))}
{kids.map((k, i) =>
React.cloneElement(k, {
key: k.props.name,
isActive: i === state
})
)}
</div>
);
};
and a wrapper to handle the isActive prop
const Tab = ({ isActive, children }) => <div hidden={!isActive}>{children}</div>
Then render them like this
<MyTabulator>
<Tab name="x list"><XList allItemList={some_list} /></Tab>
<Tab name="y list"><YList allItemList={some_list2} /></Tab>
</MyTabulator>
My take on this issue. You can wrap XItem component with React.memo:
const XItem = (props) => {
...
}
const areEqual = (prevProps, nextProps) => {
/*
Add your logic here to check if you want to rerender XItem
return true if you don't want rerender
return false if you want a rerender
*/
}
export default React.memo(XItem, areEqual);
The logic is the same if you choose to use useMemo.
If you want to use useCallback (my default choice) would require only to wrap the call you make to the api.

Child components get updated properly only after the second click in react functional component

I have the following component, where review assignments (props.peerReviewAssignmentIds) are loaded for a student's own work (related event is onClick_Submission) or a peer's work to review (related event is onClick_PeerReview ). These events work fine and the related data is loaded successfully. However, there is a problem with updating the content of the child components based on the value of the props.peerReviewAssignmentIds, which I elaborate below.
const AssignmentItem = (props) => {
const assignment = props.assignments[props.currentAssignmentId];
const onClick_Submission = (e) => {
e.preventDefault();
if (!st_showSubmission) {
props.fetchPeerReviewAssignmentForStudent(currentUserId, assignment.activeReviewRoundId);
}
set_showSubmission(!st_showSubmission);
set_isPeerReview(false);
}
const onClick_PeerReview = (e) => {
e.preventDefault();
if (!st_showPeerReviews) {
if (st_submissionContiues === false)
props.fetchPeerReviewAssignmentForReviewer(currentUserId, assignment.activeReviewRoundId);
}
set_showPeerReviews(!st_showPeerReviews);
set_isPeerReview(true);
}
return (
<>
{
st_showSubmission === true && props.peerReviewAssignmentIds.length > 0 &&
<ReviewPhaseInfoForSubmission isPeerReview={false} />
}
</>
)
}
const mapStateToProps = state => ({
peerReviewAssignmentIds: state.peerReviewAssignmentReducer.peerReviewAssignmentIds,
loading_pra: state.peerReviewAssignmentReducer.loading,
error_pra: state.peerReviewAssignmentReducer.error,
})
I will try to explain the problem with an example. When the first time onClick_Submission is triggered, props.peerReviewAssignmentIds[0] is set to 2, and all the sub components are loaded properly. Next, when onClick_PeerReview is triggered, props.peerReviewAssignmentIds[0] is set to 1, which is correct. But, the child components get updated according to the previous value of props.peerReviewAssignmentIds[0], which was 2. If the onClick_PeerReview event is triggered second time, then the child components get updated correctly according to the current value of props.peerReviewAssignmentIds[0], which is 1. Any ideas why this might be happening?
I further explain my code below.
Below is the ReviewPhaseInfoForSubmission component. In this component, based on props.peerReviewAssignmentIds[0] value (which gets updated in the parent component above) etherpadLaterSubmission variable is created with props.createSession_laterSubmission method.
const ReviewPhaseInfoForSubmission = (props) => {
const [st_is_discussFeedback, set_is_discussFeedback] = useState(false);
useEffect(() => {
set_is_discussFeedback(true);
if (props.etherpadLaterSubmission === null) {
props.createSession_laterSubmission(props.peerReviewAssignmentIds[0], discussFeedback.dueDate);
}
}, [])
return (
<div className="p-1">
{
st_is_discussFeedback === true &&
<ProvideOrDiscussFeedback provide0discuss1revise2={1} isPeerReview={props.isPeerReview} />
}
</div>
);
}
const mapStateToProps = (state) => ({
etherpadLaterSubmission: state.etherpadReducer.etherpadLaterSubmission,
loading_ep: state.etherpadReducer.loading,
})
Then, in a child component, ProvideOrDiscussFeedback (see below), the props.etherpadLaterSubmission value is used for display purposes.
const ProvideOrDiscussFeedback = (props) => {
return <div className="p-3 shadow">
{
props.etherpadLaterSubmission &&
<div>
<DisplayingEtherpad etherpadSession={props.etherpadLaterSubmission } />
</div>
}
</div>
}

react-native flatlist images flicker when list state updating

I have populated a FlatList with data fetched from Google's firebase backend. The implementation is rather standard, here's a stripped down version:
export default class Day extends Component {
state = { data : [], today: false }
componentWillMount = async () => {
const { today } = this.state;
const { calendarDb } = this.props
await calendarDb.onNewAgenda({
day : today
, then: this.parseOnListed
})
}
parseOnListed = blob => {
const { data } = this.state;
data.push(blob)
this.setState({ data: data })
}
renderItem = ({ item }) =>
<Hour data = {item}/>
render = () =>
<FlatList
data = {this.state.data}
renderItem = {this.renderItem}
keyExtractor = {item => item.ID}
/>
}
The issue is that every time a new blob is pushed into data, the <Image/> component in <Hour data={item}/> flickers. This makes the list a no-go in terms of user experience. What gives? <Hour/> is standard as well, and more or less look like this:
const Hour = ({ data }) =>
<View>
<Image source={{uri:data.uri}}/>
<Text> {data.name} </Text>
</View>
The content of <Text> does not flicker, only the image from <Image .../>
Check whether keyExtractor is getting unique ID or not.
The flat list is re-rendering on state update and images are downloaded again. Because, each row is not uniquely identified as said in comments by #Guruparan Giritharan.
I found another reason that triggers this issue, of the FlatList flikering on React native. In my case, it happened every time I updated/changed the state of any function component. So, for instance, I was keeping the fetch results (data) and the next-page-id (for the next paginated fetch) in two separate function components:
const [data, setData] = useState([]);
const [pageId, setPageId] = useState(null);
Hence, every time would capture the results of my fetch, I would first set the data update and then the page id. It was the page id update what was causing the flicker.
const onEndReachedFetch = async () ={
fetch(pageId).then(result => {
setData(result.Data);
setPageId(result.pageId);
});
}
The fix was just to put the state data together so there is a single update instead. Then react is happy and doesn't flicker when adding new items to the list.
const onEndReachedFetch = async () ={
fetch(pageId).then(result => {
setResult(result);
});
}
Beware of any side states that you may be updating in the background, as they may also cause the flickering if they are triggered by anything on the FlatList.

Categories