I have a react app, and I have a two routes, /home and /items which renders ListComponent
Items have a bool property scrollHere, which I am using to scroll to a list item automatically and it works well with the code as shown below. The problem that I have is that when I go back to /home then it tries to scroll down to the position that list item was. How can I prevent this behaviour?
export const ListComponent = (items: ItemObject) => {
const itemToScroll = useRef<null | HTMLDivElement>(null);
const mounted = useRef(false);
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
} else {
scrollToItem();
}
});
const scrollToItem = () => {
itemToScroll.current?.scrollIntoView({ behavior: "smooth" });
};
return (
<div>
<ul>
{
items.map(item =>
item.scrollHere ? (<li ref={itemToScroll}>{itemName}</li>) : <></>
}
</ul>
</div>
);
};
Related
I'm using react-navigation v6 and context api, when I navigate from my component to a screen, the context state image which even though wraps my component, becomes undefined. Below is the code flow and relevant code from my components & screens.
EXPECTED OUTPUT -> image context state should persist (as I'm using context provider)
ACTUAL OUTPUT -> image context state becomes undefined when I navigate to different screen.
AppStack
const AppStack = () => {
return (
<Stack.Navigator>
<Stack.Screen name={'AppDrawer'} component={AppDrawer} />
<Stack.Screen name={'StoreStack'} component={StoreStack} />
</Stack.Navigator>
)
}
AppDrawer
const AppDrawer = () => {
return (
<Drawer.Navigator>
<Drawer.Screen name={'AppBotTab'} component={AppBotTab} />
</Drawer.Navigator>
)
}
AppBotTab
const BotTab = createBottomTabNavigator();
const AppBotTab = () => {
return (
<BotTab.Navigator>
<BotTab.Screen name='CameraPreview' component={CameraPreview} />
//... other screens
</BotTab.Navigator>
)
}
CameraPreview -> The Culprit (seems like)
const CameraPreview = ({ navigation }) => {
const isFocused = useIsFocused();
const cameraRef = React.useRef<ExpoCamera>(null);
const [isCameraReady, setIsCameraReady] = useState(false);
const [cameraType, setCameraType] = useState(CameraType.back);
return (
isFocused && (
<Camera
cameraRef={cameraRef}
cameraType={cameraType}
flashMode={flashMode}
onCameraReady={() => setIsCameraReady(true)}>
<MediaContextProvider>
<CameraControls
isCameraReady={isCameraReady}
onToggleCamera={toggleCamera}
setFlashMode={setFlashMode}
cameraRef={cameraRef}
onDismiss={() => navigation?.goBack()}
/>
</MediaContextProvider>
</Camera>
)
);
}
CameraControls
const CameraControls = () => {
const { image, setImageAndUpdateSize } = useMedia();
const [cameraState, setCameraState] = useState('PREVIEW');
useEffect(() => {
console.log('Mounting CAMERA CONTROLS');
return () => {
console.log('image: ', image); // undefined
console.log('Umnounting CAMERA CONTROLS'); // gets called when navigating to StoreStack screen
};
}, []);
const takePhoto = async () => {
if (cameraRef.current) {
const data = await cameraRef.current.takePictureAsync(CAMERA_OPTIONS);
const source = data.uri;
if (source) {
setImageAndUpdateSize({ path: source })
cameraRef.current.pausePreview();
setCameraState('IMAGE_PREVIEW');
}
}
};
switch (cameraState) {
case 'PREVIEW':
return <CameraPreviewControls onTakePhoto={takePhoto} />;
case 'IMAGE_PREVIEW':
if (!image?.path) return null;
return <PhotoPreview imageURI={image?.path} />;
default:
return null;
}
}
PhotoPreview
const PhotoPreview = ({ imageURI }) => {
useEffect(() => {
console.log('Mounting PHOTO REVIEW');
return () => {
console.log('Umnounting PHOTO PREVIEW'); // gets called when navigating to StoreStack screen
};
}, []);
return (
<ImageBackground source={{uri:imageURI}} />
//... other components
<FiltersPanel />
)
}
FiltersPanel
const FiltersPanel = () => {
...
return (
//...
<Gifts />
)
}
Gifts Component
const Gifts = () => {
const navigation = useNavigation();
const goToStore = () => {
navigation.navigate('StoreStack', {
screen: StoreStackPages.StoreScreen,
});
};
return (
<TouchableOpacity onPress={goToStore}>
<Image source={GIFT} resizeMode="contain" style={styles.image} />
</TouchableOpacity>
);
}
MediaContext
const MediaContext = createContext({
// other default values
setImage: () => {},
});
const MediaContextProvider = ({ children }) => {
const [image, setImage] = useState<Partial<PickerImage>>();
// other states
return (
<MediaContextProvider value={{image, setImage}}></MediaContextProvider>
)
}
export const useMedia = () => {
const context = useContext(MediaContext);
const setImageAndUpdateSize = (capturedImage) => {
const {setImage} = context;
return setImage(capturedImage);
}
return {
...context,
// other functions & utilities
setImageAndUpdateSize
}
}
This is the whole flow. So, again, the problem is, in simple words, when I capture a photo in CameraPreviewControls, the image is set successfully and as the cameraState now changes to 'IMAGE_PREVIEW', the PhotoPreview component gets mounted, and I can see the ImageBackground loaded with my context image, now when I tap the GIFT icon (being in the PhotoPreview component), I navigate to StoreStack, BUT NOW when I go back from StoreStack, the context image changes to undefined and the cameraState changes to PREVIEW which shouldn't be the case as I'm still wrapped under MediaContextProvider so the image context state shouldn't be undefined and also when I navigate to StoreStack and comes, the CameraControls and PhotoPreview component gets un-mounted which also shouldn't be the case in react navigation, as according to react-navigation docs, when we navigate to a screen, the previous screen does not get un-mounted.
Can someone shed some light on this, it would be really helpful!!
Thank you in advance!
useIsFocused is triggering a rerender when you navigate away. When you come back the context is rerendered. useIsFocused can be removed to stop the rerender when navigating away.
I am learning React, and trying to build a photo Album with a a modal slider displaying the image clicked (on a different component) in the first place.
To get that, I set <img src={albums[slideIndex].url} /> dynamically and set slideIndex with the idof the imgclicked , so the first image displayed in the modal slider is the one I clicked.
The problem is that before I click in any image albums[slideIndex].urlis obviously undefined and I get a TypeError :cannot read properties of undefined
How could I solve that?
I tried with data checks with ternary operator, like albums ? albums[slideIndex].url : "no data", but doesn't solve it.
Any Ideas? what i am missing?
this is the component where I have the issue:
import React, { useContext, useEffect, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import AlbumImage from "../albumImage/AlbumImage";
import "./album.css";
import BtnSlider from "../carousel/BtnSlider";
function Album() {
const { albums, getData, modal, setModal, clickedImg } =
useContext(AlbumContext);
console.log("clickedImg id >>", clickedImg.id);
useEffect(() => {
getData(); //-> triggers fetch function on render
}, []);
///////////
//* Slider Controls
///////////
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
console.log("SlideINDEx", slideIndex ? slideIndex : "no hay");
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
console.log("nextSlide");
};
const prevSlide = () => {
console.log("PrevSlide");
};
const handleOnclick = () => {
setModal(false);
console.log(modal);
};
return (
<div className="Album_Wrapper">
<div className={modal ? "modal open" : "modal"}>
<div>
<img src={albums[slideIndex].url} alt="" />
<button className="carousel-close-btn" onClick={handleOnclick}>
close modal
</button>
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
</div>
<div className="Album_GridContainer">
{albums &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
</div>
);
}
export default Album;
THis is my AlbumContext :
import React, { createContext, useState } from "react";
export const AlbumContext = createContext();
export const AlbumContextProvider = ({ children }) => {
const [albums, setAlbums] = useState();
const [modal, setModal] = useState(false);
const [clickedImg, setClickedImg] = useState("");
const showImg = (img) => {
setClickedImg(img);
setModal(true);
console.log(clickedImg);
};
const getData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/albums/1/photos"
);
const obj = await response.json();
console.log(obj);
setAlbums(obj);
} catch (error) {
// console.log(error.response.data.error);
console.log(error);
}
};
console.log(`Albums >>>`, albums);
return (
<AlbumContext.Provider
value={{ albums, getData, showImg, modal, setModal, clickedImg }}
>
{children}
</AlbumContext.Provider>
);
};
Thanks very much in advance
Your clickedImg starts out as the empty string:
const [clickedImg, setClickedImg] = useState("");
And in the consumer, you do:
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
So, it takes the value of clickedImg.id on the first render - which is undefined, because strings don't have such properties. As a result, both before and after fetching, slideIndex is undefined, so after fetching:
albums ? albums[slideIndex].url : "no data"
will evaluate to
albums[undefined].url
But albums[undefined] doesn't exist, of course.
You need to figure out what slide index you want to be in state when the fetching finishes - perhaps start it at 0?
const [slideIndex, setSlideIndex] = useState(0);
maybe because your code for checking albums is empty or not is wrong and its always return true condition so change your code to this:
<div className="Album_GridContainer">
{albums.length > 0 &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
change albums to albums.length
I'm using react navigation v5 on the web and when I manually change the URL and refresh I am kept on the same screen instead of navigating to the new screen. I persisted my navigation state so that behavior is expected and every time the navigation state changes, it is set into local storage. When I manually change the url the navigation container does not know that I am trying to navigate thus returns me the previous navigation state. Below, the handlePushToScreen function was experimental and did not work. Does anyone know how to push a route on top when I manually change the url? Also, I've tried using the useLinkTo hook but I cannot use it since I'm outside of the navigation.
App.tsx
useEffect(() => {
const handlePushToScreen = () => {
const route = navigationRef.current?.getCurrentRoute();
const pushAction = StackActions.push(route.name);
const prevRoute = localStorage.getItem('prevRoute');
if (prevRoute !== null && prevRoute !== window.location.pathname) {
navigationRef?.current.dispatch(StackActions.push(route.name));
} else {
localStorage.setItem('prevRoute', window.location.pathname);
}
};
if (isNavMounted) {
handlePushToScreen();
}
}, [window.location.pathname, isNavMounted]);
useEffect(() => {
const restoreState = async () => {
try {
const initialUrl = await Linking.getInitialURL();
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
const state = savedStateString
? JSON.parse(savedStateString)
: undefined;
if (state !== undefined) {
setInitialState(state);
}
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
const handleNavigationChange = (state) => {
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
};
if (!isReady) {
return <Text>loading stuff...</Text>;
}
return (
<NavigationContainer
linking={linking}
initialState={initialState}
onReady={() => setIsNavMounted(true)}
onStateChange={(state) => handleNavigationChange(state)}
ref={navigationRef}
fallback={<Text>Loading…</Text>}>
<MainStackNavigator />
</NavigationContainer>
);
I'm trying to implement infinite scrolling in my private messages page. So when user scroll to top (in the div) it will load more messages.
I have a problem in the first load.
It loads all the messages before scroll is happening.
There is a way to make the scrollbar start from the bottom?
I'm using Redux-Saga to load more messages for conversation component (loadMoreMessages) and the reducer updates messages state.
Here is the code:
const ConversationComp = (props) => {
const {
loadMoreMessages,
messages
} = props;
const ref = useRef(null);
useLayoutEffect(() => {
const elem = ref.current;
if (elem) {
console.log("scrolling to bottom");
elem.scrollTop = elem.scrollHeight;
}
}, []);
useEffect(() => {
console.log("loading more messages");
loadMoreMessages();
}, []);
const onScrollHandler = (e) => {
const elem = e.target;
const threshold = 30;
if (elem.scrollTop < threshold) {
// reached top
loadMoreMessages();
}
};
return (
<div onScroll={onScrollHandler} ref={ref}>
{
messages.map(m => {
...
})
}
</div>
);
}
The desired behavior:
Load first batch of messages
scrollbar is at the bottom
When user scroll to top it loads more messages
Any ideas?
I'm trying to create a functional component that fetches data from an API and renders it to a list. After the data is fetched and rendered I want to check if the URL id and list item is equal, if they are then the list item should be scrolled into view.
Below is my code:
import React, { Fragment, useState, useEffect, useRef } from "react";
export default function ListComponent(props) {
const scrollTarget = useRef();
const [items, setItems] = useState([]);
const [scrollTargetItemId, setScrollTargetItemId] = useState("");
useEffect(() => {
const fetchData = async () => {
let response = await fetch("someurl").then((res) => res.json());
setItems(response);
};
fetchData();
if (props.targetId) {
setScrollTargetItemId(props.targetId)
}
if (scrollTarget.current) {
window.scrollTo(0, scrollTarget.current.offsetTop)
}
}, [props]);
let itemsToRender = [];
itemsToRender = reports.map((report) => {
return (
<li
key={report._id}
ref={item._id === scrollTargetItemId ? scrollTarget : null}
>
{item.payload}
</li>
);
});
return (
<Fragment>
<ul>{itemsToRender}</ul>
</Fragment>
);
}
My problem here is that scrollTarget.current is always undefined. Please advice what I'm doing wrong. Thanks in advance.
Using useCallback, as #yagiro suggested, did the trick!
My code ended up like this:
const scroll = useCallback(node => {
if (node !== null) {
window.scrollTo({
top: node.getBoundingClientRect().top,
behavior: "smooth"
})
}
}, []);
And then I just conditionally set the ref={scroll} on the node that you want to scroll to.
That is because when a reference is changed, it does not cause a re-render.
From React's docs: https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
constructor(props) {
thi.modal = React.createRef();
}
handleSwitch() {
// debugger
this.setState({ errors: [] }, function () {
this.modal.current.openModal('signup') // it will call function of child component of Modal
});
// debugger
}
return(
<>
<button className="login-button" onClick={this.handleSwitch}>Log in with email</button>
<Modal ref={this.modal} />
</>
)
React Hooks will delay the scrolling until the page is ready:
useEffect(() => {
const element = document.getElementById('id')
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [])
If the element is dynamic and based on a variable, add them to the Effect hook:
const [variable, setVariable] = useState()
const id = 'id'
useEffect(() => {
const element = document.getElementById(id)
if (element)
element.scrollIntoView({ behavior: 'smooth' })
}, [variable])