How to render AdMob banner in React Flatlist between items? - javascript

I have a React Native Flatlist that only re-renders when its data has changed.
I give it the following data (as prop):
const posts = [
{
...post1Data
},
{
...post2Data
},
{
...post3Data
},
{
...post4Data
},
{
...post5Data
},
]
And here is my FlatList renderItem:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
How can I add an AdMob ad between the FlatList data with a probability of 5% without skiping any data in the posts array?
I have tried this:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
if (Math.random() < 0.05) return <Ad ... />
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
But this causes 2 problems:
Some items from data are skipped (not returned)
When the flatlist re-renders (because of some of its props changes) the ads might disappear (there is a chance of 95%).
Any ideas? Should I render the ads randomly in the footer of my Card component like this?
const Card = memo ((props) => {
...
return (
<AuthorRow ... />
<Content ... />
<SocialRow ... /> {/* Interaction buttons */}
<AdRow />
)
}, (prevProps, nextProps) => { ... });
const AdRow = memo(() => {
return <Ad ... />
}, () => true);
I am not really sure about this option, it works but it could violate the admob regulations (?) (because I am adapting the ad to the layout of my card component)
I would appreciate any kind of guidance/help. Thank you.

I'm not sure if you ever found a solution to this problem, but I accomplished this by injecting "dummy" items into the data set, then wrapping the renderItem component with a component that switches based on the type of each item.
Assuming your flatlist is declared like this:
<FlatList data={getData()} renderItem={renderItem}/>
And your data set is loaded into a variable called sourceData that is tied to state. Let's assume one entry in your sourceData array looks like this. Note the 'type' field to act as a type discriminator:
{
"id": "d96dce3a-6034-47b8-aa45-52b8d2fdc32f",
"name": "Joe Smith",
"type": "person"
}
Then you could declare a function like this:
const getData = React.useCallback(() => {
let outData = [];
outData.push(...sourceData);
// Inject ads into array
for (let i = 4; i < outData.length; i += 5)
{
outData.splice(i, 0, {type:"ad"});
}
return outData;
}, [sourceData]);
... which will inject ads into the data array between every 4th item, beginning at the 5th item. (Since we're pushing new data into the array, i += 5 means an ad will be placed between every 4th item. And let i = 4 means our first ad will show after the 5th item in our list)
Finally, switch between item types when you render:
const renderItem = ({ item }) => (
item.type === 'ad'
?
<AdComponent ...props/>
:
<DataComponent ...props/>
);

Related

Trouble with React Native and Firebase RTDB

I'm using React Native (0.68) and Firebase RTDB (with the SDK, version 9), in Expo.
I have a screen that needs to pull a bunch of data from the RTDB and display it in a Flatlist.
(I initially did this without Flatlist, but initial rendering was a bit on the slow side.)
With Flatlist, initial rendering is super fast, huzzah!
However, I have an infinite loop re-render that I'm having trouble finding and fixing. Here's my code for the screen, which exists within a stack navigator:
export function GroupingsScreen () {
... set up a whole bunch of useState, database references (incl groupsRef) etc ...
onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
if (loaded == false) {
console.log('--start processing')
setLoaded(true);
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
console.log('--done processing')
setGroupsArray(newObject)
}
});
.... more stuff excerpted ....
return (
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)};
I'm using loaded/setLoaded to reduce re-renders, but without that code, RN immediately dumps me out for excessive re-renders. Even with it, I get lots of extra renders.
So...
Can someone point me at what's triggering the rerender? The database is /not/ changing.
Is there a better way to get RTDB info into a Flatlist than the code I've written?
I have some code that actually does change the database. That's triggering a full rerender of the whole Flatlist, which is visibly, painfully slow (probably because parts are actually rendering 10x instead of once). Help?
For completeness, here's the OneItem code, so you can see just how complex my Flatlist items are:
const OneItem = (data) => {
// console.log('got data',data)
return (
<View style={[styles.rowView, { backgroundColor: data.sku?'white': '#cccccc'}]} key={data.name}>
<TouchableOpacity style={styles.nameView} onPress={() => {
navigation.navigate('AddEditItemScreen', {purpose: 'Grouping', itemname: data.name, parent: data.parent, mode: 'fix'})
}}>
<View style={styles.nameView}>
<Text style={styles.itemtext}>{data.name}</Text>
{data.sku? null: <Text>"Tap to add SKU."</Text>}
{data.size?<Text>{data.size} </Text>: <Text>no size</Text>}
</View>
</TouchableOpacity>
<View style={styles.buttonView}>
<Button style={styles.smallButton}
onPress={() => { changeQuant(data.quantity ? data.quantity - 1 : -1, data.parent + '/ingredients/' + data.name) }}
>
{data.quantity > 0 ? <Text style={[styles.buttonText, { fontSize: 20 }]}>-</Text>
:<Image source={Images.trash} style={styles.trashButton} />}</Button>
<Text style={styles.quantitytext}>{data.quantity}</Text>
<Button style={styles.smallButton}
onPress={() => {
changeQuant(data.quantity? data.quantity +1 : 1, data.parent+'/ingredients/'+data.name)}}>
<Text style={[styles.buttonText, {fontSize: 20}]}>+</Text></Button>
</View>
</View>
)
};```
I worked out how to stop the rerender (question #1). So, within my Screen functional component, I needed to make another function, and attach the state hook and useEffect to that. I'm not totally sure I understand why, but it gets rid of extra renders. And it's enough to get #3 to tolerable, although perhaps not perfect.
Here's the new code:
export function GroupingsScreen () {
... lots of stuff omitted ...
function JustTheList() {
const [groupsArray, setGroupsArray] = useState([])
useEffect(() => {
const subscriber = onValue(groupsRef, (snapshot) => {
console.log('groups onValue triggered')
let data = snapshot.val();
let newObject = []
for (let [thisgrouping, contents] of Object.entries(data)) {
let onegroupingObject = { title: thisgrouping, data: [] }
for (let [name, innerdata] of Object.entries(contents.ingredients)) {
onegroupingObject.data.push({ name: name, sku: innerdata.sku, size: innerdata.size,
quantity: innerdata.quantity,
parent: thisgrouping
})
}
newObject.push(onegroupingObject)
}
setGroupsArray(newObject)
})
return () => subscriber();
}, [])
return(
<View style={styles.tile}>
<SectionList
sections={groupsArray}
getItemLayout={getItemLayout}
renderItem={ oneRender }
renderSectionHeader={oneSection}
initialNumToRender={20}
removeClippedSubviews={true}
/>
</View>
)
}
And then what was my return within the main functional screen component became:
return (
<JustTheList />
)
I'm still very interested in ideas for improving this code - am I missing a better way to work with RTDB and Flatlist?

Deleting list Item in react keep older items instead

I would like to delete selected item from list.
When I click on delete the right item get deleted from the list content but on UI I get always the list item fired.
I seems to keep track of JSX keys and show last values.
Here's a demo
const Holidays = (props) => {
console.log(props);
const [state, setState] = useState({ ...props });
useEffect(() => {
setState(props);
console.log(state);
}, []);
const addNewHoliday = () => {
const obj = { start: "12/12", end: "12/13" };
setState(update(state, { daysOffList: { $push: [obj] } }));
};
const deleteHoliday = (i) => {
const objects = state.daysOffList.filter((elm, index) => index != i);
console.log({ objects });
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
};
return (
<>
<Header as="h1" content="Select Holidays" />
<Button
primary
icon={<AddIcon />}
text
content="Add new holidays"
onClick={() => addNewHoliday(state)}
/>
{state?.daysOffList?.map((elm, i) => {
console.log(elm.end);
return (
<Flex key={i.toString()} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
key={i.toString()}
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>
<span>{JSON.stringify(state.daysOffList)}</span>
</Flex>
);
})}
</>
);
};
export default Holidays;
Update
I'm trying to make a uniq id by adding timeStamp.
return (
<Flex key={`${JSON.stringify(elm)} ${Date.now()}`} gap="gap.small">
<>
<Header as="h5" content="Start Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.start}/${new Date().getFullYear()}`)
}
/>
</>
<>
<Header as="h5" content="End Date" />
<Datepicker
defaultSelectedDate={
new Date(`${elm.end}/${new Date().getFullYear()}`)
}
/>
</>
<Button
primary
key={`${JSON.stringify(elm)} ${Date.now()}`}
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(i)}
/>{" "}
</Flex>
);
I was hoping that the error disappear but still getting same behaviour
Issue
You are using the array index as the React key and you are mutating the underlying data array. When you click the second entry to delete it, the third element shifts forward to fill the gap and is now assigned the React key for the element just removed. React uses the key to help in reconciliation, if the key remains stable React bails on rerendering the UI.
You also can't console log state immediately after an enqueued state update and expect to see the updated state.
setState(update(state, { daysOffList: { $set: objects } }));
console.log(state.daysOffList);
React state updates are asynchronous and processed between render cycles. The above can, and will, only ever log the state value from the current render cycle, not the update enqueued for the next render cycle.
Solution
Use a GUID for each start/end data object. uuid is a fantastic package for this and has really good uniqueness guarantees and is incredibly simple to use.
import { v4 as uuidV4 } from 'uuid';
// generate unique id
uuidV4();
To specifically address the issues in your code:
Add id properties to your data
const daysOffList = [
{ id: uuidV4(), start: "12/12", end: "12/15" },
{ id: uuidV4(), start: "12/12", end: "12/17" },
{ id: uuidV4(), start: "12/12", end: "12/19" }
];
...
const addNewHoliday = () => {
const obj = {
id: uuidV4(),
start: "12/12",
end: "12/13",
};
setState(update(state, { daysOffList: { $push: [obj] } }));
};
Update handler to consume id to delete
const deleteHoliday = (id) => {
const objects = state.daysOffList.filter((elm) => elm.id !== id);
setState(update(state, { daysOffList: { $set: objects } }));
};
Use the element id property as the React key
{state.daysOffList?.map((elm, i) => {
return (
<Flex key={elm.id} gap="gap.small">
...
</Flex>
);
})}
Pass the element id to the delete handler
<Button
primary
icon={<TrashCanIcon />}
text
onClick={() => deleteHoliday(elm.id)}
/>
Use an useEffect React hook to log any state update
useEffect(() => {
console.log(state.daysOffList);
}, [state.daysOffList]);
Demo
Note: If you don't want (or can't) install additional 3rd-party dependencies then you can roll your own id generator. This will work in a pinch but you should really go for a real proven solution.
const genId = ((seed = 0) => () => seed++)();
genId(); // 0
genId(); // 1

React Native - VirtualizedList: You have a large list that is slow to update

Introduction
I am having serious performance issues with my VirtualizedList (infinite scroll)... I have been following this to improve it but still working really slow.
When I scroll down, the UI threads go from 60fps to 30fps, and the JS thread from 60fps to 10/20fps (sometimes to 0fps). On iOS (iPhone 6) it seems to go smoothy than on my Android (Samsung Galaxy S8+)
My VirtualizedList is rendering items with different heights, and it fetches the data when its end is reached. My item extends from React.PureComponent, as it is recommended:
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
"contentLength": 20720,
"dt": 532,
"prevDt": 2933,
}
This issue comes when I scroll down around 20 items...
Code
Here is the code of the VirtualizedList:
const keyExtractor = ({ id }) => id;
...
getItem = (data, index) => data[index];
getItemCount = (data) => data.length;
renderItem = ({ item, index }) => {
const {
images,
dimensions,
description,
location,
likes,
comments,
date,
} = item;
return (
<Card
images={images}
postDimensions={dimensions}
description={description}
location={location}
likes={likes}
comments={comments}
date={date}
/>
);
};
<VirtualizedList
data={data}
getItem={this.getItem}
getItemCount={this.getItemCount}
keyExtractor={keyExtractor}
listKey={listKey}
legacyImplementation={false}
numColumns={1}
renderItem={this.renderItem}
initialNumToRender={MAX_POSTS_TO_RETRIEVE_LENGTH} // 10
windowSize={32}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={50}
removeClippedSubviews={false}
ListFooterComponent={this.renderFooter}
ListEmptyComponent={ListEmptyComponent}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
Pd: each item I am rendering has an unique id.
What I have tried
I have tried to implement my own getItemLayout method for the List but for some reason it doesn't work properly.
Warning: Failed frame type: The frame frame.length is marked as required in VirtualizedList.getItemLayout, but its value is undefined.
itemHeights = [];
getItemLayout = (data, index) => {
const length = this.itemHeights[index];
const offset = this.itemHeights.slice(0,index).reduce((a, c) => a + c, 0)
return {length, offset, index}
}
renderItem = ({ item, index }) => {
const {
images,
dimensions,
description,
location,
likes,
comments,
date,
} = item;
return (
<View onLayout={(event) => this.itemHeights[index] = event.nativeEvent.layout.height}>
<Card
images={images}
postDimensions={dimensions}
description={description}
location={location}
likes={likes}
comments={comments}
date={date}
/>
</View>
);
};
<VirtualizedList
data={data}
getItem={this.getItem}
getItemCount={this.getItemCount}
getItemLayout={this.getItemLayout}
keyExtractor={keyExtractor}
listKey={listKey}
legacyImplementation={false}
numColumns={1}
renderItem={this.renderItem}
initialNumToRender={MAX_POSTS_TO_RETRIEVE_LENGTH} // 10
windowSize={32}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={50}
removeClippedSubviews={false}
ListFooterComponent={this.renderFooter}
ListEmptyComponent={ListEmptyComponent}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
Any ideas? I have seen that it is a common problem but I haven't found any solution.
UPDATE
To solve the issue I was getting with getItemLayout just do:
getItemLayout = (data, index) => {
const length = this.itemHeights[index] || 0; // <----- if undefined return 0
const offset = this.itemHeights.slice(0,index).reduce((a, c) => a + c, 0)
return {length, offset, index}
}
If this happen to someone, just use my custom getItemLayout function and follow these steps
Using those values has worked for me.
initialNumToRender={10}
windowSize={5}
maxToRenderPerBatch={5}
updateCellsBatchingPeriod={30}
removeClippedSubviews={false}
...
onEndReachedThreshold={0.1}
Pd: my viewport is covered with 1.5 items

Append new item from database to the top of the list in component's state

I have a database listener on my app that updates my data state every time a user posts something. When I get the new post I update my state called "posts" which is an array with all the items that the FlatList renders.
The problem is that after updating the state I can't see the new post in the top of the list until I go down (to the footer of the list) and then back to the top.
My FlatList code is:
const keyExtractor = ({id}) => id
...
const renderItem = ({ item }) => {
const {
uri,
description,
location,
width,
height,
likes,
comments,
date,
} = item;
return (
<Card
uri={uri}
description={description}
location={location}
width={width}
height={height}
likes={likes}
date={date}
/>
);
};
return (
<FlatList
data={data} // data is the post state of the parent
renderItem={renderItem}
keyExtractor={keyExtractor}
initialNumToRender={15}
windowSize={WINDOW_HEIGHT * 2}
maxToRenderPerBatch={15}
updateCellsBatchingPeriod={50}
removeClippedSubviews={false}
ListFooterComponent={
isLoading ? (
<View style={styles.footer}>
<Loading type="ios" size={18} color={colors.gray} />
</View>
) : null
}
/>
);
}
And this is how I update the post state (in the parent component of the flat list)
function Posts(props) {
const [posts, setPosts] = useState([]);
useEffect(() => {
const { firebase } = props;
let postsArray = [];
// Realtime database listener
const unsuscribe = firebase
.getDatabase()
.collection("posts")
.doc(firebase.getCurrentUser().uid)
.collection("userPosts")
.orderBy("date") // Sorted by upload date
.onSnapshot((snapshot) => {
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
// Get the new post
const newPost = change.doc.data();
// Add the new post to the posts list
postsArray.unshift(newPost);
}
});
// Reversed order so that the last post is at the top of the list
setPosts(postsArray);
});
/* Pd: At the first time, this function will get all the user's posts */
return () => {
// Detach the listening agent
unsuscribe();
};
}, []);
return (
<View style={styles.container}>
<CardList data={posts} />
</View>
);
}
The issue could be caused by the way, you append new posts:
postsArray.unshift(newPost)
..
setPosts(postsArray)
It doesn't seem to affect the reference of postsArray, thus no state updates happen, no re-renders seen.
You may try, instead:
setPosts([...postsArray])
Assign a marker property extraData for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
Your update would be :
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
extraData={/* You need to create another state variable which also changes when a post is added*/}
/>
For more information visit here

React issues with createRef for nested navigation + IntersectionObserver

I have an intersectionObserver that watches some sections and highlights the corresponding navigation item. But I've only managed to get the "main sections Microsoft, Amazon working, but not the subsections Define, Branding, Design, Deduction. As seen in the gif below:
The reason why I want it structured this way is so that I can highlight the "main" sections if the subsections are in view.
Semi working demo: https://codesandbox.io/s/intersection-with-hooks-fri5jun1344-fe03x
It might seems that I might be able to copy and paste the same functionality with the subsections as well. But I'm having a hard time wrapping my head around how to deal with nested data + useRef + reducer. I was wondering if someone could give me a pointer in the right direction.
Here is an gif of the desired effect. Notice the main title (Loupe, Canon) are still highlighted if one of the subsections are in view:
It all starts with an data array
const data = [
{
title: "Microsoft",
id: "microsoft",
color: "#fcf6f5",
year: "2020",
sections: ["define", "branding", "design", "deduction"]
},
{
title: "Amazon",
id: "amazon",
color: "#FFE2DD",
year: "2018",
sections: ["define", "design", "develop", "deduction"]
},
{
title: "Apple",
id: "apple",
color: "#000",
year: "2020",
sections: ["about", "process", "deduction"]
}
];
App.js padding data object into reduce to create Refs
const refs = data.reduce((refsObj, Case) => {
refsObj[Case.id] = React.createRef();
return refsObj;
}, {});
My components passing in the props
<Navigation
data={data}
handleClick={handleClick}
activeCase={activeCase}
/>
{data.map(item => (
<Case
key={item.id}
activeCase={activeCase}
setActiveCase={setActiveCase}
refs={refs}
data={item}
/>
))}
Case.js
export function Case({ data, refs, activeCase, setActiveCase }) {
const components = {
amazon: Amazon,
apple: Apple,
microsoft: Microsoft
};
class DefaultError extends Component {
render() {
return <div>Error, no page found</div>;
}
}
const Tag = components[data.id] || DefaultError;
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.target.id !== activeCase && entry.isIntersecting) {
setActiveCase(entry.target.id);
}
});
}, observerConfig);
observer.observe(refs[data.id].current);
return () => observer.disconnect(); // Clenaup the observer if unmount
}, [activeCase, setActiveCase, refs, data]);
return (
<React.Fragment>
<section
ref={refs[data.id]}
id={data.id}
className="section"
style={{ marginBottom: 400 }}
>
<Tag data={data} />
</section>
</React.Fragment>
);
}
I've tried mapping the subsections like this but I get stuck at this part:
const subRefs = data.map((refsObj, Case) => {
refsObj[Case] = React.createRef();
return refsObj;
}, {});
Working Example
I've found a solution while trying to keep most of your logic intact. Firstly what you need to do is to store the subrefs (the sections ref) in the same object as your Case ref. So you will need an extra reduce function to create those inside the refs object:
App.js
const refs = data.reduce((refsObj, Case) => { // Put this outside the render
const subRefs = Case.sections.reduce((subrefsObj, Section) => {
subrefsObj[Section] = React.createRef();
return subrefsObj;
}, {});
refsObj[Case.id] = {
self: React.createRef(), // self is the Case ref, like Apple, Microsoft...
subRefs // This is going to be the subrefs
};
return refsObj;
}, {});
Then you add an extra state to handle which sub section is active, like const [activeSection, setActiveSection] = React.useState(); And you put it anywhere you also use the activeCase. You need that because you said that the Case and Sections need to work independently. (Both active at the same time).
Case.js
You will need to pass along the subrefs to the child components, so you do:
<Tag data={data} subRefs={refs[data.id].subRefs} />
And you will also need the intersection observer for each of the subrefs. So your useEffect will look like:
useEffect(() => {
const observerConfig = {
rootMargin: "-50% 0px -50% 0px",
threshold: 0
};
const observerCallback = (entries, isCase) => {
const activeEntry = entries.find(entry => entry.isIntersecting);
if (activeEntry) {
if (isCase) setActiveCase(activeEntry.target.id);
else setActiveSection(activeEntry.target.id);
} else if (isCase) {
setActiveCase(null);
setActiveSection(null);
}
};
const caseObserver = new IntersectionObserver(
entries => observerCallback(entries, true),
observerConfig
);
caseObserver.observe(refs[data.id].self.current);
const sectionObserver = new IntersectionObserver(
entries => observerCallback(entries, false),
observerConfig
);
Object.values(refs[data.id].subRefs).forEach(subRef => {
sectionObserver.observe(subRef.current);
});
return () => {
caseObserver.disconnect();
sectionObserver.disconnect();
}; // Clenaup the observer if unmount
}, [refs, data]);
Then in your amazon/index.js ,microsoft/index.js and apple/index.js files. You pass along the ref again:
<Template
data={this.props.data}
caseSections={caseSections}
subRefs={this.props.subRefs}
/>
Finally, in your template.js file you will have the following so you can assign the right ref:
const Template = props => {
return (
<React.Fragment>
<div
sx={{
background: "#eee",
transition: "background ease 0.5s"
}}
>
{props.data.sections &&
props.data.sections.map(subItem => (
<Container
ref={props.subRefs && props.subRefs[subItem]}
id={`${props.data.id}--${subItem}`}
key={subItem}
className="article"
>
<Section sectionId={subItem} caseSections={props.caseSections} />
</Container>
))}
</div>
</React.Fragment>
);
};
I believe most of it is covered in the post. You can check your forked working repo here
You can simplify your code. You don't really need refs or intersectionObservers for your use case. You can simply scrollIntoView using document.getElementById (you already have ids to your navs.
You can do setActiveCase very well in handleClick.
Working demo
Modify handleClick like this
const handleClick = (subTabId, mainTabName) => {
//console.log("subTabName, mainTabName", subTabId, mainTabName);
setActiveCase({ mainTab: mainTabName, subTab: subTabId.split("--")[1] }); //use this for active tab styling etc
document.getElementById(subTabId).scrollIntoView({
behavior: "smooth",
block: "start"
});
};
Navigation.js Call handleClick like this.
{item.sections &&
item.sections.map(subItem => (
<div
className={`${styles.anchor}`}
key={`#${item.title}--${subItem}`}
sx={{ marginRight: 3, fontSize: 0, color: "text" }}
href={`#${item.title}--${subItem}`}
onClick={e => {
handleClick(`${item.id}--${subItem}`, item.id);
e.stopPropagation();
}}
>
{toTitleCase(subItem)}
</div>
))}

Categories