I'm trying to create a bus animations from an array of coordinates, i'm using setTimeout to fire function to move marker to next coordinate, but i need to stop execution when user chooses another station from parent component, so when user clicks in another station, i firstly set routes to Null and fetch new coordinates for the buses of this particular station, which represents routes in the code below
The problem :
functions continue firing even after clearing timeouts and unmounting component.
I don't know what i'm doing wrong
here is my code
Bus marker component :
import React from 'react';
import { Marker } from 'react-native-yamap';
import { BusView } from '../../../../../../components/transport/bus-view';
import { Route } from '../../../../../../modules/transport/types/Route';
import { Station } from 'modules/transport/types/Station';
interface Props {
route: Route;
visible: boolean;
selectedStation: Station | null;
routes: Route[];
}
export const BusMarker = React.memo(function BusMarker(props: Props) {
const name = props.route?.bus_number?.replace(/\D/g, '');
const busRef = React.useRef<Marker>(null);
const [timeouts, setTimeOuts] = React.useState<NodeJS.Timeout[]>([]);
let timerRef = React.createRef();
const animateMarker = React.useCallback(() => {
const current = props?.route.current_location ?? 0;
let prevTimeOuts: NodeJS.Timeout[] = [];
console.log('animation start for marker',
props.route.bus_number, 'from position :',
current);
props?.route?.route?.slice(current + 1)?.forEach((bus, index) => {
timerRef.current = setTimeout(() => {
console.log('animateMarker', props.route.bus_number, 'to position index:', index);
busRef?.current?.animatedMoveTo(
{
lat: bus.lat,
lon: bus.lon,
},
1000 * props.route.average_second_per_coordinate,
);
}, index * 1000 * props.route.average_second_per_coordinate);
prevTimeOuts = [...prevTimeOuts, timerRef.current];
});
setTimeOuts(prevTimeOuts);
}, [props.selectedStation]);
const clearAllTimeouts = React.useCallback(() => {
console.log('clearAllTimeouts');
timeouts.forEach(timeout => {
clearTimeout(timeout.current);
});
setTimeOuts([]);
}, [timeouts]);
React.useEffect(() => {
animateMarker();
}, []);
React.useEffect(() => {
return () => {
clearAllTimeouts();
};
}, [props.selectedStation, props.routes]);
return (
props.route?.route?.[0] && (
<Marker
visible={props.visible}
scale={1}
ref={busRef}
key={props.route.bus_number}
point={{
lat: props.route?.route[props.route.current_location ?? 0].lat,
lon: props.route?.route[props.route.current_location ?? 0].lon,
}}
children={<BusView name={name} type={props.route?.tt_id} />}
/>
)
);
});
and this is the parent component :
React.useEffect(() => {
getRoutes(); // fetch new coordinates for buses of selected station
}, [selectedStation]);
const handleChangeStation = (station: Station) => {
setRoutes(() => null);
setSelectedStation(() => station);
};
{routes?.[0] &&
routes.map((route, index) => (
<BusMarker
key={index}
route={route}
visible={showBuses}
selectedStation={selectedStation}
routes={routes}
/>
))} // render buses on map
React.useEffect(() => {
return () => {
clearAllTimeouts();
};
}, [props.selectedStation, props.routes]);
Changing the array of timeouts does not cause the effect from above to change which actually clears the timeouts (note: clearAllTimeouts is using the "old" array).
Therefore timeouts which are stored in setTimeOuts without changing ether selectedStation or routes the new timeouts are never cancelled.
In general you can simplify your code a lot and putting all animation consern related code into one useEffect:
export const BusMarker = React.memo(function BusMarker(props: Props) {
const name = props.route?.bus_number?.replace(/\D/g, '');
const busRef = React.useRef<Marker>(null);
const currentRoute = props.route;
const currentLocation = props.route.current_location ?? 0;
React.useEffect(() => {
if (!currentRoute || !currentRoute.route || currentRoute.route.length === 0) {
/* No route to animate*/
return;
}
console.log('animation start for marker', props.route.bus_number, 'from position :', currentLocation);
const timeouts = currentRoute.route.slice(currentLocation + 1).map((bus, index) => {
return setTimeout(() => {
console.log('animateMarker', currentRoute.bus_number, 'to position index:', index);
busRef.current?.animatedMoveTo(
{
lat: bus.lat,
lon: bus.lon,
},
1000 * currentRoute.average_second_per_coordinate,
);
}, index * 1000 * currentRoute.average_second_per_coordinate);
});
return () => {
/* Cleanup all timeouts when animation parameters changed or on unmount. */
for (const timeout of timeouts) {
clearTimeout(timeout);
}
};
}, [currentRoute, props.selectedStation, currentLocation]);
/* Does props.selectedStation equal currentLocation or why did selectedStation has been a dependency beforehand? */
return (
props.route?.route?.[0] && (
<Marker
visible={props.visible}
scale={1}
ref={busRef}
key={props.route.bus_number}
point={{
lat: props.route?.route[props.route.current_location ?? 0].lat,
lon: props.route?.route[props.route.current_location ?? 0].lon,
}}
children={<BusView name={name} type={props.route?.tt_id} />}
/>
)
);
});
I'd recommand putting any dependencies which causes the animation to change into the React.useEffect dependency array.
Please note that I haven't tested this code so there might be some typos or I overlooked something.
Related
How can I make other filter button disappear when picked 1 (or multiple) value in same filter block.
Here is my code base:
const FilterBlock = props => {
const {
filterApi,
filterState,
filterFrontendInput,
group,
items,
name,
onApply,
initialOpen
} = props;
const { formatMessage } = useIntl();
const talonProps = useFilterBlock({
filterState,
items,
initialOpen
});
const { handleClick, isExpanded } = talonProps;
const classStyle = useStyle(defaultClasses, props.classes);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target)) {
isExpanded && handleClick();
}
};
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, [isExpanded]);
const list = isExpanded ? (
<Form>
<FilterList
filterApi={filterApi}
filterState={filterState}
name={name}
filterFrontendInput={filterFrontendInput}
group={group}
items={items}
onApply={onApply}
/>
</Form>
) : null;
return (
<div
data-cy="FilterBlock-root"
aria-label={itemAriaLabel}
ref={ref}
>
<Menu.Button
data-cy="FilterBlock-triggerButton"
type="button"
onClick={handleClick}
aria-label={toggleItemOptionsAriaLabel}
>
<div>
<span>
{name}
</span>
<svg
width="8"
height="5"
viewBox="0 0 8 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97291 0.193232C7.20854"
fill="currentColor"
/>
</svg>
</div>
</Menu.Button>
<div>
<div>
{list}
</div>
</div>
</div>
);
};
I am trying to achieve when I chose 1 value or more than 1 value inside filter block the other block will disappear but right now I achieved that when I chose 1 value the other filter block disappear but when I chose another value in the same block the other filter block appear again. Anyone have idea how can I work on this?
I am using React and Redux for this project
Thank you for helping me on this!!!!
Update:
Added parent component for FilterBlock.ks:
const FilterSidebar = props => {
const { filters, filterCountToOpen } = props;
const [selectedGroup, setSelectedGroup] = useState(null);
const talonProps = useFilterSidebar({ filters });
const {
filterApi,
filterItems,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleReset
} = talonProps;
const filterRef = useRef();
const classStyle = useStyle(defaultClasses, props.classes);
const handleApplyFilter = useCallback(
(...args) => {
const filterElement = filterRef.current;
if (
filterElement &&
typeof filterElement.getBoundingClientRect === 'function'
) {
const filterTop = filterElement.getBoundingClientRect().top;
const windowScrollY =
window.scrollY + filterTop - SCROLL_OFFSET;
window.scrollTo(0, windowScrollY);
}
handleApply(...args);
},
[handleApply, filterRef]
);
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={(...args) => {
console.log('args: ', ...args);
setSelectedGroup(prev =>
prev !== null ? null : groupName
);
return handleApplyFilter(...args);
}}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
/>
);
});
return (
<div className="container px-4 mx-auto">
<Menu
as="div"
className="my-16 justify-center flex flex-wrap py-5 border-y border-black border-opacity-5"
>
{result}
</Menu>
</div>
);
};
Updated added useFilterSideBar.js:
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '#apollo/client';
import { useHistory, useLocation } from 'react-router-dom';
import { useAppContext } from '#magento/peregrine/lib/context/app';
import mergeOperations from '../../util/shallowMerge';
import { useFilterState } from '../FilterModal';
import {
getSearchFromState,
getStateFromSearch,
sortFiltersArray,
stripHtml
} from '../FilterModal/helpers';
import DEFAULT_OPERATIONS from '../FilterModal/filterModal.gql';
const DRAWER_NAME = 'filter';
export const useFilterSidebar = props => {
const { filters } = props;
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
const { getFilterInputsQuery } = operations;
const [isApplying, setIsApplying] = useState(false);
const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
const [filterState, filterApi] = useFilterState();
const prevDrawer = useRef(null);
const isOpen = drawer === DRAWER_NAME;
const history = useHistory();
const { pathname, search } = useLocation();
const { data: introspectionData } = useQuery(getFilterInputsQuery);
const attributeCodes = useMemo(
() => filters.map(({ attribute_code }) => attribute_code),
[filters]
);
// Create a set of disabled filters.
const DISABLED_FILTERS = useMemo(() => {
const disabled = new Set();
// Disable category filtering when not on a search page.
if (pathname !== '/search.html') {
disabled.add('category_id');
disabled.add('category_uid');
}
return disabled;
}, [pathname]);
// Get "allowed" filters by intersection of filter attribute codes and
// schema input field types. This restricts the displayed filters to those
// that the api will understand.
const possibleFilters = useMemo(() => {
const nextFilters = new Set();
const inputFields = introspectionData
? introspectionData.__type.inputFields
: [];
// perform mapping and filtering in the same cycle
for (const { name } of inputFields) {
const isValid = attributeCodes.includes(name);
const isEnabled = !DISABLED_FILTERS.has(name);
if (isValid && isEnabled) {
nextFilters.add(name);
}
}
return nextFilters;
}, [DISABLED_FILTERS, attributeCodes, introspectionData]);
const isBooleanFilter = options => {
const optionsString = JSON.stringify(options);
return (
options.length <= 2 &&
(optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '0',
value: '0'
})
) ||
optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '1',
value: '1'
})
))
);
};
// iterate over filters once to set up all the collections we need
const [
filterNames,
filterKeys,
filterItems,
filterFrontendInput
] = useMemo(() => {
const names = new Map();
const keys = new Set();
const frontendInput = new Map();
const itemsByGroup = new Map();
const sortedFilters = sortFiltersArray([...filters]);
for (const filter of sortedFilters) {
const { options, label: name, attribute_code: group } = filter;
// If this aggregation is not a possible filter, just back out.
if (possibleFilters.has(group)) {
const items = [];
// add filter name
names.set(group, name);
// add filter key permutations
keys.add(`${group}[filter]`);
// TODO: Get all frontend input type from gql if other filter input types are needed
// See https://github.com/magento-commerce/magento2-pwa/pull/26
if (isBooleanFilter(options)) {
frontendInput.set(group, 'boolean');
// add items
items.push({
title: 'No',
value: '0',
label: name + ':' + 'No'
});
items.push({
title: 'Yes',
value: '1',
label: name + ':' + 'Yes'
});
} else {
// Add frontend input type
frontendInput.set(group, null);
// add items
for (const { label, value } of options) {
items.push({ title: stripHtml(label), value });
}
}
itemsByGroup.set(group, items);
}
}
return [names, keys, itemsByGroup, frontendInput];
}, [filters, possibleFilters]);
// on apply, write filter state to location
useEffect(() => {
if (isApplying) {
const nextSearch = getSearchFromState(
search,
filterKeys,
filterState
);
// write filter state to history
history.push({ pathname, search: nextSearch });
// mark the operation as complete
setIsApplying(false);
}
}, [filterKeys, filterState, history, isApplying, pathname, search]);
const handleOpen = useCallback(() => {
toggleDrawer(DRAWER_NAME);
}, [toggleDrawer]);
const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);
const handleApply = useCallback(() => {
setIsApplying(true);
handleClose();
}, [handleClose]);
const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);
const handleKeyDownActions = useCallback(
event => {
// do not handle keyboard actions when the modal is closed
if (!isOpen) {
return;
}
switch (event.keyCode) {
// when "Esc" key fired -> close the modal
case 27:
handleClose();
break;
}
},
[isOpen, handleClose]
);
useEffect(() => {
const justOpened =
prevDrawer.current === null && drawer === DRAWER_NAME;
const justClosed =
prevDrawer.current === DRAWER_NAME && drawer === null;
// on drawer toggle, read filter state from location
if (justOpened || justClosed) {
const nextState = getStateFromSearch(
search,
filterKeys,
filterItems
);
filterApi.setItems(nextState);
}
// on drawer close, update the modal visibility state
if (justClosed) {
handleClose();
}
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);
useEffect(() => {
const nextState = getStateFromSearch(search, filterKeys, filterItems);
filterApi.setItems(nextState);
}, [filterApi, filterItems, filterKeys, search]);
return {
filterApi,
filterItems,
filterKeys,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleClose,
handleKeyDownActions,
handleOpen,
handleReset,
isApplying,
isOpen
};
};
Update FilterList component:
const FilterList = props => {
const {
filterApi,
filterState,
filterFrontendInput,
name,
group,
itemCountToShow,
items,
onApply,
toggleItemOptionsAriaLabel
} = props;
const classes = useStyle(defaultClasses, props.classes);
const talonProps = useFilterList({ filterState, items, itemCountToShow });
const { isListExpanded, handleListToggle } = talonProps;
const { formatMessage } = useIntl();
// memoize item creation
// search value is not referenced, so this array is stable
const itemElements = useMemo(() => {
if (filterFrontendInput === 'boolean') {
const key = `item-${group}`;
return (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItemRadioGroup
filterApi={filterApi}
filterState={filterState}
group={group}
name={name}
items={items}
onApply={onApply}
labels={labels}
/>
</li>
);
}
return items.map((item, index) => {
const { title, value } = item;
const key = `item-${group}-${value}`;
if (!isListExpanded && index >= itemCountToShow) {
return null;
}
// create an element for each item
const element = (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItem
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
/>
</li>
);
// associate each element with its normalized title
// titles are not unique, so use the element as the key
labels.set(element, title.toUpperCase());
return element;
});
}, [
classes,
filterApi,
filterState,
filterFrontendInput,
name,
group,
items,
isListExpanded,
itemCountToShow,
onApply
]);
const showMoreLessItem = useMemo(() => {
if (items.length <= itemCountToShow) {
return null;
}
const label = isListExpanded
? formatMessage({
id: 'filterList.showLess',
defaultMessage: 'Show Less'
})
: formatMessage({
id: 'filterList.showMore',
defaultMessage: 'Show More'
});
return (
<li className={classes.showMoreLessItem}>
<button
onClick={handleListToggle}
className="text-sm hover_text-indigo-500 transition-colors duration-sm"
data-cy="FilterList-showMoreLessButton"
>
{label}
</button>
</li>
);
}, [
isListExpanded,
handleListToggle,
items,
itemCountToShow,
formatMessage,
classes
]);
return (
<Fragment>
<ul className={classes.items}>
{itemElements}
{showMoreLessItem}
</ul>
</Fragment>
);
};
FilterList.defaultProps = {
onApply: null,
itemCountToShow: 5
};
Update FilterRadioGroup:
const FilterItemRadioGroup = props => {
const { filterApi, filterState, group, items, onApply, labels } = props;
const radioItems = useMemo(() => {
return items.map(item => {
const code = `item-${group}-${item.value}`;
return (
<FilterItemRadio
key={code}
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
labels={labels}
/>
);
});
}, [filterApi, filterState, group, items, labels, onApply]);
const fieldValue = useMemo(() => {
if (filterState) {
for (const item of items) {
if (filterState.has(item)) {
return item.value;
}
}
}
return null;
}, [filterState, items]);
const field = `item-${group}`;
const fieldApi = useFieldApi(field);
const fieldState = useFieldState(field);
useEffect(() => {
if (field && fieldValue === null) {
fieldApi.reset();
} else if (field && fieldValue !== fieldState.value) {
fieldApi.setValue(fieldValue);
}
}, [field, fieldApi, fieldState.value, fieldValue]);
return (
<RadioGroup field={field} data-cy="FilterDefault-radioGroup">
{radioItems}
</RadioGroup>
);
};
FilterItemRadioGroup.defaultProps = {
onApply: null
};
The code seems overly complicated for what it does.
Could you perhaps make the filter block a state object like this:
filterGroup = {
title: "",
hasSelectedItems: boolean,
filterItems: []
}
Then it would be a simple matter of checking hasSelectedItems to conditionally render only one filterGroup in the UI.
I apologize I don't have the time to go in depth and try out your code, but I would put the filterBlock rendering inside the return statement and do something like this:
return(
{ (!!filterValue && (filterGroup === selectedGroup))
&&
<FilterGroup />
}
);
I find the best results come from using my state values as conditions inside my return statement.
That way, I don't have to manage complex logic in the .map() functions or with useEffect.
It looks like FilterSideBar is where you build the the array of FilterBlocks.
According to your question, I think you want to display all filter blocks if NO filters are selected, but once a filter is selected, you want the other FilterBlocks to not render, correct?
So, in your FilterSidebar module, I'd modify your "result" variable to include a check to see if anything has already been selected.
If something has been selected, then only render the FilterBlock that corresponds to that selected list item.
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
// This is the conditional rendering
(!!blockState.filterGroupSelected && blockState.filterGroupSelected === group) ?
<FilterBlock
key={group}
...
/>
: null
);
});
Then you'll have to add an item to your state object (unless you already have it) to track the group that was selected (e.g. "Printer Type"), and only allow that one to render.
If no group has a selected filter item, they should all render.
Also, you'll have to be sure to clear the selectedGroup whenever there are no selected items.
If this does not work, it may be that your state changes are not triggering a re-render. In which case it can be as simple as adding a reference to your state object in your component's return method like this:
{blockState.filterGroupSelected && " " }
It's a hack but it works, and keeps you from adding calls to useEffect.
I have two components:
A parent component with a list of products
A child component a few levels deeper with a product
Child component should change the state in parent component with the help of passed callback from parent to child.
However, I get an error: Rendered more hooks than during the previous render
Code:
// Parent
const ProductsListScreen = () => {
const [localData, setLocalData] = React.useState([
{ id: 1, count: 1 }
]);
const onProductChecked = ({ id, count }) => {
const checkedItemIndex = localData.findIndex((item) => item.id === id);
const checkedItem = checkedItemIndex ? localData[checkedItemIndex] : null;
if (checkedItem) {
// If the input count is equal to item's count in state - set checked to true
if (checkedItem.count == count) {
// Set parent state of checked product to checked true
setLocalTaskItems((prevState) => [
...prevState.slice(0, checkedItemIndex),
{
...prevState[checkedItemIndex],
countIsGood: true,
},
...prevState.slice(idx + 1),
]);
}
}
};
// List of products
return <ProductsList onProductChecked={onProductChecked} />;
};
// Child a few levels deeper - Product
const onContinuePress = (params, callback) => {
// onProductChecked callback
callback(params);
};
const Product = ({ onProductChecked }) => {
const id = 1;
const count = 1;
return (
<Button
onPress={() =>
onContinuePress(
{
id,
count,
},
onProductChecked
)
}
>
Submit
</Button>
);
};
Send localData and setLocalData as props to the child and do onProductChecked in there with props.localData and props.setLocalData
like this
<ProductsList localData={localData} setLocalData={setLocalData} />;
I built a boat visualizer using specific API. The API returns a json response that I injecting it in a table.
The problem: Sometimes during the day I noticed that the application would stop working throwing an instance of:
Unhandled Rejection (TypeError): ships.reduce is not a function
Below for completeness the print screen of the error:
Below the code I am using:
const ShipTracker = ({ ships, setActiveShip }) => {
console.log("These are the ships: ", { ships });
return (
<div className="ship-tracker">
<Table className="flags-table" responsive hover>
<thead>
<tr>
<th>#</th>
<th>MMSI</th>
<th>TIMESTAMP</th>
<th>LATITUDE</th>
<th>LONGITUDE</th>
<th>COURSE</th>
<th>SPEED</th>
<th>HEADING</th>
<th>NAVSTAT</th>
<th>IMO</th>
<th>NAME</th>
<th>CALLSIGN</th>
</tr>
</thead>
<tbody>
{ships.map((ship, index) => {
// <-- Error Here
const {
MMSI,
TIMESTAMP,
LATITUDE,
LONGITUDE,
COURSE,
SPEED,
HEADING,
NAVSTAT,
IMO,
NAME,
CALLSIGN
} = ship.AIS;
const cells = [
MMSI,
TIMESTAMP,
LATITUDE,
LONGITUDE,
COURSE,
SPEED,
HEADING,
NAVSTAT,
IMO,
NAME,
CALLSIGN
];
return (
<tr
onClick={() =>
setActiveShip(
ship.AIS.NAME,
ship.AIS.LATITUDE,
ship.AIS.LONGITUDE
)
}
key={index}
>
<th scope="row">{index}</th>
{cells.map(cell => (
<td key={ship.AIS.MMSI}>{cell}</td>
))}
</tr>
);
})}
</tbody>
</Table>
</div>
);
};
Googlemap.js
class BoatMap extends Component {
constructor(props) {
super(props);
this.state = {
ships: [],
filteredShips: [],
type: "All",
shipTypes: [],
activeShipTypes: []
};
this.updateRequest = this.updateRequest.bind(this);
this.countDownInterval = null;
this.updateInterval = null;
this.map = null;
this.maps = null;
this.previousTimeStamp = null;
}
async updateRequest() {
const url = "http://localhost:3001/hello";
const fetchingData = await fetch(url);
const ships = await fetchingData.json();
console.log("fetched ships", ships);
if (JSON.stringify(ships) !== "{}") {
if (this.previousTimeStamp === null) {
this.previousTimeStamp = ships.reduce(function(obj, ship) {
obj[ship.AIS.NAME] = ship.AIS.TIMESTAMP;
return obj;
}, {});
}
this.setState({
ships: ships,
filteredShips: ships
});
this.props.callbackFromParent(ships);
for (let ship of ships) {
if (this.previousTimeStamp !== null) {
if (this.previousTimeStamp[ship.AIS.NAME] === ship.AIS.TIMESTAMP) {
this.previousTimeStamp[ship.AIS.NAME] = ship.AIS.TIMESTAMP;
console.log("Same timestamp: ", ship.AIS.NAME, ship.AIS.TIMESTAMP);
continue;
} else {
this.previousTimeStamp[ship.AIS.NAME] = ship.AIS.TIMESTAMP;
}
}
let _ship = {
// ship data ...
};
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(_ship)
};
await fetch(
"http://localhost:3001/users/vessles/map/latlng",
requestOptions
);
// console.log('Post', Date());
}
}
}
render() {
const noHoverOnShip = this.state.hoverOnActiveShip === null;
return (
<div className="google-map">
<GoogleMapReact
bootstrapURLKeys={{ key: "key" }}
center={{
lat: this.props.activeShip ? this.props.activeShip.latitude : 37.99,
lng: this.props.activeShip
? this.props.activeShip.longitude
: -97.31
}}
zoom={5.5}
onGoogleApiLoaded={({ map, maps }) => {
this.map = map;
this.maps = maps;
// we need this setState to force the first mapcontrol render
this.setState({ mapControlShouldRender: true, mapLoaded: true });
}}
>
{this.state.mapLoaded && (
<div>
<Polyline
map={this.map}
maps={this.maps}
markers={this.state.trajectoryData}
lineColor={this.state.trajectoryColor}
/>
</div>
)}
{Array.isArray(this.state.filteredShips) ? (
this.state.filteredShips.map(ship => (
<Ship
ship={ship}
key={ship.AIS.MMSI}
lat={ship.AIS.LATITUDE}
lng={ship.AIS.LONGITUDE}
logoMap={this.state.logoMap}
logoClick={this.handleMarkerClick}
logoHoverOn={this.handleMarkerHoverOnShip}
logoHoverOff={this.handleMarkerHoverOffInfoWin}
/>
))
) : (
<div />
)}
</GoogleMapReact>
</div>
);
}
}
export default class GoogleMap extends React.Component {
state = {
ships: [],
activeShipTypes: [],
activeCompanies: [],
activeShip: null,
shipFromDatabase: []
};
setActiveShip = (name, latitude, longitude) => {
this.setState({
activeShip: {
name,
latitude,
longitude
}
});
};
setShipDatabase = ships => {
this.setState({ shipFromDatabase: ships });
};
// passing data from children to parent
callbackFromParent = ships => {
this.setState({ ships });
};
render() {
return (
<MapContainer>
{/* This is the Google Map Tracking Page */}
<pre>{JSON.stringify(this.state.activeShip, null, 2)}</pre>
<BoatMap
setActiveShip={this.setActiveShip}
activeShip={this.state.activeShip}
handleDropdownChange={this.handleDropdownChange}
callbackFromParent={this.callbackFromParent}
shipFromDatabase={this.state.shipFromDatabase}
renderMyDropDown={this.state.renderMyDropDown}
// activeWindow={this.setActiveWindow}
/>
<ShipTracker
ships={this.state.ships}
setActiveShip={this.setActiveShip}
onMarkerClick={this.handleMarkerClick}
/>
</MapContainer>
);
}
}
What I have done so far:
1) I also came across this source to help me solve the problem but no luck.
2) Also I consulted this other source, and also this one but both of them did not help me to figure out what the problem might be.
3) I dug more into the problem and found this source too.
4) I read this one too. However, neither of these has helped me fix the problem.
5) I also found this source very useful but still no solution.
Thanls for pointing to the right direction for solving this problem.
One way you could go about this is to default to an empty array if something goes wrong with your fetch request inside updateRequest:
async updateRequest() {
const url = "http://localhost:3001/hello";
const defaultValue = [];
const ships = await fetchShips(url, defaultValue);
// safe to use `Array` methods on an empty `array`
if (this.previousTimeStamp === null) {
this.previousTimestamp = ships.reduce(...);
}
}
function fetchShips(url, defaultValue) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
})
.then(data => {
if (Array.isArray(data)) {
return data;
}
// return the default value (empty array)
// so that your application doesn't crash
return defaultValue;
})
.catch(error => {
console.error(error.message);
// catch other errors and return the default
// value (empty array), so that your application doesn't crash
return defaultValue;
});
}
However, you should handle errors appropriately and show a message that says something went wrong for a better user experience rather than not showing anything at all.
async updateRequest() {
const url = "http://localhost:3001/hello";
const defaultValue = [];
const ships = await fetchShips(url, defaultValue).catch(e => e);
if (ships instanceof Error) {
// handle errors appropriately
return;
}
// otherwise continue, with an empty array still a
// possibility, but won't break the app
if (this.previousTimeStamp === null) {
this.previousTimestamp = ships.reduce(...);
}
}
function fetchShips(url, defaultValue) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
})
.then(data => {
if (Array.isArray(data)) {
return data;
}
// return the default value (empty array)
// so that your application doesn't crash
return defaultValue;
});
}
This won't solve why your ships prop is not always an array, but it will help you to protect your code against this unhandled exception
<tbody>
{Array.isArray(ships) && ships.map((ship, index) => {
const {
MMSI,
// rest of your code here
This is probably occurring because ships is not an array at the time reduce is being called, perhaps its null?
If ships is state on the parent component being passed down then perhaps it is initially null and then gets updates, so the first time the component is rendered its calling reduce on null?
If the version of JavaScript you are using supports null propagation you can use
ships?.reduce so that the first time its rendered if its null it won't try to invoke the reduce function on null, but then when it renders and it is, all should be fine, this is quite a common pattern.
If your JavaScript version doesn't support null propagation you can use
ships && ships.length > 0 && ships.reduce(...
so you should also change
{ships.map((ship, index) => { // <-- Error Here to
{
ships?.map((ship, index) =>
...
// or
ships && ships.length > 0 && ships.map((ship, index) =>
....
}
I have the following component, which you suggest me an address.
The problem is that I need the addresses you suggest to me to refer to a single state.
Is there a way to specify this restriction via API?
Or is there a way that I could do via code in your opinion?
I thought I was doing something inside, do you want it to think it's a correct way?
Could I have problems?
React.useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google)
autocompleteService.current = new window.google.maps.places.AutocompleteService();
if (!autocompleteService.current) return undefined;
if (value === '') {
setOptions([]);
return undefined;
}
fetch({ input: value }, res => {
//edit
const loc = res.map(a => (a.description.split(',').pop().trim() === 'Italia' ? a : false)).filter(Boolean);
if (active) setOptions(loc || []);
});
return () => {
active = false;
};
}, [value, fetch]);
Code:
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles, TextField, Grid, Typography } from '#material-ui/core';
import { Autocomplete } from '#material-ui/lab';
import { LocationOn } from '#material-ui/icons';
import parse from 'autosuggest-highlight/parse';
import throttle from 'lodash/throttle';
function loadScript(src, position, id) {
if (!position) return;
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
const useStyles = makeStyles(theme => ({
icon: {
color: theme.palette.text.secondary,
marginRight: theme.spacing(2)
}
}));
export default function AutocompleteGoogleMaps(props) {
const { api, value, onChange, changeinfo } = props;
const classes = useStyles();
const [options, setOptions] = React.useState([]);
const loaded = React.useRef(false);
const { i18n } = useTranslation();
let language = localStorage.getItem('lang');
if (language === null) language = i18n.language;
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${api}&libraries=places&language=${language}`,
document.querySelector('head'),
'google-maps'
);
}
loaded.current = true;
}
const handleChange = ({ target: { value } }) => {
onChange(value);
};
const onTagsChange = (event, val) => {
if (val !== null && val.description !== null) {
onChange({ target: { value: val.description } });
if (changeinfo) geocodeByAddress(val.description).then(changeinfo);
}
};
const fetch = React.useMemo(
() =>
throttle((input, callback) => {
autocompleteService.current.getPlacePredictions(input, callback);
}, 200),
[]
);
React.useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google)
autocompleteService.current = new window.google.maps.places.AutocompleteService();
if (!autocompleteService.current) return undefined;
if (value === '') {
setOptions([]);
return undefined;
}
fetch({ input: value }, res => {
if (active) setOptions(res || []);
});
return () => {
active = false;
};
}, [value, fetch]);
const geocodeByAddress = address => {
const geocoder = new window.google.maps.Geocoder();
const { OK } = window.google.maps.GeocoderStatus;
return new Promise((resolve, reject) => {
geocoder.geocode({ address }, (results, status) => {
if (status !== OK) {
return reject(status);
}
return resolve(results);
});
});
};
return (
<Autocomplete
id="google-map"
getOptionLabel={option => (typeof option === 'string' ? option : option.description)}
filterOptions={x => x}
options={options}
autoComplete
includeInputInList
freeSolo
value={value}
renderInput={params => <TextField {...params} onChange={handleChange} {...props} />}
onChange={onTagsChange}
renderOption={({
structured_formatting: {
main_text_matched_substrings: matches,
main_text: city,
secondary_text: street
}
}) => {
const parts = parse(
city,
matches.map(match => [match.offset, match.offset + match.length])
);
return (
<Grid container alignItems="center">
<Grid item>
<LocationOn className={classes.icon} />
</Grid>
<Grid item xs>
{parts.map(({ highlight, text }, key) => (
<span key={key} style={{ fontWeight: highlight ? 700 : 400 }}>
{text}
</span>
))}
<Typography variant="body2" color="textSecondary">
{street}
</Typography>
</Grid>
</Grid>
);
}}
/>
);
}
We have a similar entry for this feature request in the Google Issue Tracker so that we can provide information on it to all Google Maps Platform APIs users, as the technical aspects of this issue are of interest to a number of our customers,
Issues and Feature Requests in the Issue Tracker are directly curated by Places API specialists, who will provide updates in the Issue Tracker whenever there's news from the engineering team.
We would like to warmly invite you to view the issue in the Issue Tracker, and to star it to register your interest. This will subscribe you to receive technical updates on the issue. Starring the issue also provides us with valuable feedback on the importance of the issue to our customers, and increases the issue's priority with the product engineering team.
You can view and star the issue here:
https://issuetracker.google.com/35822067
This Issue Tracker entry is the authoritative source for public information regarding this issue, and all publicly-relevant updates will be posted there.
I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues