Why do I get TypeError: undefined is not an object (evaluating 'items.items.map') when removing a component? - reactjs

I am trying to delete an item component from a list on my React app but I get a TypeError: undefined is not an object (evaluating 'items.items.map') error when the event is triggered. I can't see what is wrong with the code.
function _delete(id) {
return fetchWrapper.delete(`${baseUrl}/${id}`);
}
const [items, setItems] = useState(null);
useEffect(() => {
shoppingService.getAll().then((x) => setItems(x));
}, []);
function deleteItem(id) {
setItems(
items.items.map((x) => {
if (x.id === id) {
x.isDeleting = true;
}
return x;
})
);
shoppingService.delete(id).then(() => {
setItems((items) => items.items.filter((x) => x.id !== id));
});
}
{items &&
items.items.map((item) => (
<ListItem divider={true} key={item.id}>
...
</ListItem>
)
}
<IconButton
onClick={() => deleteItem(item.id)}
>
What could be causing it to go wrong? The item correctly gets removed from the collection on MongoDB.
Sandbox: View sandbox here

Thanks for the sandbox, it provides more context and makes this solvable.
It looks like you are attempting to export your functional component ShoppingList and this is the culprit:
export { ShoppingList };
That is leading to the error:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
This is because when you use export { ShoppingList } you are exporting an object, not your functional component. Thus: type error. This can be verified with:
console.log({ShoppingList});
Returns:
Object {ShoppingList: function ShoppingList()}
To solve this, change your export command to:
export default ShoppingList;
This solves your original question, but other errors occur now. This is likely because shoppingService is not connected to the CodeSandbox. If you run into any further issues, I can lend a hand with another question.
I reread your question and took some liberties with removing shoppingService to idenitfy what is going on with your remove function. I believe what is happening is you are trying to edit an immutable variable items. Instead, spread the items into a new array and then filter-out the item with the matching id.
First clean-up your data frame to remove the duplicate items.items reference:
const myItems = [
{
id: "5f516ebbd8f4d11abe5919c8",
itemName: "Bacon"
},
{
id: "5f52e43e4dda46085f008aca",
itemName: "Eggs"
},
{
id: "5f57a3056a71a0143281c1a8",
itemName: "Oranges"
}
];
Here's what your function should look like:
function deleteItem(id) {
let newItems = [...items].filter((item) => item.id !== id);
setItems(newItems);
console.log(newItems);
}
CodeSandbox: https://codesandbox.io/s/stack-removing-items-m8thu?file=/src/App.js

Related

State changed in context provider not saved

So I'm trying to centralize some alert-related logic in my app in a single .tsx file, that needs to be available in many components (specfically, an "add alert" fuction that will be called from many components). To this end I am trying to use react context to make the alert logic available, with the state (an array of active alerts) stored in App.tsx.
Alerts.tsx
export interface AlertContext {
alerts: Array<AppAlert>,
addAlert: (msg: React.ReactNode, style: string, callback?: (id: string) => {}) => void,
clearAlert: (id: string) => void
}
[...]
export function AlertsProvider(props: AlertsProps) {
function clearAlert(id: string){
let timeout = props.currentAlerts.find(t => t.id === id)?.timeout;
if(timeout){
clearTimeout(timeout);
}
let newCurrent = props.currentAlerts.filter(t => t.id != id);
props.setCurrentAlerts(newCurrent);
}
function addAlert(msg: JSX.Element, style: string, callback: (id: string) => {}) {
console.log("add alert triggered");
let id = uuidv4();
let newTimeout = setTimeout(clearAlert, timeoutMilliseconds, id);
let newAlert = {
id: id,
msg: msg,
style: style,
callback: callback,
timeout: newTimeout
} as AppAlert;
let test = [...props.currentAlerts, newAlert];
console.log(test);
props.setCurrentAlerts(test);
console.log("current alerts", props.currentAlerts);
}
let test = {
alerts: props.currentAlerts,
addAlert: addAlert,
clearAlert: clearAlert
} as AlertContext;
return (<AlertsContext.Provider value={test}>
{ props.children }
</AlertsContext.Provider>);
}
App.tsx
function App(props: AppProps){
[...]
const [currentAlerts, setCurrentAlerts] = useState<Array<AppAlert>>([]);
[...]
const alertsContext = useContext(AlertsContext);
console.log("render app", alertsContext.alerts);
return (
<AlertsProvider currentAlerts={currentAlerts} setCurrentAlerts={setCurrentAlerts}>
<div className={ "app-container " + (error !== undefined ? "err" : "") } >
{ selectedMode === "Current" &&
<CurrentItems {...currentItemsProps} />
}
{ selectedMode === "History" &&
<History {...historyProps } />
}
{ selectedMode === "Configure" &&
<Configure {...globalProps} />
}
</div>
<div className="footer-container">
{
alertsContext.alerts.map(a => (
<Alert variant={a.style} dismissible transition={false} onClose={a.callback}>
{a.msg}
</Alert>
))
}
{/*<Alert variant="danger" dismissible transition={false}
show={ error !== undefined }
onClose={ dismissErrorAlert }>
<span>{ error?.msg }</span>
</Alert>*/}
</div>
</AlertsProvider>
);
}
export default App;
I'm calling alertsContext.addAlert in only one place in CurrentItems.tsx so far. I've also added in some console statements for easier debugging. The output in the console is as follows:
render app Array [] App.tsx:116
XHRGEThttp://localhost:49153/currentitems?view=Error [HTTP/1.1 500 Internal Server Error 1ms]
Error 500 fetching current items for view Error: Internal Server Error CurrentItems.tsx:94
add alert triggered Alerts.tsx:42
Array [ {…}, {…} ] Alerts.tsx:53
current alerts Array [ {…} ] Alerts.tsx:55
render app Array []
So I can see that by the end of the addAlert function the currentAlerts property appears to have been updated, but then subsequent console statement in the App.tsx shows it as empty. I'm relatively new to React, so I'm probably having some misunderstanding of how state is meant to be used / function, but I've been poking at this on and off for most of a day with no success, so I'm hoping someone can set me straight.
const alertsContext = useContext(AlertsContext);
This line in App is going to look for a provider higher up the component tree. There's a provider inside of App, but that doesn't matter. Since there's no provider higher in the component tree, App is getting the default value, which never changes.
You will either need to invert the order of your components, so the provider is higher than the component that's trying to map over the value, or since the state variable is already in App you could just use that directly and delete the call to useContext:
function App(props: AppProps){
[...]
const [currentAlerts, setCurrentAlerts] = useState<Array<AppAlert>>([]);
[...]
// Delete this line
// const alertsContext = useContext(AlertsContext);
console.log("render app", currentAlerts);
[...]
{
currentAlerts.map(a => (
<Alert variant={a.style} dismissible transition={false} onClose={a.callback}>
{a.msg}
</Alert>
))
}
}

Set default usestate with array of objects from props

I have a weird situation. I have the following code which is not working.
const [result, setResult] = useState(props.fileNamesStatus.map((file, i) => {
return {
key: file.fileStatus
}
}));
Strangely though, it works sometimes and sometimes it does not. I have used it in the JSX as follows:
I get the error Cannot read property 'key' of undefined
<ul className="in">
{props.fileNamesStatus.map((file, i) => {
console.log(result[i].key) /// Cannot read property 'key' of undefined
})}
</ul>
You need to add another useEffect hook:
useEffect(() => {
if (props.fileNamesStatus && props.fileNamesStatus.length) {
setResult(props.fileNamesStatus.map((file, i) => {
return {
key: file.fileStatus
}
}))
}
}, [props.fileNamesStatus])
But even with this change there is a chance that in your render props and state will be out of sync, so you should add additional check console.log(result[i] ? result[i].key : 'out of sync')

React infinite update loop with useCallback (react-hooks/exhaustive-deps)

Consider the following example that renders a list of iframes.
I'd like to store all the documents of the rendered iframes in frames.
import React, { useState, useEffect, useCallback } from "react";
import Frame, { FrameContextConsumer } from "react-frame-component";
function MyFrame({ id, document, setDocument }) {
useEffect(() => {
console.log(`Setting the document for ${id}`);
setDocument(id, document);
}, [id, document]); // Caution: Adding `setDocument` to the array causes an infinite update loop!
return <h1>{id}</h1>;
}
export default function App() {
const [frames, setFrames] = useState({
desktop: {
name: "Desktop"
},
mobile: {
name: "Mobile"
}
});
const setFrameDocument = useCallback(
(id, document) => {
setFrames({
...frames,
[id]: {
...frames[id],
document
}
});
},
[frames, setFrames]
);
console.log(frames);
return (
<div className="App">
{Object.keys(frames).map(id => (
<Frame key={id}>
<FrameContextConsumer>
{({ document }) => (
<MyFrame
id={id}
document={document}
setDocument={setFrameDocument}
/>
)}
</FrameContextConsumer>
</Frame>
))}
</div>
);
}
There are two issues here:
react-hooks/exhaustive-deps is complaining that setDocument is missing in the dependency array. But, adding it causing an infinite update loop.
Console logging frames shows that only mobile's document was set. I expect desktop's document to be set as well.
How would you fix this?
Codesandbox
const setFrameDocument = useCallback(
(id, document) => setFrames((frames) => ({
...frames,
[id]: {
...frames[id],
document
}
})),
[]
);
https://codesandbox.io/s/gracious-wright-y8esd
The frames object's reference keeps changing due to the state's update. With the previous implementation (ie the frames object within the dependency array), it would cause a chain reaction that would cause the component to re-render and causing the frames object getting a new reference. This would go on forever.
Using only setFrames function (a constant reference), this chain react won't propagate. eslint knows setFrames is a constant reference so it won't complain to the user about it missing from the dependency array.

How to get React/recompose component updated when props are changed?

I'm writing this product list component and I'm struggling with states. Each product in the list is a component itself. Everything is rendering as supposed, except the component is not updated when a prop changes. I'm using recompose's withPropsOnChange() hoping it to be triggered every time the props in shouldMapOrKeys is changed. However, that never happens.
Let me show some code:
import React from 'react'
import classNames from 'classnames'
import { compose, withPropsOnChange, withHandlers } from 'recompose'
import { addToCart } from 'utils/cart'
const Product = (props) => {
const {
product,
currentProducts,
setProducts,
addedToCart,
addToCart,
} = props
const classes = classNames({
addedToCart: addedToCart,
})
return (
<div className={ classes }>
{ product.name }
<span>$ { product.price }/yr</span>
{ addedToCart ?
<strong>Added to cart</strong> :
<a onClick={ addToCart }>Add to cart</a> }
</div>
)
}
export default compose(
withPropsOnChange([
'product',
'currentProducts',
], (props) => {
const {
product,
currentProducts,
} = props
return Object.assign({
addedToCart: currentProducts.indexOf(product.id) !== -1,
}, props)
}),
withHandlers({
addToCart: ({
product,
setProducts,
currentProducts,
addedToCart,
}) => {
return () => {
if (addedToCart) {
return
}
addToCart(product.id).then((success) => {
if (success) {
currentProducts.push(product.id)
setProducts(currentProducts)
}
})
}
},
}),
)(Product)
I don't think it's relevant but addToCart function returns a Promise. Right now, it always resolves to true.
Another clarification: currentProducts and setProducts are respectively an attribute and a method from a class (model) that holds cart data. This is also working good, not throwing exceptions or showing unexpected behaviors.
The intended behavior here is: on adding a product to cart and after updating the currentProducts list, the addedToCart prop would change its value. I can confirm that currentProducts is being updated as expected. However, this is part of the code is not reached (I've added a breakpoint to that line):
return Object.assign({
addedToCart: currentProducts.indexOf(product.id) !== -1,
}, props)
Since I've already used a similar structure for another component -- the main difference there is that one of the props I'm "listening" to is defined by withState() --, I'm wondering what I'm missing here. My first thought was the problem have been caused by the direct update of currentProducts, here:
currentProducts.push(product.id)
So I tried a different approach:
const products = [ product.id ].concat(currentProducts)
setProducts(products)
That didn't change anything during execution, though.
I'm considering using withState instead of withPropsOnChange. I guess that would work. But before moving that way, I wanted to know what I'm doing wrong here.
As I imagined, using withState helped me achieving the expected behavior. This is definitely not the answer I wanted, though. I'm anyway posting it here willing to help others facing a similar issue. I still hope to find an answer explaining why my first code didn't work in spite of it was throwing no errors.
export default compose(
withState('addedToCart', 'setAddedToCart', false),
withHandlers({
addToCart: ({
product,
setProducts,
currentProducts,
addedToCart,
}) => {
return () => {
if (addedToCart) {
return
}
addToCart(product.id).then((success) => {
if (success) {
currentProducts.push(product.id)
setProducts(currentProducts)
setAddedToCart(true)
}
})
}
},
}),
lifecycle({
componentWillReceiveProps(nextProps) {
if (this.props.currentProducts !== nextProps.currentProducts ||
this.props.product !== nextProps.product) {
nextProps.setAddedToCart(nextProps.currentProducts.indexOf(nextProps.product.id) !== -1)
}
}
}),
)(Product)
The changes here are:
Removed the withPropsOnChange, which used to handle the addedToCart "calculation";
Added withState to declare and create a setter for addedToCart;
Started to call the setAddedToCart(true) inside the addToCart handler when the product is successfully added to cart;
Added the componentWillReceiveProps event through the recompose's lifecycle to update the addedToCart when the props change.
Some of these updates were based on this answer.
I think the problem you are facing is due to the return value for withPropsOnChange. You just need to do:
withPropsOnChange([
'product',
'currentProducts',
], ({
product,
currentProducts,
}) => ({
addedToCart: currentProducts.indexOf(product.id) !== -1,
})
)
As it happens with withProps, withPropsOnChange will automatically merge your returned object into props. No need of Object.assign().
Reference: https://github.com/acdlite/recompose/blob/master/docs/API.md#withpropsonchange
p.s.: I would also replace the condition to be currentProducts.includes(product.id) if you can. It's more explicit.

How to defined component /binding when using React ref in Reasonml?

I am having issues integrating react-system-notification module in my app, having read the documentation about Reason React Ref I am not sure why the reference is not passed down the stack; a hint would be much appreciated.
I keep getting the error below, I have used this component in the past in React but it seems that there is some issue when used in ReasonML/React. I suspect a null reference is passed down which breaks the component.
Element type is invalid: expected a string (for built-in components)
or a class/function (for composite components) but got: undefined. You
likely forgot to export your component from the file it's defined in,
or you might have mixed up default and named imports.
Check the render method of Notifications.
Binding:
module NotificationSystem = {
[#bs.module "react-notification-system"] external reactClass : ReasonReact.reactClass = "default";
let make = ( children ) =>
ReasonReact.wrapJsForReason(
~reactClass,
~props=Js.Obj.empty(),
children
)
};
Component
type action =
| AddNotification(string);
type state = {
_notificationSystem: ref(option(ReasonReact.reactRef)),
};
let setNotificationSystemRef = (notificationRef, {ReasonReact.state: state}) =>
state._notificationSystem := Js.toOption(notificationRef) ;
let component = ReasonReact.reducerComponent("Notifications");
let addNotification = (message, state) => {
switch state._notificationSystem^ {
| None => ()
| Some(r) => ReasonReact.refToJsObj(r)##addNotification({"message": message, "level": "success"});
}
};
let make = (_children) => {
...component,
initialState: () => {_notificationSystem: ref(None) },
reducer: (action, state) =>
switch action {
| AddNotification(message) => ReasonReact.SideEffects(((_) => addNotification(message, state)))
},
render: ({handle, reduce}) => (
<div>
<NotificationSystem ref=(handle(setNotificationSystemRef)) />
<button onClick=(reduce( (_) => AddNotification("Test Notification Test"))) > (ReasonReact.stringToElement("Click")) </button>
</div>
)
};
My guess would be that react-notification-system is not distributed as an es6 component, and therefore does not export default. Try removing default from the external:
[#bs.module "react-notification-system"] external reactClass : ReasonReact.reactClass = "";
You should always start by trying out the simplest implementation first, then build incrementally from there, to minimize possible causes of errors. Especially when dealing with something as error-prone as the js boundary. In this case that would be without the complex ref handling. You'll likely find that it still does not work, because of the above, and that you've been looking in the wrong place because you bit off more than you can chew.
After some further investigation, thanks to glensl hint and some messages exchanged on Discord I am posting the complete answer.
The issue was related to way bsb generated the "require" statement in the javascript output:
[#bs.module "react-notification-system"] external reactClass : ReasonReact.reactClass = "default";
Was being emitted as:
var ReactNotificationSystem = require("react-notification-system");
instead of
var NotificationSystem = require("react-notification-system");
Mightseem a little hacky however I got bsb to emit the correct javascript was using the following statement:
[#bs.module ] external reactClass : ReasonReact.reactClass = "react-notification-system/dist/NotificationSystem";
Then with some minor tweaking to the wrapper component, I was able to get it working with the following code:
module ReactNotificationSystem = {
[#bs.module ] external reactClass : ReasonReact.reactClass = "react-notification-system/dist/NotificationSystem";
let make = ( children ) =>
ReasonReact.wrapJsForReason(
~reactClass,
~props=Js.Obj.empty(),
children
)
};
type action =
| AddNotification(string);
type state = {
_notificationSystem: ref(option(ReasonReact.reactRef)),
};
let setNotificationSystemRef = (notificationRef, {ReasonReact.state}) =>
state._notificationSystem := Js.Nullable.to_opt(notificationRef) ;
let component = ReasonReact.reducerComponent("Notifications");
let addNotification = (message, state) => {
switch state._notificationSystem^ {
| None => ()
| Some(r) => ReasonReact.refToJsObj(r)##addNotification({"message": message, "level": "success"});
}
};
let make = (_children) => {
...component,
initialState: () => {_notificationSystem: ref(None) },
reducer: (action, state) =>
switch action {
| AddNotification(message) => ReasonReact.SideEffects(((_) => addNotification(message, state)))
},
render: ({handle, reduce}) => (
<div>
<ReactNotificationSystem ref=(handle(setNotificationSystemRef)) />
<button onClick=(reduce( (_) => AddNotification("Hello"))) > (ReasonReact.stringToElement("Click")) </button>
</div>
)
};
A full sample working project can be found on Github here:

Resources