How to fix error TS2345 in functional component React? - reactjs

I have a small React + Typescript application. I am running a filtering function which is executed when some buttons are clicked or a value is entered into an input.
I have interfaces everywhere. I also passed the interface to the state, or the fact that I will have an empty array. But I keep getting an error with the following text:
"TS2345: Argument of type 'TodoItemInterface[] | undefined' is not assignable to parameter of type 'TodoItemInterface[]'. Type 'undefined' is not assignable to type 'TodoItemInterface[]'."
I can’t find how and where to pass that I definitely won’t have undefined or fix this error in a different way. Please help. Below I send the code of the function in useEffect and functions with a state that are related to it.
error is thrown here in filterItems.
useEffect(() => {
const newArray = searchInputFilter(filterItems(todoListStateArray, infoSearchObject.filter), textForSearch);
setVisibleTodoListItems(newArray);
}, [infoSearchObject, todoListStateArray, textForSearch])
Another code
const filterItems = (arr: TodoItemInterface[], filter: string) => {
if (filter === 'all') {
return arr;
} else if (filter === 'active') {
return arr.filter((item) => (!item.done));
} else if (filter === 'done') {
return arr.filter((item) => item.done);
}
};
const searchInputFilter = (arr: TodoItemInterface[], search: string) => {
if (search.length === 0) {
return arr;
}
return arr.filter((item) => {
return item.subtitle.toLowerCase().includes(search.toLowerCase());
});
}
const [infoSearchObject, setInfoSearchObject] = useState({inWork: '', done: '', filter: 'all'});
const [todoListStateArray, setTodoListStateArray] = useState<TodoItemInterface[] | []>(todoListDefault);
const [visibleTodoListItems, setVisibleTodoListItems] = useState<TodoItemInterface[] | []>(todoListDefault);
const [textForSearch, setTextForSearch] = useState('');

That is actually one of the benefits in using typescript; It’s actually telling you there is the possibility that your filterItems can possibly return undefined, and that happens when neither of your conditions are true.
As a solution, you should add a return value when none of the conditions are met:
const filterItems = (arr: TodoItemInterface[], filter: string): TodoItemInterface[] => {
if (filter === 'all') {
return arr;
} else if (filter === 'active') {
return arr.filter((item) => (!item.done));
} else if (filter === 'done') {
return arr.filter((item) => item.done);
}
return arr;
};
To note that in the example I also added a return type to your function; That is pretty important to ensure consistency in what get actually returned from it.

Related

CHROME - EXTENSION: Rendered fewer hooks than expected

I am working on a project which creates a web extension. I am checking if my function returns different, I set the new value. Then, I am using the states in return function. However I am getting an error : Rendered fewer hooks than expected. This may be caused by an accidental early return statement. How can I fix that?
const [isSecure, setIsSecure] = useState<boolean>(true)
const [isTabEmpty, setIsTabEmpty] = useState<boolean>(false)
useEffect(() => {
if (hostIsSecure() !== isSecure) {
setIsSecure(hostIsSecure)
}
if (tabEmpty() !== isTabEmpty) {
setIsTabEmpty(tabEmpty)
}
}, [host, tabEmpty, hostIsSecure])
function hostIsSecure(): boolean {
if (protocol && protocol === 'https:') {
return true
} else return false
}
function tabEmpty(): boolean {
if (
curTabId &&
Object.keys(tabHosts).filter(
(id) => curTabId.toString() === id.toString()
).length === 1
)
return false
else return false
}

React with TypeScript: how to type useState hook with previous state

I have a React + TS application and I'm writing a custom hook to update the header of a table. Basically, I can click on different checkboxes and hide/show that specific column of the table.
const useHeader = (headerConfiguration: TableHeader[]) => {
const [header, setHeader] = useState(headerConfiguration);
const updateHeader = (column: string) => {
setHeader(prevState => { // <- this line has the error
prevState.forEach(el => {
if (el.name === column) el.visible = !el.visible;
});
});
};
return {
header,
updateHeader,
};
};
export default useHeader;
As the code shown, I need to update the visible property based on the column name.
Currently I have an error tough:
Argument of type '(prevState: TableHeader[]) => void' is not
assignable to parameter of type 'SetStateAction<TableHeader[]>'.
Type '(prevState: TableHeader[]) => void' is not assignable to type
'(prevState: TableHeader[]) => TableHeader[]'.
Type 'void' is not assignable to type 'TableHeader[]'.
I get what the error is saying but I'm not sure how to fix it, if I write the same function using the library Immer it works without any error:
setHeader(
produce(header, draftHeader => {
draftHeader.forEach(el => {
if (el.name === column) {
el.visible = !el.visible;
}
});
}),
);
How can I fix this?
When not using immer, you're responisble to produce and return a new state. So instead of manipulating prevState you need to create a copy of it, with the changes you require applied.
For example, with something like this:
const updateHeader = (column: string) => {
setHeader(prevState => {
// return a copy prevState
return prevState.map(
el => el.name === column
? {...el, visible: !el.visible} // copy inner item, if changed
: el;
);
});
};
Please try this:
const useHeader = (headerConfiguration: TableHeader[]) => {
const [header, setHeader] = useState(headerConfiguration);
const updateHeader = (column: string) => {
header.forEach(el => {
if (el.name === column) el.visible = !el.visible;
});
setHeader(header);
};
return {
header,
updateHeader,
};
};
export default useHeader;

React - Loop inside a loop not rendering

I have a map loop inside another loop like this:
{"undefined" !== typeof this.props.categoryObject['products'] && Object.keys(this.props.categoryObject['products']).length > 0 && Object.keys(this.props.categoryObject['products']).map(keyProd => {
const product = this.props.categoryObject['products'][keyProd];
if("undefined" !== typeof product) {
Object.keys(this.props.activeFilters).map(keyFilter => {
console.warn(this.props.activeFilters[keyFilter]);
return (<div>test</div>)
})
}
})}
The console works, but not the render. Any idea why ?
Thank you
The problem here that outer .map doesn't have return statement and your outer code doesn't have return statement too.
So you have to change your code to the following
return ("undefined" !== typeof this.props.categoryObject['products'] && Object.keys(this.props.categoryObject['products']).length > 0 && Object.keys(this.props.categoryObject['products']).map(keyProd => {
const product = this.props.categoryObject['products'][keyProd];
if("undefined" !== typeof product) {
return Object.keys(this.props.activeFilters).map(keyFilter => {
console.warn(this.props.activeFilters[keyFilter]);
return (<div>test</div>)
})
}
}))
Also some notes on how you can make your code more readable with new es6 features. Commented version
// It's better to extract products into separate variable
// as there are a lot of copy paste code for this action
const { products } = this.props.categoryObject;
// `undefined` is falsy value so you can just test next condition for early exit
if (!products) { return null; }
// 1. no need to test for `.length` as iterating empty array will make 0 iterations
// 2. use Object.entries you can immediately get key and value
return Object.entries(products).map(([key, product]) => {
// Same things as with `products` variable
if (product) {
// I think for your future code you can use `Object.entries` here too
return Object.keys(this.props.activeFilters).map(keyFilter => {
return (<div>test</div>)
})
}
})
Final version:
const { products } = this.props.categoryObject;
if (!products) { return null; }
return Object.entries(products).map(([key, product]) => {
if (product) {
return Object.keys(this.props.activeFilters).map(keyFilter => {
return (<div>test</div>)
})
}
})
NOTE to use all of them in all common browser you have to configure your babel properly

ESLint | Assignment to property of function parameter 'payload'

I have the following utility method: it removes all the empty keys of a payload object.
Here is the code:
const removeEmptyKeysUtil = (payload: any): any => {
Object.keys(payload).map(
(key): any => {
if (payload && payload[key] === '') {
delete payload[key];
}
return false;
}
);
return payload;
};
export default removeEmptyKeysUtil;
But I get the following eslint error:
Assignment to property of function parameter 'payload'.eslint(no-param-reassign)
It was suggested to me that I use either object destructuring or Object.assign. But I am a little confused on how to do that.
For example, destructuring:
if (payload && payload[key] === '') {
const {delete payload[key], ...actualPayload} = payload;
}
return false;
But I get this error:
Block-scoped variable 'payload' used before its declaration.
I know, I can disable the rule, but I do not want to do that. I want to properly code that branch
Can you help me a little bit? I don't think I understand those 2 concepts at all. Thank you.
Lint is warning you to fulfill one of the properties called "immutability".
So when you receive a parameter to use in this function (which is an object) indicates that what you return from that function is a new object with the modifications you want but that this is a new object.
PD: In addition, if you use Typescript and you know what that payload is made of, it would be best if you created an interface with its data rather than assigning any because it can help you select internal properties and avoid errors, as well as the response it will return.
One solution could be this:
const removeEmptyKeysUtil = (payload: any): any =>
Object.keys(payload)
.filter(key => payload[key] !== "")
.reduce((result, key) => ({ ...result, [key]: payload[key] }), {});
export default removeEmptyKeysUtil;
I know this answer is probably not exactly what you were looking for. Since your code will perform badly on complicated object I created a quick solution which will give the result you wanted. I hope it helps.
function isEmptyObject(obj) {
if (!obj || typeof obj !== 'object') return false;
if (obj.constructor === Array) return obj.length === 0;
return Object.keys(obj).length === 0 && obj.constructor === Object;
}
function removeEmptyKeysUtil(obj) {
if (!obj) return {};
Object.keys(obj).map(key => {
// Add additional check here for null, undefined, etc..
if (obj[key] === '') delete obj[key];
if (obj.constructor === Object) {
obj[key] = removeEmptyKeysUtil(obj[key])
}
if (obj.constructor === Array) {
for (let i = obj.length; i >= 0; i--) {
obj[i] = removeEmptyKeysUtil(obj[i])
if (isEmptyObject(obj[i])) {
obj.splice(i, 1);
}
}
}
if (isEmptyObject(obj[key])) {
delete obj[key];
}
})
return obj;
}
const obj = {
test: '11',
test1: '1',
test2: {
test: '',
test1: ''
},
test3: [
{
test: ''
},
{
test: ''
},
{
test: '3'
},
{
test33: {
test: '1',
test1: ''
}
}
]
};
console.log(removeEmptyKeysUtil(obj))

React.JS: Nested Filter/Map Arrays

I'm trying to learn React as part of a course I'm doing with Udacity. I've been battling this issue for a couple of days and can't seem to figure a way out of it.
Basically, I have an API call that returns an array of objects as a promise, I map or filter through these (I've tried both) and need to find the elements that already exist in an array booksOnShelves. I do this by comparing the .id property of each object. Once we find a match I need to set the .shelf property to the same value as the one in the existing Array and if the book doesn't exist I need to set its value to 'none'. All is good here but the problem is when I find a match the property updates the shelf to the correct one, but the iteration continues with the next book that obviously doesn't match and overwrites the .shelf value with none.
Here's the code for this particular method:
class SearchBar extends Component {
state = {
query: '',
result: []
}
getBooks = (query) => {
const booksOnShelves = this.props.books;
BooksAPI.search(query)
.then(res => {
if (res instanceof Array && res.length > 0) {
res.filter( searchedBooks => {
return booksOnShelves.filter( onShelves => {
if ( searchedBooks.id === onShelves.id) {
return searchedBooks.shelf = onShelves.shelf
} else {
return searchedBooks.shelf = 'none
}
})
})
this.setState({result: res.sort(sortBy('title'})
} else {
this.setState({result: []})
}
})
.catch( err => { console.log('ERROR: ', err)})
}
I've tried so many things but none was able to maintain the value of the shelf property.
Is there any way to stop the iteration from overwriting the matched value?
Any ideas how to achieve the intended outcome?
Thank you in advance for the help.
Should work fine in your case:
BooksAPI.search(query).then(res => {
if (res instanceof Array && res.length > 0) {
booksOnShelves.forEach((book) => {
const correspondingRes = res.find((item) => { return (item.id === book.id) });
if (correspondingRes) {
book.shelf = correspondingRes.shelf;
} else {
book.shelf = “none”;
}
});
}
Problem sorted with the help of all you guys I've made it work. Thanks a lot.
Just for reference here's the code that worked.
BooksAPI.search( query )
.then( res => {
if (res instanceof Array && res.length > 0) {
res.map( booksFromSearch => {
booksOnShelves.find( book => {
if( booksFromSearch.id === book.id) {
booksFromSearch.shelf = book.shelf
return booksFromSearch;
} else {
booksFromSearch.shelf = 'none'
}
})
})

Resources