I am trying to add some markers on Map. Here I have added a custom addMarker function. I want to pass myGeoJson param from a buttonClick of another component
export default function Map() {
const mapContainer = useRef(null);
const map = useRef(null);
const [lng] = useState(91.638);
const [lat] = useState(23.735);
const [zoom] = useState(6.5);
const [API_KEY] = useState('YOUR_MAPBOX_API_KEY');
useEffect(() => {
if (map.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: `style.json`,
center: [lng, lat],
zoom: zoom,
maxZoom: 22,
minZoom: 6
});
});
const addMarkers = (myGeoJson) => {
map.current.addSource('points', {
'type': 'geojson',
'data': myGeoJson
});
// Add a symbol layer
map.current.addLayer({
'id': 'points-1',
'type': 'symbol',
'source': 'points',
'layout': {
'icon-image': 'icon-12',
'text-field': ['get', 'title'],
'text-font': [
'roboto',
'MuliBold'
],
'text-offset': [0, 0.5],
'text-anchor': 'top'
}
});
}
return (
<div className="map-wrap">
<div ref={mapContainer} className="map" />
</div>
);
}
Now the second container is
import {addMarkers} from './map'
export const SideBar = props => {
///getting myGeoJson data by calling API
...
const onClick = () => {
addMarkers(myGeoJson);
};
...
return (
<div>
<Button onClisk={onClick}>Draw Markers</Button>
</div>
}
But the addMarkers() can not be accessible from Map()
You are pretty close to your solution. In order to access the addMarkers method from the Map component you should wrap it with a forwardRef.
Doing so can expose some methods to sibling components, like your Button
An working example can be found at code-sandbox
Example:
// Map.js
import { forwardRef, useImperativeHandle } from "react";
export const Map = forwardRef((props, ref) => {
const addMarkers = (myGeoJson) => {
console.log("addMarkers", myGeoJson);
alert("Added marker: " + JSON.stringify(myGeoJson));
};
useImperativeHandle(ref, () => ({
addMarkers
}));
return <div>map</div>;
});
// Button.js
export const Button = ({ onClick }) => {
return (
<div>
<button onClick={onClick}>Draw Markers</button>
</div>
);
};
// App.js
import { Button } from "./Button";
import { Map } from "./Map";
import { useRef } from "react";
export default function App() {
const mapRef = useRef(undefined);
const onClick = () => {
if (mapRef.current === undefined) return; // Map ref is not set yet
mapRef.current.addMarkers({ lat: 123.345, lon: 44.345 });
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Button onClick={onClick} />
<Map ref={mapRef} />
</div>
);
}
Related
I am trying to combine react and fabricjs but I am stuck.
Here is my code
import React, { useState, useEffect, useRef } from 'react';
import { fabric } from "fabric";
function App() {
const [canvas, setCanvas] = useState('');
useEffect(() => {
setCanvas(initCanvas());
}, []);
const initCanvas = () => (
new fabric.Canvas('canvas', {
height: 800,
width: 800,
backgroundColor: 'pink' ,
selection: false,
renderOnAddRemove: true,
})
)
canvas.on("mouse:over", ()=>{
console.log('hello')
})
return (
<div >
<canvas id="canvas" />
</div>
);
}
export default App;
The problem is canvas.on as it causes the error 'Uncaught TypeError: canvas.on is not a function'
Please tell me what am I doing wrong here
During the initial render, your canvas variable is set to your initial state, '' from useState(''). It's not until after this that your useEffect will run, updating the state value.
Recommendation: Move your event handlers into the useEffect and use a ref instead of state for your canvas value. refs have the property of being directly mutable and not requiring a rerender for their new value to be available.
import React, { useState, useEffect, useRef } from 'react';
import { fabric } from "fabric";
function App() {
const canvas = useRef(null);
useEffect(() => {
canvas.current = initCanvas();
canvas.current.on("mouse:over", () => {
console.log('hello')
});
// destroy fabric on unmount
return () => {
canvas.current.dispose();
canvas.current = null;
};
}, []);
const initCanvas = () => (
new fabric.Canvas('canvas', {
height: 800,
width: 800,
backgroundColor: 'pink' ,
selection: false,
renderOnAddRemove: true,
})
);
return (
<div >
<canvas ref={canvas} />
</div>
);
}
export default App;
It's worth noting that if you don't need a reference to the canvas elsewhere in your component, you don't need to use state or a ref and can use a local variable within the useEffect.
useEffect(() => {
const canvas = initCanvas();
canvas.on("mouse:over", () => {
console.log('hello')
});
// destroy fabric on unmount
return () => {
canvas.dispose();
};
})
Actually the problem is that you trying to call canvas.on when it is an empty string in canvas (initial state)
Since we are only need to create fabric.Canvas once, I would recommend to store instance with React.useRef
I created an example for you here:
--> https://codesandbox.io/s/late-cloud-ed5r6q?file=/src/FabricExample.js
Will also show the source of the example component here:
import React from "react";
import { fabric } from "fabric";
const FabricExample = () => {
const fabricRef = React.useRef(null);
const canvasRef = React.useRef(null);
React.useEffect(() => {
const initFabric = () => {
fabricRef.current = new fabric.Canvas(canvasRef.current);
};
const addRectangle = () => {
const rect = new fabric.Rect({
top: 50,
left: 50,
width: 50,
height: 50,
fill: "red"
});
fabricRef.current.add(rect);
};
const disposeFabric = () => {
fabricRef.current.dispose();
};
initFabric();
addRectangle();
return () => {
disposeFabric();
};
}, []);
return <canvas ref={canvasRef} />;
};
export default FabricExample;
I've got a simple example of React Context that uses useMemo to memoize a function and all child components re-render when any are clicked. I've tried several alternatives (commented out) and none work. Please see code at stackblitz and below.
https://stackblitz.com/edit/react-yo4eth
Index.js
import React from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import { GlobalProvider } from "./GlobalState";
function App() {
return (
<GlobalProvider>
<Hello />
</GlobalProvider>
);
}
render(<App />, document.getElementById("root"));
GlobalState.js
import React, {
createContext,useState,useCallback,useMemo
} from "react";
export const GlobalContext = createContext({});
export const GlobalProvider = ({ children }) => {
const [speakerList, setSpeakerList] = useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
]);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakerList((currentState) => {
return currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
});
});
},[]);
// const provider = useMemo(() => {
// return { clickFunction: clickFunction, speakerList: speakerList };
// }, []);
//const provider = { clickFunction: clickFunction, speakerList: speakerList };
const provider = {
clickFunction: useMemo(() => clickFunction,[]),
speakerList: speakerList,
};
return (
<GlobalContext.Provider value={provider}>{children}</GlobalContext.Provider>
);
};
Hello.js
import React, {useContext} from "react";
import Speaker from "./Speaker";
import { GlobalContext } from './GlobalState';
export default () => {
const { speakerList } = useContext(GlobalContext);
return (
<div>
{speakerList.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</div>
);
};
Speaker.js
import React, { useContext } from "react";
import { GlobalContext } from "./GlobalState";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</>
);
});
Couple of problems in your code:
You already have memoized the clickFunction with useCallback, no need to use useMemo hook.
You are consuming the Context in Speaker component. That is what's causing the re-render of all the instances of Speaker component.
Solution:
Since you don't want to pass clickFunction as a prop from Hello component to Speaker component and want to access clickFunction directly in Speaker component, you can create a separate Context for clickFunction.
This will work because extracting clickFunction in a separate Context will allow Speaker component to not consume GlobalContext. When any button is clicked, GlobalContext will be updated, leading to the re-render of all the components consuming the GlobalContext. Since, Speaker component is consuming a separate context that is not updated, it will prevent all instances of Speaker component from re-rendering when any button is clicked.
Demo
const GlobalContext = React.createContext({});
const GlobalProvider = ({ children }) => {
const [speakerList, setSpeakerList] = React.useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true }
]);
return (
<GlobalContext.Provider value={{ speakerList, setSpeakerList }}>
{children}
</GlobalContext.Provider>
);
};
const ClickFuncContext = React.createContext();
const ClickFuncProvider = ({ children }) => {
const { speakerList, setSpeakerList } = React.useContext(GlobalContext);
const clickFunction = React.useCallback(speakerIdClicked => {
setSpeakerList(currentState => {
return currentState.map(rec => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
});
});
}, []);
return (
<ClickFuncContext.Provider value={clickFunction}>
{children}
</ClickFuncContext.Provider>
);
};
const Speaker = React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const clickFunction = React.useContext(ClickFuncContext)
return (
<div>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</div>
);
});
function SpeakerList() {
const { speakerList } = React.useContext(GlobalContext);
return (
<div>
{speakerList.map(rec => {
return (
<Speaker speaker={rec} key={rec.id} />
);
})}
</div>
);
};
function App() {
return (
<GlobalProvider>
<ClickFuncProvider>
<SpeakerList />
</ClickFuncProvider>
</GlobalProvider>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can also see this demo on StackBlitz
this will not work if you access clickFuntion in children from provider because every time you updating state, provider Object will be recreated and if you wrap this object in useMemolike this:
const provider = useMemo(()=>({
clickFunction,
speakerList,
}),[speakerList])
it will be recreated each time clickFunction is fired.
instead you need to pass it as prop to the children like this:
import React, {useContext} from "react";
import Speaker from "./Speaker";
import { GlobalContext } from './GlobalState';
export default () => {
const { speakerList,clickFunction } = useContext(GlobalContext);
return (
<div>
{speakerList.map((rec) => {
return <Speaker speaker={rec} key={rec.id} clickFunction={clickFunction }></Speaker>;
})}
</div>
);
};
and for provider object no need to add useMemo to the function clickFunction it's already wrapped in useCallback equivalent to useMemo(()=>fn,[]):
const provider = {
clickFunction,
speakerList,
}
and for speaker component you don't need global context :
import React from "react";
export default React.memo(({ speaker,clickFunction }) => {
console.log("render")
return (
<>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</>
);
});
https://codesandbox.io/s/react-16-2fss1
import React, { useRef, useEffect } from "react";
import { render } from "react-dom";
import Autosuggest from "react-autosuggest";
const styles = {
fontFamily: "sans-serif",
textAlign: "center"
};
const Wrapper = () => {
const inputRef = useRef();
useEffect(() => {
if (typeof inputRef.current !== "undefined") {
inputRef.current.setAttribute("de-di-var", "");
}
}, []);
return <App ref={inputRef} />;
};
const inputProps = {
placeholder: "",
value: "",
onChange: (event, { newValue }) => {
console.log("change");
},
label: "",
feedback: null,
error: null
};
const App = React.forwardRef(
({ error }, ref) => {
return (
<div style={styles}>
<h4>Reference</h4>
<Autosuggest
inputProps={inputProps}
ref={ref}
suggestions=""
undefined={false}
/>
</div>
);
}
);
render(<Wrapper />, document.getElementById("root"));
Ok, so I am trying to use a ref in my wrapper component and then use the function setAttribute to add an attribute to the input field inside the component Autocomplete, but it won't let me. Is there any reason for this, because I am struggling to figure out why ref.current may be undefined, because that's what I am assuming I get the message error.
There is no such function within AutoSuggest reference.
You can check it by logging the reference:
const Wrapper = () => {
const inputRef = useRef();
useEffect(() => {
console.log(inputRef.current);
console.log(inputRef.current.setAttribute);
// inputRef.current.setAttribute('de-di-var', '');
}, []);
return <App ref={inputRef} />;
};
I keep on getting undefined while trying to access values from the the component.Here is my Provider file content :
import React from "react";
import { FlyToInterpolator } from "react-map-gl";
export const MapContext = React.createContext();
export function MapProvider(props) {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
const onLoad = () => {
setViewport(DRC_MAP);
};
return (
<MapContext.Provider
value={{
viewport,
setViewport,
onLoad
}}
{...props}
/>
);
}
export const { Consumer: MapConsumer } = MapContext;
export const withMap = Component => props => {
return (
<MapConsumer>{value => <Component map={value} {...props} />}</MapConsumer>
);
};
// this is what state gets initialised as
const INITIAL_STATE = {
height: "100vh",
width: "100%",
longitude: 23.071374,
latitude: -3.6116245,
zoom: 1.33
};
const DRC_MAP = {
longitude: 23.656,
latitude: -2.88,
zoom: 4,
transitionDuration: 3000,
transitionInterpolator: new FlyToInterpolator(),
transitionEasing: t => t * (2 - t)
};
So when i try to use the viewport ot any other values defined i get undefined.Here is my Map component that is using the above code.
import React, { useContext } from "react";
import ReactMapGL from "react-map-gl";
import { MapContext } from "./contexts/MapProvider";
const MAPBOX_TOKEN ="secret"
const mapStyle = "mapbox://styles/jlmbaka/cjvf1uy761fo41fp8ksoil15x";
export default function Map() {
const { viewport, setViewport, onLoad } = useContext(MapContext);
return (
<ReactMapGL
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle={mapStyle}
onViewportChange={nextViewport => setViewport(nextViewport)}
onLoad={onLoad}
ref={ref => (window.mapRef = ref && ref.getMap())}
{...viewport}
/>
);
}
I've read several problems which are similar to mine but,none of them are adapted for my case.Here they are :
Context value undefined in React
React context state property is undefined
You made a Context.Provider:
export function MapProvider({ children, ...props }) {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
const onLoad = () => {
setViewport(DRC_MAP);
};
return (
<MapContext.Provider
value={{
viewport,
setViewport,
onLoad
}}
{...props}
>
{children} // <-- Children are consumers
</MapContext.Provider>
);
}
But you didn't consume the context:
// Somewhere in the code you need to consume its context
function Consumer() {
return (
<MapProvider>
<Map />
</MapProvider>
);
}
And then useContext will be valid:
export default function Map() {
// Child of MapContext.Provider,
// so it can consume the context.
const { viewport, setViewport, onLoad } = useContext(MapContext);
...
}
I'm new to react and I'm trying component functional style.
I have simple todo list. I would like to strike out todo item from list using style property. From Chrome debug mode I do not see immediate reaction on checkbox changes, also Item is not striked out... It seams to me, that it is problem with how I manage state of components. I would appreciate some guidance.
App.js
import React, {useState} from 'react';
import Todos from "./components/Todos";
import './App.css'
const App = () => {
const [todos, setTodos] = useState(
[
{id: 1, title: 'Take out the trash', completed: false},
{id: 2, title: 'Dinner with wife', completed: false},
{id: 3, title: 'Meeting boss', completed: false}
]
);
const markComplete = id => {
console.log((new Date()).toString());
todos.map(todo => {
if (todo.id === id) {
todo.completed = ! todo.completed;
}
return todo;
});
setTodos(todos);
};
return (
<div className="App">
<Todos todos={todos} markComplete={markComplete}/>
</div>
);
};
export default App;
Todos.js
import React from "react";
import TodoItem from "./TodoItem";
const Todos = ({todos, markComplete}) => {
return (
todos.map(todo => (
<TodoItem key={todo.id} todoItem={todo} markComplete={markComplete} />
))
);
};
export default Todos;
TodoItem.js
import React from "react";
const TodoItem = ({todoItem, markComplete}) => {
const getStyle = () => {
console.log("style: " + todoItem.completed);
return {
background: '#f4f4f4',
padding: '10px',
borderBottom: '1px #ccc dotted',
textDecoration: todoItem.completed ? 'line-through' : 'none'
}
};
return (
<div style={getStyle()}>
<p>
<input type="checkbox" onChange={markComplete.bind(this, todoItem.id)}/>{' '}
{todoItem.title}
</p>
</div>
);
};
export default TodoItem;
I expect that this getStyle() will follow state... somehow...
Don't mutate state. In markComplete function, you are mutating the todos array directly. Change your function like this to avoid mutation
const markComplete = id => {
console.log((new Date()).toString());
let newTodos = todos.map(todo => {
let newTodo = { ...todo };
if (newTodo.id === id) {
newTodo.completed = !newTodo.completed;
}
return newTodo;
});
setTodos(newTodos);
};
Array.prototype.map() returns a new Array, which you are throwing away. You need to use the new array, e.g.:
const markComplete = id => {
...
setTodos(totos.map(...))