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
}
Related
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.
A button calls the function signAllBrowsed, which contains two other functions:
loadSafetyLetters is a hook that makes a database call for some data and sets it in context
signAll is a hook that tries to access data in context to do something with it
Context is getting set properly, but when signAll accesses it, the data is not updated. Is there a way to access the updated context without directly passing it to the 2nd function? Or is there a way to call a callback once context is updated and accessible? Seems the updated context is only available after a re-render.
The component containing signAllBrowsed and the 2 hooks are siblings.
code in above image:
setModalVisible(true)
const logHeader = 'SafetyLetterHome::SignAllBrowsed'
try {
const response = await loadSafetyLetters(false) // before beginning sign all, get a fresh list of letters from db
if (Configs.SHOW_REQUEST_LOGS) console.log(`${logHeader} response`, response)
if (response === 'no api error') {
await signAll()
navigation.navigate('SafetyLetterSign')
}
} catch (error) {
const errorMessage = error.status && error.status.message ? error.status.message : error
Alert.alert('Database Error', errorMessage)
console.log(`${logHeader}`, errorMessage)
}
}
loadSafetyLetters calls the loadLetters hook:
const [getLetters] = useGetLetters()
const [sortLetters] = useSortLetters()
const [hasAPIError] = useHasAPIError()
const navigation = useNavigation()
const { setModalVisible, setShowSignAll, setSortedLetters, setUnsortedLetters } = useContext(SafetyContext)
const loadLetters = async (sort = true) => {
try {
const response = await getLetters()
const logHeader = 'SafetyHome::loadLetters'
const errorMessage = 'The following error occurred when trying to load letters:'
if (Configs.SHOW_REQUEST_LOGS) console.log(`${logHeader} response`, response)
const error = hasAPIError(response, logHeader, errorMessage)
if (error) return error
const { data } = response.data.payload
let unsortedLetters = []
if (data !== null && data.length > 0) {
data.map((item) => {
// grab only unsigned letters
if (
item.assignmentStatus === SafetySources.PENDING ||
item.assignmentStatus === SafetySources.BROWSED ||
item.assignmentStatus === SafetySources.QUESTIONS_COMPLETE
) {
unsortedLetters.push({
safetyLetterId: item.safetyLetterId,
title: item.title,
assignmentStatus: item.assignmentStatus,
filePath: item.filePath,
embeddableToken: item.embeddableToken,
sponsorId: item.sponsorId,
letterDate: item.letterDate,
form16: item.form16Enabled === '1' ? true : false,
sponsorName: item.sponsorName,
type: item.letterType,
sortOrder: item.sortOrder, // dear doctor; sortOrder === 1
})
}
})
}
if (unsortedLetters.length > 0) {
let bletters = unsortedLetters.filter((letter) => letter.assignmentStatus === SafetySources.BROWSED || letter.assignmentStatus === SafetySources.QUESTIONS_COMPLETE)
console.log('useLoadLetters; setting fresh pull of letters in context, including ', bletters.length, ' browsed letters')
setUnsortedLetters(unsortedLetters) // set in context
setShowSignAll( // show/hide sign all button
unsortedLetters.some((letter) =>
letter.assignmentStatus === SafetySources.BROWSED ||
letter.assignmentStatus === SafetySources.QUESTIONS_COMPLETE,
))
}
if (sort) {
if (unsortedLetters.length > 0) {
let sortedLetters = sortLetters(unsortedLetters) // sort letters with hook
setSortedLetters(sortedLetters) // set in context
}
}
} catch (error) {
console.log('SafetyHome::loadLetters ', error)
const errorMessage = error.status && error.status.message ? error.status.message : error
Alert.alert(
'Error Loading Letters',
`A database error has occurred. Please try again. (${errorMessage})`,
)
navigation.navigate('Home')
} finally {
setModalVisible(false)
}
}
return [loadLetters]
}
signAll hook:
const { state: { unsortedLetters },
setF16Browsed,
setQcAndBrowsed,
setModalVisible,
setSelectedLetter
} = useContext(SafetyContext)
const signAll = async () => {
let qcAndBrowsed = [] // set letter groups in context
let f16Browsed = []
unsortedLetters.forEach((letter) => {
if (
letter.assignmentStatus === SafetySources.BROWSED ||
letter.assignmentStatus === SafetySources.QUESTIONS_COMPLETE
) {
if (
letter.form16 &&
letter.assignmentStatus !== SafetySources.QUESTIONS_COMPLETE
) {
f16Browsed.push(letter)
} else {
qcAndBrowsed.push(letter)
}
}
})
setQcAndBrowsed(qcAndBrowsed)
setF16Browsed(f16Browsed)
// begin sign all with first f16 letter
if (f16Browsed.length > 0) {
setSelectedLetter(f16Browsed[0])
} else {
setSelectedLetter(null) // clear any previous viewed letter
}
setModalVisible(false)
}
return [signAll]
}
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
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))
render() {
const rowLeft = [];
const rowRight = [];
let a = this.props.ntn;
Object.keys(this.props.ntn).map((keyName, keyIndex) =>{
if (keyName === "_id" || keyName === "name" || keyName === "description" || keyName === "instant" || keyName === "active") {
if (keyName === "beacon" || keyName === "group") {
return rowLeft.push(<InfoRow notification={keyName} notificationValue={a[keyName].name.toString()} key={keyIndex}/>)
}
else if (a[keyName].offers) {
return rowLeft.push(<InfoRow notification={keyName} notificationValue={a[keyName].offers.toString()} key={keyIndex}/>)
}
else {
return rowLeft.push(<InfoRow notification={keyName} notificationValue={a[keyName].toString()} key={keyIndex}/>)
}}
});
Object.keys(this.props.ntn).map((keyName, keyIndex) =>{
if (keyName === "levelType" || keyName === "triggeringEvents" || keyName === "type" || keyName === "beacon" || keyName === "inbox") {
if (keyName === "beacon" || keyName === "group") {
return rowRight.push(<InfoRow notification={keyName} notificationValue={a[keyName].name.toString()} key={keyIndex}/>)
}
else if (a[keyName].offers) {
return rowRight.push(<InfoRow notification={keyName} notificationValue={a[keyName].offers.toString()} key={keyIndex}/>)
}
else {
return rowRight.push(<InfoRow notification={keyName} notificationValue={a[keyName].toString()} key={keyIndex}/>)
}}
});
return (
i did something like this
actually I'm fetching values and showing all the details on the page
what the thing is now I'm getting this warning
"Expected to return a value at the end of this function array-callback-return in React JS"
Any solution? How to deal with it?
TLDR: The simplest fix is to use Object.keys(this.props.ntn).forEach instead of .map and make all return rowLeft.push just rowLeft.push.
Long answer:
The ESLint array-callback-return warning makes sure that you always return a value from methods like map, filter and reduce. You are not returning a value in you first if which has the following form:
if condition1 {
if condition2 {
return
}
// here you are missing a return
}
else if ...
However, you are not using map correctly. You should be using forEach.
Of course, your code can be rewritten using map, consider:
const {ntn} = this.props;
const rowLeft = Object.keys(ntn).map((keyName, keyIndex) => {
const value = ntn[keyName];
let notificationValue;
if (['_id', 'name', 'description', 'instant', 'active', 'beacon', 'group'].includes(keyName) {
notificationValue = value.name.toString();
} else if (value.offers) {
notificationValue = value.offers.toString();
} else {
notificationValue = value.toString();
}
return (
<InfoRow notification={keyName} notificationValue={notificationValue} key={keyIndex}/>
);
});
Also note that in your example the first condition is never executed because it would require keyName to have two values at once. I have replaced it with one condition which I guess is what you want.