Context variable not updating in child component React Context - reactjs

I have multiple child components (component generate through recursion) inside my main component. Now the problem is when I updated the context variable in the parent component the child component doesn't render the updated value
here is my context provider
function MainLayoutProvider({ children }) {
const [mainJson, setMainJson] = useState([
{
component:'section',
id:'1111',
content:null,
type:'section',
cmType:'normal',
class:'',
style:{},
props:{}
}
]);
return (
<MainLayout.Provider value={mainJson}>
<MainDispatchLayout.Provider value={setMainJson}>
{children}
</MainDispatchLayout.Provider>
</MainLayout.Provider>
);
}
Here I included it on my main component
function App() {
return (
<DndProvider backend={HTML5Backend}>
<MainLayoutProvider>
<PlayGround></PlayGround>
</MainLayoutProvider>
</DndProvider>
);
}
inside the PlayGround component, there is another component name DropSection, where I am updating the 'mainJson' value
function DropSection() {
const board = useContext(MainLayout);
const setBoard = useContext(MainDispatchLayout);
const [{ isOver }, drop] = useDrop(() => ({
accept: "image",
drop(item, monitor) {
const didDrop = monitor.didDrop();
if (!didDrop ) {
addItemToBoard(item.sectionName)
}
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));
const addItemToBoard = (sectionName) => {
let newJson = {
component:sectionName,
id:IdGenerator(),
content:null,
type:'text',
cmType:'normal',
class:'',
style:{},
props:{}
}
setBoard((board) => [...board, newJson]);
};
return (
<div ref={drop}>
<h4 className="text-center">DropZone</h4>
{board.map((config,index) => <RenderCard key={config.id} config={config} />)}
</div>
);
}
but in the RenderCard component, the value of 'mainJson' is not updating or rendering, I am getting the old value which initializes in MainLayoutContext
function RenderCard({config}) {
const board =useContext(MainLayout);
const setBoard = useContext(MainDispatchLayout);
const [{ isOver }, drop] = useDrop(() => ({
accept: "image",
drop(item, monitor) {
const didDrop = monitor.didDrop();
if (!didDrop ) {
addItemToBoard(item.sectionName)
}
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
}));
const addItemToBoard = async (sectionName) => {
let newJson = {
component:sectionName,
id:IdGenerator(),
content:null,
type:'text',
cmType:'normal',
class:'',
style:{},
props:{}
}
setBoard((board) => [...board, newJson]);
};
if(config.cmType == 'complex'){
return RenderComplexCard(config)
}
var configProperty = {
style: config.style,
id:config.id,
};
return React.createElement(
config.component,
configProperty,
config.content &&
( config.type == "section" && Array.isArray(config.content) ? config.content.map(c => <RenderCard key={c.id} config={c} />) : config.content )
);
}

I had similar issue and solved by using useRef hook, if you do not want to use useState.
Do not forget to reference the variable by using .current when accessing it.

Related

How to destroy google map on demand rides and deliveries in React

I was following the documentation to implement google map on demand rides and deliveries solution (ODRD) here.
And my Map component in React:
const MapComponent = ({ styles }) => {
const ref = useRef(null);
const tripId = useRef<string>('');
const locationProvider =
useRef<google.maps.journeySharing.FleetEngineTripLocationProvider>();
const [error, setError] = useState<string | undefined>();
const mapOptions = useRef<MapOptionsModel>({
showAnticipatedRoutePolyline: true,
showTakenRoutePolyline: true,
destinationMarker: ICON_OPTIONS.USE_DEFAULT,
vehicleMarker: ICON_OPTIONS.USE_DEFAULT,
});
const [trip, setTrip] = useState<TripModel>({
status: null,
dropOff: null,
waypoints: null,
});
const setTripId = (newTripId: string) => {
tripId.current = newTripId;
if (locationProvider.current) locationProvider.current.tripId = newTripId;
};
const setMapOptions = (newMapOptions: MapOptionsModel) => {
mapOptions.current.showAnticipatedRoutePolyline =
newMapOptions.showAnticipatedRoutePolyline;
mapOptions.current.showTakenRoutePolyline =
newMapOptions.showTakenRoutePolyline;
mapOptions.current.destinationMarker = newMapOptions.destinationMarker;
mapOptions.current.vehicleMarker = newMapOptions.vehicleMarker;
setTripId(tripId.current);
};
const authTokenFetcher = async () => {
const response = await fetch(
`${PROVIDER_URL}/token/consumer/${tripId.current}`
);
const responseJson = await response.json();
return {
token: responseJson.jwt,
expiresInSeconds: 3300,
};
};
useEffect(() => {
locationProvider.current =
new google.maps.journeySharing.FleetEngineTripLocationProvider({
projectId: PROVIDER_PROJECT_ID,
authTokenFetcher,
tripId: tripId.current,
pollingIntervalMillis: DEFAULT_POLLING_INTERVAL_MS,
});
locationProvider.current.addListener(
'error',
(e: google.maps.ErrorEvent) => {
setError(e.error.message);
}
);
locationProvider.current.addListener(
'update',
(
e: google.maps.journeySharing.FleetEngineTripLocationProviderUpdateEvent
) => {
if (e.trip) {
setTrip({
status: e.trip.status,
dropOff: e.trip.dropOffTime,
waypoints: e.trip.remainingWaypoints,
});
setError(undefined);
}
}
);
const mapViewOptions: google.maps.journeySharing.JourneySharingMapViewOptions =
{
element: ref.current as unknown as Element,
locationProvider: locationProvider.current,
anticipatedRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showAnticipatedRoutePolyline,
};
},
takenRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showTakenRoutePolyline,
};
},
destinationMarkerSetup: ({ defaultMarkerOptions }) => {
if (
mapOptions.current.destinationMarker !== ICON_OPTIONS.USE_DEFAULT
) {
defaultMarkerOptions.icon =
mapOptions.current.destinationMarker.icon;
}
return { markerOptions: defaultMarkerOptions };
},
vehicleMarkerSetup: ({ defaultMarkerOptions }) => {
if (mapOptions.current.vehicleMarker !== ICON_OPTIONS.USE_DEFAULT) {
// Preserve some default icon properties.
if (defaultMarkerOptions.icon) {
defaultMarkerOptions.icon = Object.assign(
defaultMarkerOptions.icon,
mapOptions.current.vehicleMarker.icon
);
}
}
return { markerOptions: defaultMarkerOptions };
},
};
const mapView = new google.maps.journeySharing.JourneySharingMapView(
mapViewOptions
);
// Provide default zoom & center so the map loads even if trip ID is bad or stale.
mapView.map.setOptions(DEFAULT_MAP_OPTIONS);
}, []);
return (
<div style={styles.map} ref={ref} />
);
};
And my App component like this:
import React from 'react';
import { Wrapper, Status } from '#googlemaps/react-wrapper';
import MapComponent from './src/components/MapComponent';
import { API_KEY } from './src/utils/consts';
const render = (status: Status) => <Text>{status}</Text>;
const App = () => {
return (
<Wrapper
apiKey={API_KEY}
render={render}
version={'beta'}
// #ts-ignore
libraries={['journeySharing']}
>
<MapComponent />
</Wrapper>
);
};
Everything will works fine but I do not know how to destroy the map when component unmount in React. That's why my App always call API update the trip info.
I was tried to use clean up function in useEffect:
useEffect(() => {
locationProvider.current =
new google.maps.journeySharing.FleetEngineTripLocationProvider({
projectId: PROVIDER_PROJECT_ID,
authTokenFetcher,
tripId: tripId.current,
pollingIntervalMillis: DEFAULT_POLLING_INTERVAL_MS,
});
locationProvider.current.addListener(
'error',
(e: google.maps.ErrorEvent) => {
setError(e.error.message);
}
);
const updateEvent = locationProvider.current.addListener(
'update',
(
e: google.maps.journeySharing.FleetEngineTripLocationProviderUpdateEvent
) => {
if (e.trip) {
setTrip({
status: e.trip.status,
dropOff: e.trip.dropOffTime,
waypoints: e.trip.remainingWaypoints,
});
setError(undefined);
}
}
);
const mapViewOptions: google.maps.journeySharing.JourneySharingMapViewOptions =
{
element: ref.current as unknown as Element,
locationProvider: locationProvider.current,
anticipatedRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showAnticipatedRoutePolyline,
};
},
takenRoutePolylineSetup: ({ defaultPolylineOptions }) => {
return {
polylineOptions: defaultPolylineOptions,
visible: mapOptions.current.showTakenRoutePolyline,
};
},
destinationMarkerSetup: ({ defaultMarkerOptions }) => {
if (
mapOptions.current.destinationMarker !== ICON_OPTIONS.USE_DEFAULT
) {
defaultMarkerOptions.icon =
mapOptions.current.destinationMarker.icon;
}
return { markerOptions: defaultMarkerOptions };
},
vehicleMarkerSetup: ({ defaultMarkerOptions }) => {
if (mapOptions.current.vehicleMarker !== ICON_OPTIONS.USE_DEFAULT) {
// Preserve some default icon properties.
if (defaultMarkerOptions.icon) {
defaultMarkerOptions.icon = Object.assign(
defaultMarkerOptions.icon,
mapOptions.current.vehicleMarker.icon
);
}
}
return { markerOptions: defaultMarkerOptions };
},
};
const mapView = new google.maps.journeySharing.JourneySharingMapView(
mapViewOptions
);
// Provide default zoom & center so the map loads even if trip ID is bad or stale.
mapView.map.setOptions(DEFAULT_MAP_OPTIONS);
return () => {
mapView.map = null // or mapView.map.setmap(null);
google.maps.event.removeListener(updateEvent);
};
}, []);
But it was not working. Hope anyone can help me find out this. Thanks

ag-grid keyboard navigation for frameworkComponents is not working

I'm trying to enable the keyboard action for my ag-grid.
I have few icons in my ag-grid I'm trying to click the icons through keyboard navigation.
to trigger the event via keyboard navigation I used onCellKeyPress where the user clicks enter key I'll trigger the event.
const onCellKeyPress = useCallback(
(keyPressEvent) => {
if (!isNil(keyPressEvent)) {
const {
event: { key },
colDef: { cellRenderer },
data: { indexId },
} = keyPressEvent;
if (key === ENTER_KEY) {
if (cellRenderer === 'deleteButtonSchemaRenderer') {
deleteRef?.current?.handleClickDeleteRow();
}
}
}
}
,[deleteRef?.current?.handleClickDeleteRow])
in framework component i just import my delete icon react component where i'm just doing the click event.
framework component
const frameworkComponents = useMemo(
() => ({
customNoRowsOverlay: ({
message,
iconName,
className,
}: ISystemNotification) => (
<SystemNotification
message={message}
iconName={iconName}
className={className}
/>
),
deleteButtonRenderer: (props: IDeleteRowButton) => {
const {
node: {
data: { id },
},
} = props;
return (
<DeleteRowButton
{...props}
setIsRowDeleting={setIsRowDeleting}
isDisabled={typeof id !== 'string'}
ref={deleteRef}
/>
);
},
}),
[ setIsRowDeleting]
);
deleterow component
const DeleteRowButton = forwardRef(
(
{ setIsRowDeleting, node, isDisabled = false }: IDeleteRowButton,
ref?: Ref<IUseRef | MutableRefObject<HTMLElement | null>>
) => {
useImperativeHandle(ref, () => ({ handleClickDeleteRow }));
const isRowDeleting = pathOr(false, ['data', 'isDeleting'], node);
const [isDeleting, setIsDeleting] = useState<boolean>(isRowDeleting);
const { t } = useTranslation();
const isGridDataChanged = useSelector(selectIsGridDataChanged);
const onClick = useCallback(
({ node, setIsDeleting, isRowDeleting, setIsRowDeleting, data }) => {
if (!isNil(data)) {
node.data = data;
}
const rowData = node.data;
setIsDeleting(!isRowDeleting);
let updatedData = {};
if (rowData.isDeleting) {
updatedData = { ...rowData, isDeleting: false };
} else {
updatedData = { ...rowData, isDeleting: true };
}
node.setData(updatedData);
setIsRowDeleting(true);
},
[]
);
const handleDeleteRow = useMemo(
() =>
debounce(() => {
onClick({
node,
setIsDeleting,
isRowDeleting,
setIsRowDeleting,
data: null,
});
}, 0),
[isRowDeleting, node, onClick, setIsRowDeleting]
);
const handleClickDeleteRow = useCallback(
(rowData: RowNode) => {
onClick({
node,
setIsDeleting,
isRowDeleting,
setIsRowDeleting,
data: rowData,
});
},
[isRowDeleting, node, onClick, setIsRowDeleting]
);
// Setting row active if changes was canceled
useEffect(() => {
if (!isGridDataChanged && !isRowDeleting && isDeleting) {
setIsDeleting(false);
}
}, [isDeleting, isGridDataChanged, isRowDeleting]);
return (
<Icon
disabled={isDisabled}
onClick={handleDeleteRow}
data-testid={TestIds.DeleteButtonIcon}
icon='trash'
title={t('Delete')}
/>
);
}
);
here it's working fine when the user is clicking via mouse, I'm trying to integrate keyboard action. but it always updates the last row of the ag-grid. I don't know the reason.
can any one help me in this.

useCallback and memoization

How the memorized callback function works? In some articles I read that the function is recreated if we do not use useCallback. But if it is recreated, should it be different from the prev version? In my code I didn't notice that there was a difference in callback functions.
My question is: Why in both cases, my set size is 1?
from off doc useCallback
Returns a memoized callback.
Pass an inline callback and an array of dependencies. useCallback will
return a memoized version of the callback that only changes if one of
the dependencies has changed. This is useful when passing callbacks to
optimized child components that rely on reference equality to prevent
unnecessary renders (e.g. shouldComponentUpdate).
import { useCallback } from "react";
const dataSource = [
{
id: 1,
model: "Honda",
color: "red",
},
{
id: 2,
model: "Mazda",
color: "yellow",
},
{
id: 3,
model: "Toyota",
color: "green",
},
];
const Car = ({ model, color, set, onCarClick }) => {
const onClick = () => onCarClick(model, color);
set.add(onCarClick);
console.log(set.size);
return (
<div onClick={onClick}>
Model: {model} Color: {color}
</div>
);
};
const CarsCallback = ({ cars, set }) => {
const onCarClick = (model, color) => {
console.log(model, color);
};
console.log("CarsCallback");
return (
<>
{cars.map((car) => {
return (
<Car
key={car.id}
set={set}
{...car}
onCarClick={onCarClick}
/>
);
})}
</>
);
};
const CarsUseCallback = ({ cars, set }) => {
const onCarClick = useCallback((model, color) => {
console.log(model, color);
}, []);
console.log("CarsUseCallback");
return (
<>
{cars.map((car) => {
return (
<Car
key={car.id}
{...car}
set={set}
onCarClick={onCarClick}
/>
);
})}
</>
);
};
export default function App() {
return (
<div className="App">
<CarsCallback cars={dataSource} set={new Set()} />
<CarsUseCallback cars={dataSource} set={new Set()} />
</div>
);
}
Because CarsUseCallback and CarsCallback was triggered once.
We can see the only one log of CarsUseCallback and CarsCallback.
If we re-render the CarsUseCallback and CarsCallback, we can see the size is 1 and 2.
const CarsCallback = ({ cars, set }) => {
const [count, setCount] = useState(1);
console.log('CarsCallback');
useEffect(() => {
setCount(2);
}, []);
// ...
};
const CarsUseCallback = ({ cars, set }) => {
const [count, setCount] = useState(1);
console.log('CarsUseCallback');
useEffect(() => {
setCount(2);
}, []);
// ...
}

Best way to use useMemo/React.memo for an array of objects to prevent rerender after edit one of it?

I'm struggling with s performance issue with my React application.
For example, I have a list of cards which you can add a like like facebook.
Everything, all list is rerendering once one of the child is updated so here I'm trying to make use of useMemo or React.memo.
I thought I could use React.memo for card component but didn't work out.
Not sure if I'm missing some important part..
Parent.js
const Parent = () => {
const postLike= usePostLike()
const listData = useRecoilValue(getCardList)
// listData looks like this ->
//[{ id:1, name: "Rose", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc },
// { id:2, name: "Helena", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc },
// { id: 3, name: "Gutsy", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc }]
const memoizedListData = useMemo(() => {
return listData.map(data => {
return data
})
}, [listData])
return (
<Wrapper>
{memoizedListData.map(data => {
return (
<Child
key={data.id}
data={data}
postLike={postLike}
/>
)
})}
</Wrapper>
)
}
export default Parent
usePostLike.js
export const usePressLike = () => {
const toggleIsSending = useSetRecoilState(isSendingLike)
const setList = useSetRecoilState(getCardList)
const asyncCurrentData = useRecoilCallback(
({ snapshot }) =>
async () => {
const data = await snapshot.getPromise(getCardList)
return data
}
)
const pressLike = useCallback(
async (id) => {
toggleIsSending(true)
const currentList = await asyncCurrentData()
...some api calls but ignore now
const response = await fetch(url, {
...blabla
})
if (currentList.length !== 0) {
const newList = currentList.map(list => {
if (id === list.id) {
return {
...list,
liked: true,
likedNum: list.likedNum + 1,
}
}
return list
})
setList(newList)
}
toggleIsSending(false)
}
},
[setList, sendYell]
)
return pressLike
}
Child.js
const Child = ({
postLike,
data
}) => {
const { id, name, avatarImg, image, bodyText, likedNum, liked } = data;
const onClickPostLike = useCallback(() => {
postLike(id)
})
return (
<Card>
// This is Material UI component
<CardHeader
avatar={<StyledAvatar src={avatarImg} />}
title={name}
subheader={<SomeImage />}
/>
<Image drc={image} />
<div>{bodyText}</div>
<LikeButton
onClickPostLike={onClickPostLike}
liked={liked}
likedNum={likedNum}
/>
</Card>
)
}
export default Child
LikeButton.js
const LikeButton = ({ onClickPostLike, like, likedNum }) => {
const isSending = useRecoilValue(isSendingLike)
return (
<Button
onClick={() => {
if (isSending) return;
onClickPostLike()
}}
>
{liked ? <ColoredLikeIcon /> : <UnColoredLikeIcon />}
<span> {likedNum} </span>
</Button>
)
}
export default LikeButton
The main question here is, what is the best way to use Memos when one of the lists is updated. Memorizing the whole list or each child list in the Parent component, or use React.memo in a child component...(But imagine other things could change too if a user edits them. e.g.text, image...)
Always I see the Parent component is highlighted with React dev tool.
use React.memo in a child component
You can do this and provide a custom comparator function:
const Child = React.memo(
({
postLike,
data
}) => {...},
(prevProps, nextProps) => prevProps.data.liked === nextProps.data.liked
);
Your current use of useMemo doesn't do anything. You can use useMemo as a performance optimization when your component has other state updates and you need to compute an expensive value. Say you have a collapsible panel that displays a list:
const [expand, setExpand] = useState(true);
const serverData = useData();
const transformedData = useMemo(() =>
transformData(serverData),
[serverData]);
return (...);
useMemo makes it so you don't re-transform the serverData every time the user expands/collapses the panel.
Note, this is sort of a contrived example if you are doing the fetching yourself in an effect, but it does apply for some common libraries like React Apollo.

How to add a function in const Target = props => {

How do I add a function to connect to one of my components onChange? Creating a function like this returns an error code of 'cardActionResponse' is not defined.
What the benefit of using a const class like this?
const Target = props => {
const { markAsDone } = useContext(ItemContext);
const [{ isOver }, drop] = useDrop({
accept: 'Item',
drop: (item, monitor) => console.log(item),
collect: monitor => ({
isOver: !!monitor.isOver()
})
})
//Cannot do this. How else can I make a function to connect to CreateVideoCard?
cardActionResponse = (event) => {
console.log(event);
}
return (
<div className="target top80 right30" ref={drop} style={{ backgroundColor: isOver ? 'black' : '' }} >
<TitleDescription class="z1"/>
<div class="right10 left10">
<CreateVideoCard onChange={this.cardActionResponse} />
<CreateDescriptionCard></CreateDescriptionCard>
<CreateAudioCard></CreateAudioCard>
<CreateTermsCard></CreateTermsCard>
</div>
</div>
);
};
export default Target;
Functional components don't have it's own context (this), so you should simply use const variable.
Please use
const cardActionResponse = (event) => {
console.log(event);
}
and then
<CreateVideoCard onChange={cardActionResponse} />

Resources