I have a progress bar(react-native-elements) that is animated using useEffect(). This component has MapView(react-native-maps) and it's re-rendered on every tick thus I can't move the map.
How can I still have that progress bar without re-rendering the map?
Home
const Home = ({ navigation }) => {
...some code above...
useEffect(() => {
//setProgressValue(0);
var timer;
if (progressValue != 0) {
timer = setInterval(() => {
if (progressValue <= 1) {
setProgressValue(progressValue + 0.040);
}
else {
setProgressValue(0);
setTimeOver(true);
}
}, 1000);
return () => {
clearInterval(timer);
}
}
}, [progressValue]);
... code below ...
return (
...
<MainMap notification={notification} />
...
)
MainMap component
<View style={StyleSheet.absoluteFillObject}>
<MapView
ref={mapRef}
provider={PROVIDER_GOOGLE}
region={geo.location}
style={[StyleSheet.absoluteFillObject]}
loadingEnabled={true}
minZoomLevel={16}
maxZoomLevel={20}
>
{/* Current users location */}
{geo.location &&
<Marker identifier={'me'} pinColor={"red"} key={(Date.now()-1).toString()} title={"Это я"} coordinate={geo.location} />
}
{/* Location where help is needed */}
{notification &&
<Marker identifier={'target'} pinColor={COLORS.primary} key={Date.now.toString()} title={"Помогите!"} coordinate={notification.request.content.data.loc} />
}
</MapView>
</View>
I used the useMemo hook for the map and passed params that will trigger its rerender making sure that other state changes are not triggering it.
Related
Using the library "# lottiefiles/react-lottie-player"
You need to get lottieRef to interact with animation, but I get null.
Code reference: https://codesandbox.io/s/great-rgb-7dp4j0?file=/src/HorizontalPicker/HorizontalPicker.jsx
import React, {useEffect, useRef} from "react";
import {Player} from "#lottiefiles/react-lottie-player";
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
There is also an interesting point.
If you output lottie, we get an object with null (while there is something inside it)
And if you output lottie.current, we get null.
Reference to an example of this thing: https://ibb.co/RQWxLkJ
Can you pass the lottie ref into your ref like the following (see ref on the div):
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current && lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
This way I am able to get the animation object when I console lottie.current
I'd prefer to use useState to save the animationData rather than a ref. But there was also an issue on the player where the 'load' event was firing but the player hadn't finish setting its internal instance of the animation therefor calling play() wouldn't work. This was happening only on functional components in React that's why it went undiscovered.
I've made a few changes to your code and to the player, using v1.5.2 you should be able to accomplish what you're looking for:
import css from "./HorizontalPicker.module.css";
import React, { useRef, useState, useEffect } from "react";
import { Player } from "#lottiefiles/react-lottie-player";
const HorizontalPicker = () => {
const player = useRef(null);
// const lottie = useRef(null);
const [lottie, setLottie] = useState(null);
useEffect(() => {
if (lottie) {
console.log(" Lottie animation data : ");
console.log(lottie);
// You can also play by calling the underlying lottie method
// lottie.play();
}
}, [lottie]);
return (
<>
<Player
onEvent={(event) => {
// console.log(event);
if (event === "instanceSaved" && player && player.current) {
console.log("Playing animation..");
player.current.play();
}
}}
lottieRef={(data) => {
setLottie(data);
}}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"
}
/>
</>
);
};
export default HorizontalPicker;
Sandbox:
https://codesandbox.io/s/lottie-react-functional-component-2bs8fs?file=/src/HorizontalPicker/HorizontalPicker.jsx
Cheers!
Well, according to official documentation https://github.com/LottieFiles/lottie-react, lottieRef represents a callback function which is fired by Player component (and this function returns AnimationFrame object)
I'm not familiar with this library, and whatever I'll describe next are just my assumptions :) Seems that whenever player "plays", it displays frames one-by-one (from json file in "src" attribute"). And whenever player displays frames from .json file - Player fires "lottieRef" event which you utilize to set lottie.current. And player starts displaying frames only when it loads .json data using "src" parameter in Player definition (see network tab in your browser to ensure that additional http request presents)
And in this case everything seems pretty logical: you try to access "lottie" variable in useEffect of your component but it's empty as far as Player did not manage to display any frame yet because the callback (lottieRef) did not fire yet as far as .json file is not loaded yet. No matter if .json file is large or small, Player requests it via additional http call, which requires some (even minital) amount time. And useEffect fires before .json is loaded immediately after rendering DOM (that's how ReactJS works)
On the other hand, if you delay a bit request to "lottie" ref - you'll see that it is populated:
Code example:
import React, { useEffect, useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
useEffect(() => {
setTimeout(() => console.log(lottie), 50);
}, []);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
So if you delay onEvent callback event a bit, "lottie" ref would be initialized by that moment and .play() would work:
onEvent={(event) => {
event === 'load' &&
setTimeout(
() => lottie.current && lottie.current.play()
);
}
}
BUT, if the only purpose you have is to execute Player whenever it's ready - why not to use "autoplay" built-in property ? (autoplay={true})
import React, { useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={true}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
Hope it'll help
I'm trying to console.log the bounds of a Mapbox map in my React app every time I move around the map.
Here's my current attempt:
return (
<>
<DeckGL
layers={layers}
initialViewState={INITIAL_VIEW_STATE}
controller={true}
onViewStateChange={stateChangeFunc}
>
<ReactMapGL
reuseMaps
mapStyle={mapStyle}
preventStyleDiffing={true}
ref={(ref) => console.log(ref.getMap().getBounds())}
mapboxApiAccessToken={token}
/>
</DeckGL>
</>
);
The bounds are printed when the map loads, but I'm having trouble printing when moving around the map. Is there a prop that I should use to access this functionality? Putting ref={(ref) => console.log(ref.getMap().getBounds())} in DeckGL doesn't work. Is there a prop equivalent to onViewStateChange for ReactMapGL? That might allow me to create a function that prints out the ref.
You can try:
Listen onViewStateChange directly from deck (recommended using some debounce)
Access viewState
Use getBounds method from WebMercatorViewport class
import DeckGL, {WebMercatorViewport} from 'deck.gl';
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, ms);
};
}
function handleViewStateChange({ viewState }) {
const bounds = new WebMercatorViewport(viewState).getBounds();
console.log(bounds);
};
<DeckGL
...
onViewStateChange={debounce(handleViewStateChange, 500)}
/>
I have a component that I trigger via a state object:
const Alerts = () => {
const alert = useStore((state) => state.alert);
const setAlert = useStore((state) => state.setAlert);
const alertsModalRef = useRef(null);
useEffect(() => {
console.log("TRIGGER");
if (alert) {
alertsModalRef.current?.present();
}
}, [alert]);
...
I trigger it with a button click from another component on different screens. For example:
export function UpdateSomething() {
const setAlert = useStore((state) => state.setAlert);
return (
<View>
<Button
title="Continue"
onPress={() => {
setAlert("something");
}}
/>
</View>
);
}
When I load my app and hit a triggering button, I see "TRIGGER" is printed once to the console. However, when I go to another screen and hit another button or even when I then go back to the first screen and hit the same button again, it then prints "TRIGGER" three times.
I suspect that I need to add some kind of cleanup method to the useEffect - but if so, what is the cleanup required there?
And if it's not that? what is going on?
Sorry, I realize this may have been asked in various forms a million times before. I read the docs and many posts, but still confused.
Update 1
As Drew recommended, I tested and saw the Alerts component is re-mounted whenever I visit a screen in my app, which is using React Navigation.
I also wrap each screen with the following Provider:
Navigator
const WrappedSearchScreen = withModalProvider(SearchScreen);
function SearchNavigator() {
const SearchStack = createStackNavigator();
return (
<SearchStack.Navigator>
<SearchStack.Screen
name="Search"
component={WrappedSearchScreen}
/>
...
</SearchStack.Navigator>
);
}
Provider
...
import { Alerts } from "./Alerts";
import { useStore } from "./store";
export const withModalProvider = (Component: FC) => () => {
const progressBarVisible = useStore((state) => state.progressBarVisible);
return (
<BottomSheetModalProvider>
<Component />
<Alerts />
<ProgressBar
indeterminate
color="green"
visible={progressBarVisible}
style={{ position: "absolute", bottom: 0, left: 0 }}
/>
</BottomSheetModalProvider>
);
};
PDFDownloadLink from react-pdf library downloads a pdf when someone clicks on it.
I want to trigger this click event via code based on some condition .
How do I explicitly invoke the click of PDFDownloadLink through code?
A bit late, but you can pass a ref to the render function's return value and use it to call click() on imperatively. For that to work you need to use a separate component wrapper:
const DownloadLink = memo(function () {
const linkRef = useRef(null)
const onLoadingFinished = useCallback(function () {
// When this function is called the first time it is safe to initiate the download
const elem = linkRef?.current
if (elem !== null) {
elem.click()
}
}, [])
return (
<PDFDownloadLink document={<MyDoc />} fileName={'my-file.pdf'}>
{({ blob, url, loading, error }) => (
// You shouldn't call setState() here, so we need to use a separate component to keep track of whether the document has finished rendering
<WorkaroundContainer ref={linkRef} loading={loading} onLoadingFinished={onLoadingFinished} />
)}
</PDFDownloadLink>
)
})
const WorkaroundContainer = forwardRef(function ({ loading, onLoadingFinished }, ref) {
useEffect(() => {
if (!loading) {
onLoadingFinished()
}
}, [loading])
// If you only want to initiate the download imperatively, hide the element via CSS (e.g. `visibility: hidden`)
return (
<div ref={ref}>
{loading ? 'Loading...' : 'Download PDF'}
</div>
)
})
I'm rendering a lot of Text components and saving positions (in order to scroll to them), but I would like to improve performance by "bundling" it by a debounce (and are open for other suggestions that would improve performance).
...
import debounce from 'lodash/debounce'
...
class ContextProvider extends Component {
....
setContentPosition = (layout, key) => {
const contentPos = { ...this.state.contentPos }
contentPos[key] = layout.nativeEvent.layout.y
// TODO: make it debounce https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js
this.setState({ contentPos }, () => console.log('contPos updated'))
}
...
render() {
return (
<Provider
value={{
setContentPosition: this.setContentPosition,
}}
>
{this.props.children}
</Provider>
)
}
}
I have tried a couple of different combinations, without luck. Was expecting this to work:
...
render() {
return (
<Provider
value={{
setContentPosition: debounce(this.setContentPosition, 200),
}}
>
{this.props.children}
</Provider>
)
}
}
It throws the following error:
Update 01
The following change (for contentPos[key])
setContentPosition = (layout, key) => {
const contentPos = { ...this.state.contentPos }
//console.log(layout.nativeEvent)
contentPos[key] = layout.nativeEvent
// TODO: make it debounce https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js
this.setState({ contentPos }, () => {
console.log('contPos updated')
})
}
displays this warning instead:
The position is to be used for scrolling to a text component (when searching) and I have some testing code that scrolls to some Text component - works on iOS but not on Android?