updating props when using createControlComponent in react-leaflet 3 - javascript

Following the offical reference for Higher Level Component Factory to update props for a Control Component
The core APIs export other high-level component factories that can be
used in a similar way.
I've mimicked the example - but I get a syntax error for the following:
import L from "leaflet";
import "leaflet-routing-machine";
import { createControlComponent } from "#react-leaflet/core";
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'
function setWaypoints(props)
{
return {
waypoints: [
L.latLng(props.startLat, props.startLng),
L.latLng(props.endLat, props.endLng)
],
lineOptions: {
styles: [{ color: "#0500EE", weight: 4 }]
},
show: false,
addWaypoints: false,
routeWhileDragging: true,
draggableWaypoints: true,
fitSelectedRoutes: true,
showAlternatives: false,
createMarker: function() { return null; },
}
}
function createRoutingMachine(props, context)
{
const instance = new L.Routing.control(setWaypoints(props))
return
{
instance, context: { ...context, overlayContainer: instance }
}
}
function updateRoutingMachine(instance, props, prevProps)
{
if (props.endLat !== prevProps.endLat || props.endLng !== prevProps.endLng)
{
instance.setWaypoints(props)
}
}
const RoutingMachine = createControlComponent(createRoutingMachine, updateRoutingMachine)
export default RoutingMachine;
Missing semicolon. (35:25)
33 | return 34 | {
35 | instance, context: { ...context, overlayContainer: instance }
| ^ 36 | }
If I change this to:
function createRoutingMachine(props)
{
const instance = new L.Routing.control(setWaypoints(props))
return instance
}
The compiler is happy, but the component never updates.
I know I'm creating the Control Component incorrectly, but I can't find the information for the correct implementation.
Related:
How to use Leaflet Routing Machine with React-Leaflet 3?
How to extend TileLayer component in react-leaflet v3?

You will notice that in the docs, createcontrolcomponent lists only one argument, which is a function to create the instance. You are expecting it to behave like createlayercomponent, which takes two arguments. In createlayercomponent, the second argument is a function to update the layer component when the props change. However, createcontrolcomponent offers no such functionality. react-leaflet is assuming, much like vanilla leaflet, that once your control is added to the map, you won't need to alter it directly.
This gets a bit confusing in terms of leaflet-routing-machine, because you don't need to change the instance of the control, but rather you need to call a method on it which affects the map presentation.
IMO, the best way to go is to use a state variable to keep track of whether or not your waypoints have changed, and use a ref to access the underlying leaflet instance of the routing machine, and call setWayPoints on that:
// RoutineMachine.jsx
const createRoutineMachineLayer = (props) => {
const { waypoints } = props;
const instance = L.Routing.control({
waypoints,
...otherOptions
});
return instance;
};
// Takes only 1 argument:
const RoutingMachine = createControlComponent(createRoutineMachineLayer);
// Map.jsx
const Map = (props) => {
// create a ref
const rMachine = useRef();
// create some state variable, any state variable, to track changes
const [points, setPoints] = useState(true);
const pointsToUse = points ? points1 : points2;
// useEffect which responds to changes in waypoints state variable
useEffect(() => {
if (rMachine.current) {
rMachine.current.setWaypoints(pointsToUse);
}
}, [pointsToUse, rMachine]);
return (
<MapContainer {...props}>
<RoutineMachine ref={rMachine} waypoints={pointsToUse} />
<button onClick={() => setPoints(!points)}>
Toggle Points State and Props
</button>
</MapContainer>
);
};
Working Codesandbox
Bonus: a cheap and easy way to force a rerender on your <RoutineMachine> component (or any react component) is to assign it a key prop, and change that key prop when you want to rerender it. This might be a uuid, or even a unique set of waypoints ran through JSON.stringify. Just an idea.

Related

How to handle useReducer dispatch on props change?

At the outset I'd like to say that I'm a React beginner and I'm stuck! And I've no idea how to proceed.
I'll be very happy if someone can either help me get my current code working or suggest an alternate approach.
So the most simple background is that I have a React app running on Electron framework to build a desktop application that reads in a JSON file and performs various tasks which includes displaying trees and graphs from the JSON data. To show the graph I have a multiple Menu components which allow the user to select what they want to see. The current state of the Menus are maintained by a component called the MainPane which uses a custom hook called useMenuState for maintaining the menu state (this hook uses only useState).
The values from useMenuState hook are passed by the MainPane as props to a component called the TreeViewer which uses another custom hook called useTreeState which uses a useReducer hook (to generate current tree using the information provided in the props). The custom hook returns the tree data and dispatcher function provided by the reducer hook.
To be able to show a different tree when user selects a new menu option, I have added a useEffect hook in the TreeViewer component which calls the dispatch function.
So I've added the values from the prop in the dependancy array of the useEffect function.
But the effect function is not running when the props change? As far as I can tell, all props being passed are created as new objects and no object state is mutated.
Here is a simple representation of my code (it has all the bits I talked about).
// CUSTOM HOOK useMenuState
const useMenuState = (props) => {
const [menu1State, updateMenu1] = useState();
const [menu2State, updateMenu2] = useState();
const handleMenuClick = (pMenuIdx, pClickedVal) = {
// generate and update respective menu state value using the jsonObj in props
};
let menuState = {
menu1: menu1State,
menu2: menu2State
};
return { menuState, menuClickHandler:handleMenuClick};
}
// COMPONENT MainPane
const MainPane = (props) => {
// props here gets a object that manages the data in the JSON file
const {menuState, menuClickHandler} = useMenuState({...props});
let treeViewerProps = {
treeType: menuState.menu1,
treeID: menuState.menu2,
jsonObj: props.jsonObj
};
return (<TreeViewer {...treeViewerProps} />);
}
// CUSTOM HOOK useTreeState
const useTreeState = (props) => {
const updaterFunction = (currentTreeState, action) => {
// update tree state as per action and return
}
let initTreeState = {} // build initial state of the tree
const [treeObj, treeUpdateDispatcher] = useReducer(updaterFunction, initTreeState);
let treeStateObj = { treeData: treeObj, treeUpdater: treeUpdateDispatcher };
return treeStateObj;
}
// COMPONENT TreeViewer
const TreeViewer = (props) => {
let treeStateProps = {
jsonObj: props.jsonObj,
treeType: props.treeType,
treeID: props.treeID
}
useEffect(()=>{
treeUpdater({type:'NEW TREE', id: props.treeID})
}, [props.treeType, props.treeID]); // look for changes in the tree type or ID and call the dispatch function
const { treeData, treeUpdater } = useTreeState(treeStateProps); // custom hook to handle tree State
return (<Tree data={treeData} />);
}
Why isn't the useEffect hook running when the props change?
Is there a better way to handle this?
Any pointers will be hugely appreciated!
Thanks in advance!

Coordinates array from PolylineMeasure plugin (react leaflet)

I have 3 files:
1.
PolylineMeasure.jsx
import { MapControl, withLeaflet } from "react-leaflet";
import * as L from "leaflet";
class PolylineMeasure extends MapControl {
createLeafletElement() {
return L.control.polylineMeasure({
position: "topleft",
unit: "metres",
showBearings: true,
clearMeasurementsOnStop: false,
showClearControl: true,
showUnitControl: true,
});
}
componentDidMount() {
const { map } = this.props.leaflet;
const polylineMeasure = this.leafletElement;
polylineMeasure.addTo(map);
}
}
export default withLeaflet(PolylineMeasure);
Map.jsx
import { Map, TileLayer } from "react-leaflet";
import PolylineMeasure from "./PolylineMeasure";
import "leaflet/dist/leaflet.css";
import "leaflet/dist/leaflet.css";
import "leaflet.polylinemeasure/Leaflet.PolylineMeasure.css";
import "leaflet.polylinemeasure/Leaflet.PolylineMeasure";
const Leaflet = () => {
return (
<>
<Map
center={[52.11, 19.21]}
zoom={6}
scrollWheelZoom={true}
style={{ height: 600, width: "50%" }}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<PolylineMeasure />
</Map>
</>
);
};
export default Leaflet;
I'm using nextjs so I had to import without SSR.
home.js
import dynamic from "next/dynamic";
function HomePage() {
const Map = dynamic(() => import("../components/Map"), {
loading: () => <p>A map is loading</p>,
ssr: false,
});
return <Map />;
}
export default HomePage;
https://github.com/ppete2/Leaflet.PolylineMeasure
Using demos in link above, I was able to log an array of coorfinates like this:
{ ... }
polylineMeasure.addTo(map);
function debugevent() {
polylineMeasure._arrPolylines[0].arrowMarkers.map((el) => {
console.log(el._latlng);
});
}
map.on("polylinemeasure:toggle", debugevent);
How can I access these coordinates in nextjs (home.js file)?
How to render PolylineMeasure (Map.jsx file) already with coordinates by passing down an array as props?
So this is about 2 things: lifting up state, and capturing Leaflet.Polyline's internal events.
First, let's keep track of a state variable in Home.js, and pass its setter down into the map component:
function HomePage() {
const [pointarray, setPointarray] = useState()
const Map = dynamic(() => import("../components/Map"), {...})
return <Map setPointarray={setPointarray} />;
}
Now in Map, we need to get a reference to the underlying leaflet map so that we can attach some event handlers. You're using createLeafletElement and withLeaflet, so I assume you're using reat-leaflet version 2. (I recommend updating to v3 when you can).
const Leaflet = ({ setPointarray }) => {
const mapRef = React.useRef()
useEffect(() => {
if (mapRef && mapRef.current){
mapRef.current.leafletElement.on(
'polylinemeasure:finish',
currentLine => setPointarray(currentLine.getLatLngs())
)
}
}, [mapRef])
return (
<>
<Map
ref={mapRef}
...
>
<TileLayer ... />
<PolylineMeasure />
</Map>
</>
);
};
What happens here is that a ref is attached to your Map component, which references the underlying leaflet L.map instance. When that ref is ready, the code inside the useEffect if statement runs. It gets the map instance from mapRef.current.leafletElement, and attaches an event handler based on Leaflet.PolylineMeasure's events, specifically the event of when a drawing is complete. When that happens, it saves the drawn line to the state variable, which lives in the Home component.
There are a lot of variations on this, it just depends on what you're trying to do exactly. As far as feeding preexisting polyline coordinates down to PolylineMeasurer as props, I couldn't find any examples of that even with the vanilla leaflet PolylineMeasurer. I found a comment from the plugin author saying that "restoring of drawed measurements is not possible", which is essentially what we're talking about doing by passing props down to that component. I'm sure it can be done by digging into the source code and programmatically drawing a polyline, but I've run out of time, I'll try to revisit that later.
react-leaflet version 3 answer
As per request, here's how to do this with react-leaflet v3, while initializing the polylinemeasurer with data passed down as props.
Create custom react-leaflet v3 control
Creating custom components with react-leaflet is easier than ever. Take a look at createcontrolcomponent. If you're not used to reading these docs, it boils down to this: to create a custom control component, you need to make a function that returns the leaflet instance of the control you want to make. You feed that function to createcontrolcomponent, and that's it:
import { createControlComponent } from "#react-leaflet/core";
const createPolylineMeasurer = (props) => {
return L.control.polylineMeasure({ ...props });
};
const PolylineMeasurer = createControlComponent(createPolylineMeasurer);
export default PolylineMeasurer;
Altering the original plugin to seed data
However, in our case, we want to add some extra logic to pre-seed the PolylineMeasurer with some latlngs that we pass down as a prop. I put in a pull request to the original plugin to add a .seed method. However, in the case of react-leaflet, we need to be more careful than using the code I put there. A lot of the methods required to draw polylines are only available after the L.Control.PolylineMeasure has been added to the map. I spent probably way too much time trying to figure out where in the react/react-leaflet lifecyle to intercept the instance of the polylineMeasure after it had been added to the map, so my eventual solution was to alter the source code of Leaflet.PolylineMeasure.
In the onAdd method, after all the code has run, we add in this code, which says that if you use a seedData option, it will draw that seed data once the control is added to the map:
// inside L.Control.PolylineMeasure.onAdd:
onAdd: function(map) {
// ... all original Leaflet.PolylineMeasure code here ...
if (this.options.seedData) {
const { seedData } = this.options;
seedData.forEach((polyline) => {
// toggle draw state on:
this._toggleMeasure();
// start line with first point of each polyline
this._startLine(polyline[0]);
// add subsequent points:
polyline.forEach((point, ind) => {
const latLng = L.latLng(point);
this._mouseMove({ latLng });
this._currentLine.addPoint(latLng);
// on last point,
if (ind === polyline.length - 1) {
this._finishPolylinePath();
this._toggleMeasure();
}
});
});
}
return this._container;
}
This code programatically calls all the same events that would be called if a user turned on the control, clicked around, and drew their lines that way.
Tying it together
So now our <PolylineMeasurer /> component takes as its props the options that would be fed to L.control.polylineMeasure, in addition to a new optional prop called seedData which will cause the map to be rendered with that seedData:
const Map = () => {
return (
<MapContainer {...mapContainerProps}>
<TileLayer url={url} />
<PolylineMeasurer
position="topleft"
clearMeasurementsOnStop={false}
seedData={seedData}
/>
</MapContainer>
);
};
Working Codesandbox
Caveat
If by some other mechanism in your app the seedData changes, you cannot expect the PolylineMeasurer component to react in the same way that normal React components do. In create leaflet, this control is added to the map once with the options you feed it, and that's it. While some react-leaflet-v3 component factory functions come with an update paramter, createcontrolcomponent does not (i.e. its first argument is a function which creates a control instance, but it does not accept a second argument to potentially update the control instance like, say, createlayercomponent does).
That being said, you can apply a key prop to the PolylineMeasurer component, and if your seedData is changed somewhere else in the app, also change the key, and the PolylineMeasurer will be forced to rerender and draw your new data.

How can I update every React components using a context without provider?

Given this simple custom hook
import React, { createContext, useContext } from 'react';
const context = {
__prefs: JSON.parse(localStorage.getItem('localPreferences') || null) || {} ,
get(key, defaultValue = null) {
return this.__prefs[key] || defaultValue;
},
set(key, value) {
this.__prefs[key] = value;
localStorage.setItem('localPreferences', JSON.stringify(this.__prefs))
}
};
const LocalPreferenceContext = createContext(context);
export const useLocalPreferences = () => useContext(LocalPreferenceContext);
export const withLocalPreferences = Component => () => <Component localPreferences={ useLocalPreferences() } />;
When I use either of these, calling set on the context does not update anything. Sure, how React would know that I have updated anything? But what could be done to make it work (excluding using a Provider)?
** Edit **
Ok, so what is the alternative other than using useContext then? That's the real question, really; how do I update the components using this hook (or HOC)? Is useState the only way? How? Using some event emitter?
I think using context does make sense here, but you will need to use a provider, as that's a core part of how context works. Rendering a provider makes a value available to components farther down the tree, and rendering with a new value is what prompts the consumers to rerender. If there's no provider than you can at least get access to a default value (which is what you have in your code), but the default never changes, so react has nothing to notify the consumers about.
So my recommendation would be to add in a component with a provider that manages the interactions with local storage. Something like:
const LocalPreferenceProvider = () => {
const [prefs, setPrefs] = useState(
() => JSON.parse(localStorage.getItem("localPreferences") || null) || {}
);
// Memoized so that it we don't create a new object every time that
// LocalPreferenceProvider renders, which would cause consumers to
// rerender too.
const providedValue = useMemo(() => {
return {
get(key, defaultValue = null) {
return prefs[key] || defaultValue;
},
set(key, value) {
setPrefs((prev) => {
const newPrefs = {
...prev,
[key]: value,
};
localStorage.setItem("localPreferences", JSON.stringify(newPrefs));
return newPrefs;
});
},
};
}, [prefs]);
return (
<LocalPreferenceContext.Provider value={providedValue}>
{children}
</LocalPreferenceContext.Provider>
);
};
You mentioned in the comments that you wanted to avoid having a bunch of nested components, and you already have a big stack of providers. That is something that will often happen as the app grows in size. Personally, my solution to this is to just extract the providers into their own component, then use that component in my main component (something like<AllTheProviders>{children}</AllTheProviders>). Admittedly this is just an "out of sight, out of mind" solution, but that's all i really tend to care about for this case.
If you do want to completely get away from using providers, then you'll need to get away from using context too. It may be possible to set up a global object which is also an event emitter, and then have any components that want to get access to that object subscribe to the events.
The following code is incomplete, but maybe something like this:
const subscribers = [];
let value = 'default';
const globalObject = {
subscribe: (listener) => {
// add the listener to an array
subscribers.push(listener);
// TODO: return an unsubscribe function which removes them from the array
},
set: (newValue) {
value = newValue;
this.subscribers.forEach(subscriber => {
subscriber(value);
});
},
get: () => value
}
export const useLocalPreferences = () => {
let [value, setValue] = useState(globalObject.get);
useEffect(() => {
const unsubscribe = globalObject.subscribe(setValue);
return unsubscribe;
}, []);
return [value, globalObject.set];
})
You could pull in a pub/sub library if you don't want to implement it yourself, or if this is turning into to much of a project, you could use an existing global state management library like Redux or MobX

Creating a var referencing a store from redux

I am currently working on creating a var that references a store from redux. I created one but within the render(). I want to avoid that and have it called outside of the render. Here is an example of it. I was recommended on using componentWillMount(), but I am not sure how to use it. Here is a snippet of the code I implemented. Note: It works, but only when I render the data. I am using double JSON.parse since they are strings with \
render() {
var busData= store.getState().bus.bus;
var driverData= store.getState().driver.gdriveras;
var dataReady = false;
if (busData&& driverData) {
dataReady = true;
console.log("========Parsing bus data waterout========");
var bus_data_json = JSON.parse(JSON.parse(busData));
console.log(bus_data_json);
console.log("========Parsing driver data waterout========");
var driver_data_json = JSON.parse(JSON.parse(driverData));
console.log(driver_datat_json);
busDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
driverDatajson.forEach(elem => {
elem.time = getFormattedDate(elem.time)
});
...
}
}
Here is an example of react-redux usage that will probably help you.
Don't forget to add StoreProvider to your top three component (often named App).
I warned you about the fact that React and Redux are not meant to be used by beginner javascript developer. You should consider learn about immutability and functional programming.
// ----
const driverReducer = (state, action) => {
switch (action.type) {
// ...
case 'SET_BUS': // I assume the action type
return {
...state,
gdriveras: JSON.parse(action.gdriveras) // parse your data here or even better: when you get the response
}
// ...
}
}
// same for busReducer (or where you get the bus HTTP response)
// you can also format your time properties when you get the HTTP response
// In some other file (YourComponent.js)
class YourComponent extends Component {
render() {
const {
bus,
drivers
} = this.props
if (!bus || !drivers) {
return 'loading...'
}
const formatedBus = bus.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
const formatedDrivers = drivers.map(item => ({
...item,
time: getFormattedDate(item.time)
}))
// return children
}
}
// this add bus & drivers as props to your component
const mapStateToProps = state => ({
bus: state.bus.bus,
drivers: state.driver.gdriveras
})
export default connect(mapStateToProps)(YourComponent)
// you have to add StoreProvider from react-redux, otherwise connect will not be aware of your store

Why isn't `useContext` re-rendering my component?

As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode

Categories

Resources