Stop Mapbox.js from re-mounting on every state change? - reactjs

I am running into a particularly painful issue utilising react/redux/mapbox and was hoping to get some advice. I have a container component which houses both a Map component and a destination bar component which shows information about the route. I also need to expose the mapbox instance to the window object when it is available.
The problem I seem to be having is that when I render the Map component, i need to wait for the mapbox.load event, at which point I can then set the map instance to either a useState or useRef for later use in the code. If I set it as state then the map enters into a endless loop where it sets the state, re-loads the map and tries to set the state again. If I set it as a ref, then it wont re-render any of the details on the other component. Also if I were to use the window.map instance at any point it also re-mounts the Map component and starts this whole process off again.
function App() {
const { data, isError } = useConfigEndpointQuery();
const { route, to, from, routingDestinationData, routingOverviewData } =
useAppSelector((state) => state.routing);
const modals = useAppSelector((state) => state.ui.modals);
const mapInstance = useRef<LivingMap>();
const [isUserActive, setIsUserActive] = useState(false);
const [mode, setMode] = useState(ComponentMode.OVERVIEW);
const [isStaticUserLocationChevron, setIsStaticUserLocationChevron] =
useState(false);
const [countdownTimeInSeconds, setCountdownTimeInSeconds] = useState(
INITIAL_COUNTDOWN_TIME / 1000
);
const countdownInterval = useRef<NodeJS.Timer | undefined>();
const userActiveInterval = useRef<NodeJS.Timer | undefined>();
const setChevronInterval = useRef<NodeJS.Timer | undefined>();
const userLocationControl = useRef<UserLocationControl>();
const routingControl = useRef<RoutingControl>();
const geofenceControl = useRef<GeofenceControl>();
const floorControl = useRef<FloorControl>();
return data ? (
<div className={styles.container}>
<TopBar distance={totalLength} time={totalTime} />
<Map
bearing={data.areas[0].view.bearing}
zoom={data.areas[0].view.zoom}
maxZoom={data.areas[0].view.maxZoom}
minZoom={data.areas[0].view.minZoom}
center={data.areas[0].view.center}
extent={data.areas[0].view.extent}
floor={data.floors.find((floor) => !!floor.default)!.universal_id}
floors={data.floors}
mapStyle={`${getUrl(URLTypes.STYLES)}/styles.json`}
accessToken={process.env.REACT_APP_MAPBOX_TOKEN as string}
onMapReady={(map) => {
mapInstance.current = map;
userLocationControl.current = map.getPluginById<UserLocationControl>(
PLUGIN_IDS.USER_LOCATION
);
routingControl.current = map.getPluginById<RoutingControl>(
PLUGIN_IDS.ROUTING
);
geofenceControl.current = map.getPluginById<GeofenceControl>(
PLUGIN_IDS.GEOFENCE
);
floorControl.current = map.getPluginById<FloorControl>(
PLUGIN_IDS.FLOOR
);
}}
/>
</div>
) : isError ? (
<div>Error loading config</div>
) : null;
}
Is there a way by which I can keep a single instance of a mapbox map while also reading/writing data to the redux store and interacting with the map instance without causing the component to re-mount the map component each time?

It appears it was re-rendering needlessly as I was passing a function into the map component without wrapping it in useCallback()

Related

How to prevent components from refreshing when state in context is changed (React)

Im shooting my shot at making a tiktok clone as a first project to learn React. I want to have a global isVideoMuted state. When you toggle it, it should mute or unmute all sound of all videos.
Except something is not working properly. I understand that react re-renders everything when you change one thing within the contextprovider parent component from a childcomponent. This resets the tiktok "scrollprogress" to zero, since its a simple vertical slider. Is there anyway I can prevent this from happening?
This is my VideoContextProvider:
const VideoContextProvider = ({ children }: any) => {
const [isVideoMuted, setIsVideoMuted] = useState(true);
const [videos, setVideos] = useState([] as Video[]);
return (
<VideoContext.Provider
value={{
videos,
setVideos,
isVideoMuted,
setIsVideoMuted,
}}
>
{children}
</VideoContext.Provider>
);
};
And this is the VideoCard.tsx (one single video):
const VideoCard: FC<Props> = ({ video }) => {
const router = useRouter();
const [isLiked, setIsLiked] = useState(false);
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext);
return (
.... (all the remaining videocode is here, including the <video>)
<Box p="xs">
{isVideoMuted ? (
<VscMute
fontSize="28px"
color="white"
onClick={() => setIsVideoMuted(false)}
/>
) : (
<VscUnmute
fontSize="28px"
color="white"
onClick={() => setIsVideoMuted(true)}
/>
)}
</Box>
...
);
};
export default VideoCard;
// export const VideoCardMemoized = memo(VideoCard);
)
See this video for an example of the bug: https://streamable.com/8ljkhl
Thanks!
Edit:
What I've tried so far:
Making a memoized version of the VideoCard and using that, refreshing still occurs
Moving const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext); to a seperate component (VideoSidebar). Problem still occurs, since I have to 'subscribe' to the isVideoMuted variable in the video element
It rerenders only components that are subscribed to that context.
const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext);
To prevent rerendering child components of the subscribed components you can use React.memo
export default React.memo(/* your component */)

Update only necessary states in useContext hook

In the following example, a click on the first button re-renders both of the child components (ComponentOne and ComponentTwo), although the state of the second number does not change (because the whole useContext array value changes). I would like to know how we can detect multiple individual values, or any other method to only render states that changed.
const TwoNumbers = createContext();
function ComponentOne(){
const number = useContext(TwoNumbers)[0];
return(
<div>{number}</div>
)
}
function ComponentTwo(){
const number = useContext(TwoNumbers)[1];
return(
<div>{number}</div>
)
}
function App() {
const [numberOne, setNumberOne] = useState(0);
const [numberTwo, setNumberTwo] = useState(0);
return (
<TwoNumbers.Provider value={[numberOne, numberTwo]}>
<div className="App">
<ComponentOne/>
<ComponentTwo/>
<button onClick = {()=>{setNumberOne(numberOne+1)}}>First+1</button>
<button onClick = {()=>{setNumberTwo(numberTwo+1)}}>Second+1</button>
</div>
</TwoNumbers.Provider>
);
}
In real scenario the two numbers will represent the view count and other real-time data I would like to update after the backend updated. I see Stack Overflow is not doing this. Is real-time state matching an expensive move? Does it extensively add more time for page loading?
One solution is to create two Contexts and use two context providers. Then each of the child components (ComponentOne and ComponentTwo) consumes its own respective context. Finally in order to prevent a child component from being updated by its parent (App) due to the changes on the context value of the other sibling component, you can use React.memo.
const ContextNumberOne = createContext();
const ContextNumberTwo = createContext();
React.memo(function ComponentOne(){
const number = useContext(ContextNumberOne);
return(
<div>{number}</div>
)
})
React.memo(function ComponentTwo(){
const number = useContext(ContextNumberTwo);
return(
<div>{number}</div>
)
})
function App() {
const [numberOne, setNumberOne] = useState(0);
const [numberTwo, setNumberTwo] = useState(0);
return (
<ContextNumberOne.Provider value={numberOne}>
<ContextNumberTwo.Provider value={numberTwo}>
<div className="App">
<ComponentOne/>
<ComponentTwo/>
<button onClick = {()=>{setNumberOne(numberOne+1)}}>First+1</button>
<button onClick = {()=>{setNumberTwo(numberTwo+1)}}>Second+1</button>
</div>
</ContextNumberTwo.Provider>
</ContextNumberOne.Provider>
);
}
Look into using Redux instead, it specially solves this problem. You can use useSelector to subscribe to the particular bit of state that you care about, and only rerender when that changes.

Sorting data from an API (redux-toolkit)

I'm building a crypto app with react and redux-toolkit.
I'm trying to find a way to manage the data from the API. More specifically i want to be able to sort by value, volume, etc. and add an "isFavourite" property for each coin but i think (correct me if i'm wrong) that the only way to do this is by copying the data to another state. What i've tried so far was adding another state where i passed the data like this:
const [list, setList] = useState()
useEffect(() => {
setList(data)
}, [data])
//"const coinData = list?.data?.coins" instead of "const coinData = data?.data?.coins"
but then an error occured because the data on the "list" were "undefined".
The code bellow is the one that is running without any problems. How can i manage the API data? Am i on the right path or is there a more slick way to do what i want? Thank you!
function Main () {
const { data, error, isFetching } = useGetCryptosQuery()
if(isFetching) return 'Loading...'
const globalStats = data?.data?.stats
const coinData = data?.data?.coins
const coinList = coinData.map(coin => {
return (
<Coin
price = {coin.price}
key = {coin.uuid}
id = {coin.uuid}
name = {coin.name}
icon = {coin.iconUrl}
/>)
})
return (
<div>
<h2>Main</h2>
{coinList}
</div>
)
}
export default Main
You are on the right track - I set up something similar and added a check for null trying to map the data, and that avoids the error you probably got.
const coinList = coinData ? coinData.map((coin) => {
///...coin component
}) : <div></div>;
Then, instead of an error for undefined data, it will return an empty div - until the data is there, then it will render ok.

Prevent context.consumer from re-rendering component

I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.

Cant use values from (fetched) context(provider) as input to my useState

Short version:
I have a useEffect in a ContextProvider where I fetch some data from server. In my component I
use this Context and want to init another useState()-hook with the "incoming" data. The value in
[value, setValue] = useState(dataFromContext) does not get set/inited with dataFromContext. Why, oh why?
The very long version:
Im trying to follow (and expand on) this example of how to use context/useContext-hook in react
So, I have an AppContext and a ContextProvider, I wrap my in in index.js, everything in the example works nice.
Now I want to load data in useEffect inside my ContextProvider, so my context provider now looks like this
const AppContextProvider = ({ children }) => {
const profileInfoUrl = BASE_URL + `users/GetCurrentUsersProfileInfo/`
const {loading, getTokenSilently} = useAuth0();
const [profileInfo, setProfileInfo] = useState( {})
useEffect(() => {
if (!loading && profileInfo.user.username === '' ) {
Communication.get(profileInfoUrl, getTokenSilently, setProfileInfo);
}
},[loading, getTokenSilently])
const context = {
profileInfo,
setProfileInfo
};
return (
<AppContext.Provider value={ context }>
{children}
</AppContext.Provider>
);
}
Now, in my component, I want to display this data. One of them is users selected categories, a list of an id and isSelected, which should feed a checkboxlist.
const TsProfileSettingsPage = ( ) => {
//..
const { profileInfo, setProfileInfo } = useContext(AppContext);
const userCategories = Object.assign({}, ...profileInfo.userDetails.categories.map((c) => ({[c.id]: c})));
const [checkedCategories, setCheckedCategories] = useState(userCategories);
console.log("profileInfo.userDetails.categories : " + JSON.stringify(profileInfo.userDetails.categories));
console.log("userCategories : " + JSON.stringify(userCategories));//correct dictionary
console.log("checkedCategories : " + JSON.stringify(checkedCategories)); //empty object!
//...rest of code
So I load users selection and converts is to a dictionary that ends up in "userCategories". I use that "userCategories" to init checkedCategories. BUT, from the console.logs below, userCategories is set correctly, but checkedCategories is not, its becomes an empty object!
So my question is. Am I thinking wrong here? (obviously I am), I just want to have an external state.
Everything loads ok, but when I use it to init a useState() in my component it does not get set. I have tested to have another useEffect() in my component, but I just get the feeling that Im doing something more basic error.
Thanks for any help

Resources