Way to avoid non-null assertion while using useState in async Data? - reactjs

Trying to use strict typescript with Next.js and React-query
There is problem that useQuery's return data is Post | undefined.
So I should make data given with useQuery not null with ! operator while allocating to useState
ESlint does not like non-null type assertion.
I know I could turn it off... But I want to do strict type check so I don't want to avoid this.
One way that I found was to use if statement to do null check
but React Hooks is not able to wrap it with if statement...
Is there any brilliant way to do it?
My code is like this
const Edit = () => {
const router = useRouter();
const { id } = router.query;
const { data } = useQuery<Post>('postEdit', () => getPost(id as string));
const [post, setPost] = useState<TitleAndDescription>({
title: data!.title,
content: data!.content,
});
const editMutation = usePostEditMutation();
return (
<MainLayout>
<Wrapper>
{data && <Editor post={post} setPost={setPost} />}
</Wrapper>
</MainLayout>
);
};
export interface TitleAndDescription {
title: string;
content: Descendant[];
}

You can do
const [post, setPost] = useState<TitleAndDescription>({
title: data?.title ?? '',
content: data?.content ?? []
});

The non-null assertion is for cases when you want to exclude null or undefined values from the type. For most cases you don't need it, you can cast to the type you want:
(data as Post).title
or provide a fallback value:
data?.title || ''

While you can do:
const [post, setPost] = useState<TitleAndDescription>({
title: data?.title ?? '',
content: data?.content ?? []
});
as the current accepted answer suggests, it won't solve your problem as you will always have undefined in your local state, as it won't update automatically when new data comes in from useQuery.
data will be undefined on the first render because react-query needs to fetch it first. With useState, you are only passing in the initial value of the first render - which is undefined.
There is no need to copy data from react-query to local state. This just duplicates the single source of truth. You can just use the data returned from useQuery as it will always reflect the server value:
const { data } = useQuery<Post>('postEdit', () => getPost(id as string));
const post = data ?? { title: '', content: [] }

Related

How to use useEffect correctly with useContext as a dependency

I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.

React and Apollo useMutation --- Cannot read property '0' of undefined

Can't seem to figure out away to handle this when the page loads. I continually get the error 'TypeError: Cannot read property '0' of undefined' when I load the component. The useMutation is using the react-apollo library, but for some reason it is checking if causes[0].name exists before we even run the mutation. The cause_name is initially empty on page load, but then first becomes undefined when we first interact with DonateBox. However then when we set a cause value, it correctly sends the array. So I'm not sure why this error continues to persist? Even if it is undefined, it shouldn't be complaining right? Any help would be great, it's driving me crazy.
const DonatePage = () => {
const [causes, setCauses] = useState('')
const [category, setCategory] = useState('cause')
const [amount, setAmount] = useState('25')
const [frequency, setFrequency] = useState('once')
const [saveDonation] = useMutation(CREATE_DONATION, {
variables: {
date: moment().toDate(),
type: 'cause',
recurring: 'false',
amount: Number(amount),
cause_name: undefined || null ? undefined : causes[0]?.name
},
onCompleted() {
}
})
return (
<DonateBox
goToPayment={setCurrentPage}
setCauseProps={setCauses}
setAmount={setAmount}
setCategory={setCategory}
/>
)
}
Pass your variables to the mutation when you call the mutation function. And remove variables from your useMutation() options.
const [saveDonation] = useMutation(CREATE_DONATION, {
onCompleted() {
}
});
...
...
...
saveDonation({
variables: {
date: moment().toDate(),
type: 'cause',
recurring: 'false',
amount: Number(amount),
cause_name: undefined || null ? undefined : causes[0].name
}
})

How can I avoid having to check undefined context values in my children when using React Context?

I have the following:
type ExploreState = {
loading: boolean;
posts?: Post[] // array of objects, has id and some other attrs
}
const ExploreStateContext = React.createContext<ExploreState>({
loading: true
});
const ExploreStateProvider: React.FunctionComponent = (props) => {
const [loading, setLoading] = React.useState(true);
const [posts, setPosts] = React.useState<Post[]>();
React.useEffect(() => {
// fetch posts
setPosts(posts);
setLoading(false);
}, []);
if (loading) { return <div>Loading</div> }
return (
<ExploreStateContext.Provider value={{ loading, posts}}>
{props.children}
</ExploreStateContext.Provider>
}
Now, if I use this context in some child component, I run into an issue where I have to check if posts is undefined which is really annoying.
const { posts } = React.useContext(ExploreStateContext)
posts.map(doSomething) // posts is possibly undefined!!!
Is there a good way to avoid this undefined checking?
May be you are looking for the optional chaining operator
You use like this posts?.map(post => ...)
To avoid undefined checking, append ! at the end of the variable from which the check needs to be removed.
Eg. posts!.map(doSomething)
If you want to avoid checking, empty array as initial value for post might work for you. In other words don't let posts to ever be undefined.

React.useState is changing initialValues const

I'm experiencing some odd behavior with react's useState hook. I would like to know why this is happening. I can see a few ways to sidestep this behavior, but want to know whats going on.
I am initializing the state with the following const:
const initialValues = {
order_id: '',
postal_code: '',
products: [
{
number: '',
qty: ''
}
]
}
const App = (props) => {
const [values, setValues] = React.useState(initialValues);
...
products is an array of variable size. As the user fills in fields more appear.
The change handler is:
const handleProductChange = (key) => (field) => (e) => {
if (e.target.value >= 0 || e.target.value == '') {
let products = values.products;
products[key][field] = e.target.value;
setValues({ ...values, products });
}
}
What I am noticing is that if I console log initialValues, the products change when the fields are changed. None of the other fields change, only inside the array.
Here is a codepen of a working example.
How is this possible? If you look at the full codepen, you'll see that initialValues is only referenced when setting default state, and resetting it. So I don't understand why it would be trying to update that variable at all. In addition, its a const declared outside of the component, so shouldn't that not work anyway?
I attempted the following with the same result:
const initialProducts = [
{
number: '',
qty: ''
}
];
const initialValues = {
order_id: '',
postal_code: '',
products: initialProducts
}
In this case, both consts were modified.
Any insight would be appreciated.
Alongside exploding state into multiple of 1 level deep you may inline your initial:
= useState({ ... });
or wrap it into function
function getInitial() {
return {
....
};
}
// ...
= useState(getInitial());
Both approaches will give you brand new object on each call so you will be safe.
Anyway you are responsible to decide if you need 2+ level nested state. Say I see it legit to have someone's information to be object with address been object as well(2nd level deep). Splitting state into targetPersonAddress, sourePersonAddress and whoEverElsePersonAddress just to avoid nesting looks like affecting readability to me.
This would be a good candidate for a custom hook. Let's call it usePureState() and allow it to be used the same as useState() except the dispatcher can accept nested objects which will immutably update the state. To implement it, we'll use useReducer() instead of useState():
const pureReduce = (oldState, newState) => (
oldState instanceof Object
? Object.assign(
Array.isArray(oldState) ? [...oldState] : { ...oldState },
...Object.keys(newState).map(
key => ({ [key]: pureReduce(oldState[key], newState[key]) })
)
)
: newState
);
const usePureState = initialState => (
React.useReducer(pureReduce, initialState)
);
Then the usage would be:
const [values, setValues] = usePureState(initialValues);
...
const handleProductChange = key => field => event => {
if (event.target.value >= 0 || event.target.value === '') {
setValues({
products: { [key]: { [field]: event.target.value } }
});
}
};
Probably the simplest move forward is to create a new useState for products which I had started to suspect before asking the question, but a solution to keep the logic similar to how it was before would be:
let products = values.products.map(product => ({...product}));
to create a completely new array as well as new nested objects.
As #PatrickRoberts pointed out, the products variable was not correctly creating a new array, but was continuing to point to the array reference in state, which is why it was being modified.
More explanation on the underlying reason initialValues was changed: Is JavaScript a pass-by-reference or pass-by-value language?

React JS Error: Invalid attempt to destructure non-iterable instance

I have a sort filter that takes an array to populate the options. Trying to see the option value equal to the text within the array but I get the error within the title:
Invalid attempt to destructure non-iterable instance
I need to pass the text as the value within the option tag so that when the user updates the filter, the correct text displays to the choice the user made.
Here is my code:
function Sorting({by, order, rp}: SortingProps) {
const opts = [
['Price (low)', 'price', 'asc'],
['Price (high)', 'price', 'desc'],
['Discount (low)', 'discount', 'asc'],
['Discount (high)', 'discount', 'desc'],
['Most popular', 'arrival', 'latest'],
['Most recent', 'arrival', 'latest'],
];
const onChange = (i) => {
const [text, by, order] = opts[i];
refresh({so: {[by]: order}});
/* GA TRACKING */
ga('send', 'event', 'My Shop Sort By', text, 'Used');
};
return (
<div className={cn(shop.sorting, rp.sorting.fill && shop.sortingFill)}>
<Select className={shop.sortingSelect} label="Sort By" onChange={onChange} value={`${by}:${order}`}>
{opts.map(([text], i) =>
<Option key={i} value={text}>{text}</Option>
)}
</Select>
</div>
)
}
I caused this error a few times because whenever I write a useState hook, which I would do often, I'm used to using an array to destructure like so:
const [ state, setState ] = useState();
But my custom hooks usually return an object with properties:
const { data, isLoading } = useMyCustomFetchApiHook();
Sometime I accidentally write [ data, isLoading ] instead of { data, isLoading }, which tiggers this message because you're asking to destructure properties from an array [], when the object you're destructuring from is an object {}.
I also encountered a similar error and honestly, I did a very silly mistake maybe because of editor autocomplete.
I had to make use of the useState hook but somehow due to autocomplete, I wrote it like this.
const [state, setState] = useEffect(defaultValue);
instead of :(.
const [state, setState] = useState(defaultValue);
Hope it will help as an error message, in this case, was not helpful at all until I spent some time debugging this.
The error Invalid attempt to destructure non-iterable instance occurs because of a logic/coding error. The following javascript is used to illustrate the problem:
[aaa,bbb] = somefunc()
When somefunc() is called it must return an array of at least two items. If it doesn't there is no way to convert the result from somefunc() into values for aaa and bbb. For example, the code:
[aaa,bbb] = { 'some': 'object'}
would produce this error.
So the error is really a Javascript coding error and it is just happening inside React code that handles this situation by printing the error shown. See MDN for destructuring assignment documentation.
As #Mayank Shukla states in his answer, the answer to the OP question is to fix this line of code:
const [text, by, order] = opts[i];
By changing it to this:
const [text, by, order] = opts[i.target.value];
With my above description it should be clearer that opts[i] the original code by the OP was not returning an array of at least 3 items so the javascript runtime was not able to set the values of the variables text, by and order. The modified/fixed code does return an array so the variables can be set.
After looking for an answer to this question I realized that the other answers were correct, and I am just summarizing the root cause of the error message.
If anybody is using useState() hooks, and facing above issue while using context. They can try below solution.
In place of []
const [userInfo, setUserInfo] = useContext(userInfoContext);
Use {}
const {userInfo, setUserInfo} = useContext(userInfoContext); // {} can fix your issue
I straight up tried to assign it an empty object!
Bad :(
const [state, setState] = {};
Good :)
const [state, setState] = useState({});
You aren't passing an argument along with your onChange, it's a pretty common thing to miss - however a little less obvious with a select/option combination.
It should look something like:
class Sorting extends React.Component {
constructor(props) {
super(props);
this.opts = [
['Price (low)', 'price', 'asc'],
['Price (high)', 'price', 'desc'],
['Discount (low)', 'discount', 'asc'],
['Discount (high)', 'discount', 'desc'],
['Most popular', 'arrival', 'latest'],
['Most recent', 'arrival', 'latest'],
];
this.state = {
selected: 0, // default value
}
this.onChange = this.onChange.bind(this);
}
onChange(i) {
const [text, by, order] = opts[i.target.value];
};
render() {
return (
<div>
<select onChange={this.onChange} value={this.state.selected}>
{this.opts.map(([text], i) =>
<option key={i} value={i}>{text}</option>
)}
</select>
</div>
)
}
}
ReactDOM.render(<Sorting />, document.getElementById("a"));
Note I stripped out your classes and styles to keep it simple. Also note you were using uppercase Select and Option - unless these are custom in house components, they should be lowercase.
Note2 I also introduced state, because the state of the select needs to be stored somewhere - if you are maintaining the state of the select box outside of this component, you can obviously use a combination of props/callbacks to maintain that value one level higher.
http://codepen.io/cjke/pen/egPKPB?editors=0010
I encountered this question because I had the same error, but in order to make it work for me, I wrote
const [someRef] = useRef(null);
When it should have been
const someRef = useRef(null); // removed the braces []
Make sure your useState is a function call not an array type.
useState('') not useState['']
Problem is with variable i, i will be the event object, use i.target.value to get the value selected by the user, one more thing you used text as the value of the options, instead of that use the index, it will work, try this:
const onChange = (i) => {
const [text, by, order] = opts[i.target.value];
refresh({so: {[by]: order}});
/* GA TRACKING */
ga('send', 'event', 'My Shop Sort By', text, 'Used');
};
return (
<div className={cn(shop.sorting, rp.sorting.fill && shop.sortingFill)}>
<select className={shop.sortingSelect} label="Sort By" onChange={onChange} value={`${by}:${order}`}>
{opts.map(([text], i) =>
<option key={i} value={i}>{text}</option>
)}
</select>
</div>
)
Check this fiddle: https://jsfiddle.net/5pzcr0ef/
This error can also happen if you have an async function that returns an array, but you forget to run it with await. This will result in that error:
const myFunction = async () => {
return [1, 2]
}
const [num1, num2] = myFunction();
Whereas this will succeed:
const [num1, num2] = await myFunction();
Invalid attempt to destructure non-iterable instance
says the instance you are trying to iterate is not iterable. What you should do is checking whether the opt object is iterable and can be accessed in the JSX code.
When using React Context API, this error can occur if you try to use React.useContext() in a component that is not wrapped in the <ContextProvider>
For example, the following code would throw this error:
const App = () => {
const [state, setState] = React.useContext(MyContext)
return (
<ContextProvider>
<SubComponent />
</ContextProvider>
);
}
You can use the line:
const [state, setState] = React.useContext(MyContext)
inside the SubComponent, but not inside the App component. If you want to use it in the App component, place App component inside another component and wrap the App component in <ContextProvider></ContextProvider>.
const App = () => {
const [state, setState] = React.useContext(MyContext)
return (
<div>
<SubComponent />
</div>);
}
const Master = () => {
<ContextProvider>
<App/>
</ContextProvider>
}
In my Case i did this mistake
const {width,height} = Dimensions("window") to const[width ,height] = Dimensions("window)
For me the issue was that I tried to destructure useState incorrectly.
I wrote
const [counter] = useState(0)[0];
instead of
const counter = useState(0)[0];
My 5 cents.
I did
const [seconds, setSeconds] = 0
instead of
const [seconds, setSeconds] = useState(0)
Hope it helps someone. It got me mad for a minute or two because "it worked yesterday" and error was reported on top of functional component body actually, so it wasn't giving right clues and I had to dig deeper. I commented out whole function body just to make sure everything was OK with my arguments... and the error was below in code.

Resources