state is not being updated when using React Hooks - reactjs

I am currently playing around with the new React Hooks feature, and I have run into an issue where the state of a functional component is not being updated even though I believe I am doing everything correctly, another pair of eyes on this test app would be much appreciated.
import React, { useState, useEffect } from 'react';
const TodoList = () => {
let updatedList = useTodoListState({
title: "task3", completed: false
});
const renderList = () => {
return (
<div>
{
updatedList.map((item) => {
<React.Fragment key={item.title}>
<p>{item.title}</p>
</React.Fragment>
})
}
</div>
)
}
return renderList();
}
function useTodoListState(state) {
const [list, updateList] = useState([
{ title: "task1", completed: false },
{ title: "task2", completed: false }
]);
useEffect(() => {
updateList([...list, state]);
})
return list;
}
export default TodoList;
updateList in useTodoListState's useEffect function should be updating the list variable to hold three pieces of data, however, that is not the case.

You have a few problems here:
In renderList you are misusing React.Fragment. It should be used to wrap multiple DOM nodes so that the component only returns a single node. You are wrapping individual paragraph elements each in their own Fragment.
Something needs to be returned on each iteration of a map. This means you need to use the return keyword. (See this question for more about arrow function syntax.)
Your code will update infinitely because useEffect is updating its own hook's state. To remedy this, you need to include an array as a second argument in useEffect that will tell React to only use that effect if the given array changes.
Here's what it should all look like (with some reformatting):
import React, { useState, useEffect } from 'react';
const TodoList = () => {
let updatedList = useTodoListState({
title: "task3", completed: false
});
return (
<React.Fragment> // #1: use React.Fragment to wrap multiple nodes
{
updatedList.map((item) => {
return <p key={item.title}>{item.title}</p> // #2: return keyword inside map with braces
})
}
</React.Fragment>
)
}
function useTodoListState(state) {
const [list, updateList] = useState([
{ title: "task1", completed: false },
{ title: "task2", completed: false }
]);
useEffect(() => {
updateList([...list, state]);
}, [...list]) // #3: include a second argument to limit the effect
return list;
}
export default TodoList;
Edit:
Although I have tested that the above code works, it would ultimately be best to rework the structure to remove updateList from useEffect or implement another variable to control updating.

Related

Setting state with React Context from child component

Hey y'all I am trying to use the Context API to manage state to render a badge where the it's not possible to pass props. Currently I am trying to use the setUnreadNotif setter, but it seems because I am using it in a method that loops through an array that it is not working as expected. I have been successful updating the boolean when only calling setUnreadNotif(true/false); alone so I know it works. I have tried many other approaches unsuccessfully and this seems the most straight forward. My provider is wrapping app appropriately as well so I know its not that. Any help is greatly appreciated.
Here is my Context
import React, {
createContext,
Dispatch,
SetStateAction,
useContext,
useState,
} from 'react';
import { getContentCards } from 'ThisProject/src/utils/braze';
import { ContentCard } from 'react-native-appboy-sdk';
export interface NotificationsContextValue {
unreadNotif: boolean;
setUnreadNotif: Dispatch<SetStateAction<boolean>>;
}
export const defaultNotificationsContextValue: NotificationsContextValue = {
unreadNotif: false,
setUnreadNotif: (prevState: SetStateAction<boolean>) => prevState,
};
const NotificationsContext = createContext<NotificationsContextValue>(
defaultNotificationsContextValue,
);
function NotificationsProvider<T>({ children }: React.PropsWithChildren<T>) {
const [unreadNotif, setUnreadNotif] = useState<boolean>(false);
return (
<NotificationsContext.Provider
value={{
unreadNotif,
setUnreadNotif,
}}>
{children}
</NotificationsContext.Provider>
);
}
function useNotifications(): NotificationsContextValue {
const context = useContext(NotificationsContext);
if (context === undefined) {
throw new Error('useUser must be used within NotificationsContext');
}
return context;
}
export { NotificationsContext, NotificationsProvider, useNotifications };
Child Component
export default function NotificationsPage({
navigation,
}: {
navigation: NavigationProp<StackParamList>;
}) {
const [notificationCards, setNotificationCards] = useState<
ExtendedContentCard[]
>([]);
const user = useUser();
const { setUnreadNotif } = useNotifications();
const getCards = (url: string) => {
if (url.includes('thisproject:')) {
Linking.openURL(url);
} else {
navigation.navigate(ScreenIdentifier.NotificationsStack.id, {
screen: ScreenIdentifier.NotificationsWebView.id,
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
params: {
uri: `${getTrustedWebAppUrl()}${url}`,
title: 'Profile',
},
});
}
getContentCards((response: ContentCard[]) => {
response.forEach((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
});
});
Braze.requestContentCardsRefresh();
};
return (
<ScrollView style={styles.container}>
<View style={styles.contentContainer}>
{notificationCards?.map((item: ExtendedContentCard) => {
return (
<NotificationCard
onPress={getCards}
key={item.id}
id={item.id}
title={item.title}
description={item.cardDescription}
image={item.image}
clicked={item.clicked}
ctaTitle={item.domain}
url={item.url}
/>
);
})}
</View>
</ScrollView>
);
}
Fixed Issue
I was able to fix the issue by foregoing the forEach and using a findIndex instead like so:
getContentCards((response: ContentCard[]) => {
response.findIndex((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
setUnreadNotif(false);
});
});
An issue I see in the getContentCards handler is that it is mis-using the Array.prototype.findIndex method to issue unintended side-effects, the effect here being enqueueing a state update.
getContentCards((response: ContentCard[]) => {
response.findIndex((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
setUnreadNotif(false);
});
});
What's worse is that because the passed predicate function, e.g. the callback, never returns true, so each and every element in the response array is iterated and a state update is enqueued and only the enqueued state update when card.clicked === false evaluates true is the unreadNotif state set true, all other enqueued updates set it false. It may be true that the condition is true for an element, but if it isn't the last element of the array then any subsequent iteration is going to enqueue an update and set unreadNotif back to false.
The gist it seems is that you want to set the unreadNotif true if there is some element with a falsey card.clicked value.
getContentCards((response: ContentCard[]) => {
setUnreadNotif(response.some(card => !card.clicked));
});
Here you'll see that the Array.prototype.some method returns a boolean if any of the array elements return true from the predicate function.
The some() method tests whether at least one element in the array
passes the test implemented by the provided function. It returns true
if, in the array, it finds an element for which the provided function
returns true; otherwise it returns false. It doesn't modify the array.
So long as there is some card in the response that has not been clicked, the state will be set true, otherwise it is set to false.

React-component is not re-rendered when the store is changed, neither automatically nor even by force update

This functional component should display a sorted list with checkboxes at each item that change the values in the store.
For some reason it is not re-rendered when the store is changed. And without a re-renderer, it (and the whole application) works very crookedly and halfway. I suspect that this is because the store object remains the same, albeit with new content. But I don’t understand how to fix it. I have even inserted a force update to the checkbox handler, but for some reason it does not work too.
Component:
import React, { useState, useReducer } from 'react';
import { ReactSortable } from 'react-sortablejs';
import ListItem from '#mui/material/ListItem';
import Checkbox from '#mui/material/Checkbox';
import { connect } from 'react-redux';
import { setGameVisible, setGameInvisible } from '../store/actions/games';
interface IGamesListProps {
games: [];
setGameVisible: (id: string) => void;
setGameInvisible: (id: string) => void;
}
interface ItemType {
id: string;
name: string;
isVisible: boolean;
}
const GamesList: React.FunctionComponent<IGamesListProps> = ({games, setGameVisible, setGameInvisible}) => {
const [state, setState] = useState<ItemType[]>(games);
// eslint-disable-next-line
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); // this way of force updating is taken from the official React documentation (but even it doesn't work!)
const onCheckboxChangeHandle = (id: string, isVisible: boolean) => {
isVisible ? setGameInvisible(id) : setGameVisible(id);
forceUpdate(); // doesn't work :(((
}
return (
<ReactSortable list={state} setList={setState} tag='ul'>
{state.map((item) => (
<ListItem
sx={{ maxWidth: '300px' }}
key={item.id}
secondaryAction={
<Checkbox
edge="end"
onChange={() => onCheckboxChangeHandle(item.id, item.isVisible)}
checked={item.isVisible}
/>
}
>
{item.name}
</ListItem>
))}
</ReactSortable>
);
};
export default connect(null, { setGameVisible, setGameInvisible })(GamesList);
Reducer:
import { SET_GAMES, SET_GAME_VISIBLE, SET_GAME_INVISIBLE } from '../actions/games';
export const initialState = {
games: [],
};
export default function games(state = initialState, action) {
switch(action.type) {
case SET_GAMES: {
for(let obj of action.payload.games) {
obj.isVisible = true;
}
return {
...state,
games: action.payload.games,
};
}
case SET_GAME_VISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = true;
};
}
return {
...state,
};
}
case SET_GAME_INVISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = false;
};
}
return {
...state,
};
}
default:
return state;
}
}
Thank you for any help!
Note: By the information You gave I came with the idea of the problem, but I posted here because it is going to be explanatory and long.
First of all, you don't pass the new game via mapStateToProps into Component in a state change, and even you do, useState won't use new game prop value for non-first render. You must use useEffect and trigger changes of the game and set the to state locally.
At this point you must find the inner state redundant and you can remove it and totally rely on the redux state.
const mapStateToProp = (state) => ({
games: state.games // you may need to change the path
})
connect(mapStateToProp, { setGameVisible, setGameInvisible })(GamesList);
Second, the reducer you made, changes the individual game item but not the games list itself. because it is nested and the reference check by default is done as strict equality reference check-in redux state === state. This probably doesn't cause an issue because the outer state changes by the way, but I think it worth it to mention it.
for(let obj of action.payload.games) {
obj.isVisible = true; // mutating actions.payload.games[<item>]
}
return {
...state,
games: [...action.payload.games], // add immutability for re-redenr
};
// or use map
return {
...state,
games: action.payload.games.map(obj => ({...obj, isVisible:true})),
};
Third, It's true your forceUpdate will cause the component to re-render, and you can test that by adding a console.log, but it won't repaint the whole subtree of your component including inner children if their props don't change that's because of performance issue. React try to update as efficiently as possible. Also you the key optimization layer which prevent change if the order of items and id of them doesn't change

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.

updating current time every second in react without rendering

Many of my components in a react native app require to know what the current time is every second. This way I can show updated real-time information.
I created a simple functionality to set the state with new Date(), but whenever I set the state, the component re-renders, which is a waste my case.
Here is what I have:
...
export default function App() {
const [currentDateTime, setCurrentDateTime] = useState(() => new Date().toLocaleString());
useEffect(() => {
const secondsTimer = setInterval(() => {
setCurrentDateTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(secondsTimer);
}, [setCurrentDateTime]);
console.log('RENDERING');
<Text>{currentDateTime}</Text>
...
I can see the console logs RENDERING every second.
Is there a way to avoid this rerendering and still update currentDateTime
Consider using shouldComponentUpdate lifecycle method; It's purpose is for preventing unnecessary renders. Add this method and tell your component not to update if this particular part of your state changes. As an example, you might add this shouldComponentUpdate() that rejects updates that are more than
// Example logic for only re-rendering every 5 seconds; Adapt as needed.
shouldComponentUpdate(nextProps, nextState) {
if (this.lastUpdatedTimeInSeconds+5 >= nextState.timeinseconds) {
return false;
}
this.lastUpdatedTimeInSeconds = nextState.timeinseconds
return true;
}
Further Reading: https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/using_should_component_update.html
If I understand what you're saying, you want to update the DOM without triggering React's lifecycle. This is possible using refs (see React.useRef):
import * as React from "react";
import "./styles.css";
export default function App() {
const dateTimeRef = React.useRef<HTMLSpanElement>(null);
console.log("RENDERING");
React.useEffect(() => {
const secondsTimer = setInterval(() => {
if (dateTimeRef.current) {
dateTimeRef.current.innerText = new Date().toLocaleString()
}
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <span ref={dateTimeRef} />;
}
See working demo - https://codesandbox.io/s/nice-snow-kt500?file=/src/App.tsx
Update 1
If you want to use a component such as Text, then the component will have to forward the ref to the dom, like here:
import * as React from "react";
import "./styles.css";
const Text = React.forwardRef<HTMLSpanElement>((props: any, ref) => {
console.log("RENDERING TEXT")
return <span ref={ref}></span>
});
export default function App() {
const dateTimeRef = React.useRef<HTMLSpanElement>(null);
console.log("RENDERING APP");
React.useEffect(() => {
const secondsTimer = setInterval(() => {
if (dateTimeRef.current) {
dateTimeRef.current.innerText = new Date().toLocaleString();
}
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <Text ref={dateTimeRef} />;
}
See working demo - https://codesandbox.io/s/jolly-moon-9zsh2?file=/src/App.tsx
Eliya Cohen's answer was conceptually correct. To avoid re-rendering, we cannot use state with an interval. We need to reference the element. Unfortunately, I wasn't able to adopt Eliya's React code to React Native in the same manner, so I did some more digging and found docs on directly manipulating React Native components.
In short, you can manipulate built in RN components' PROPS and avoid re-rendering by not changing the state.
Since the <Text> component doesn't set its value with a prop, such as <Text text="my text" />, we are not able to use this method to update it. But what does work is updating the value of a TextInput since its set with the value prop. All we need to do to make the <TextInput> behave like a <Text> is to set its prop editable to false, and of course avoid default styling of it that would make it look like an input.
Here is my solution. If someone has a better one, please do propose it.
import React, { useEffect } from 'react';
import { TextInput } from 'react-native';
const Timer: React.FC = () => {
updateTime = (currentTime) => {
time.setNativeProps({ text: currentTime });
};
useEffect(() => {
const secondsTimer = setInterval(() => {
updateTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <TextInput ref={(component) => (time = component)} editable={false} />;
};
export default Timer;
I also tried this and this is what that worked for me after a few attempts with Typescript.
const timeTextInput = useRef<TextInput>(null);
useEffect(()=>{
const timer = setInterval(() => {
timeTextInput.current?.setNativeProps({ text: new Date().toLocaleString() });
}, 1000);
return () => clearInterval(timer);
}, []);
Hope this helps someone in the future.

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.

Resources