Setting state with React Context from child component - reactjs

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.

Related

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

Cannot update a component while rendering a different Component - ReactJS

I know lots of developers had similar kinds of issues in the past like this. I went through most of them, but couldn't crack the issue.
I am trying to update the cart Context counter value. Following is the code(store/userCartContext.js file)
import React, { createContext, useState } from "react";
const UserCartContext = createContext({
userCartCTX: [],
userCartAddCTX: () => {},
userCartLength: 0
});
export function UserCartContextProvider(props) {
const [userCartStore, setUserCartStore] = useState([]);
const addCartProduct = (value) => {
setUserCartStore((prevState) => {
return [...prevState, value];
});
};
const userCartCounterUpdate = (id, value) => {
console.log("hello dolly");
// setTimeout(() => {
setUserCartStore((prevState) => {
return prevState.map((item) => {
if (item.id === id) {
return { ...item, productCount: value };
}
return item;
});
});
// }, 50);
};
const context = {
userCartCTX: userCartStore,
userCartAddCTX: addCartProduct,
userCartLength: userCartStore.length,
userCartCounterUpdateCTX: userCartCounterUpdate
};
return (
<UserCartContext.Provider value={context}>
{props.children}
</UserCartContext.Provider>
);
}
export default UserCartContext;
Here I have commented out the setTimeout function. If I use setTimeout, it works perfectly. But I am not sure whether it's the correct way.
In cartItemEach.js file I use the following code to update the context
const counterChangeHandler = (value) => {
let counterVal = value;
userCartBlockCTX.userCartCounterUpdateCTX(props.details.id, counterVal);
};
CodeSandBox Link: https://codesandbox.io/s/react-learnable-one-1z5td
Issue happens when I update the counter inside the CART popup. If you update the counter only once, there won't be any error. But when you change the counter more than once this error pops up inside the console. Even though this error arises, it's not affecting the overall code. The updated counter value gets stored inside the state in Context.
TIL that you cannot call a setState function from within a function passed into another setState function. Within a function passed into a setState function, you should just focus on changing that state. You can use useEffect to cause that state change to trigger another state change.
Here is one way to rewrite the Counter class to avoid the warning you're getting:
const decrementHandler = () => {
setNumber((prevState) => {
if (prevState === 0) {
return 0;
}
return prevState - 1;
});
};
const incrementHandler = () => {
setNumber((prevState) => {
return prevState + 1;
});
};
useEffect(() => {
props.onCounterChange(props.currentCounterVal);
}, [props.currentCounterVal]);
// or [props.onCounterChange, props.currentCounterVal] if onCounterChange can change
It's unclear to me whether the useEffect needs to be inside the Counter class though; you could potentially move the useEffect outside to the parent, given that both the current value and callback are provided by the parent. But that's up to you and exactly what you're trying to accomplish.

Why am I able to conditionally call a hook one way, but not the other?

Context:
When I refresh the dashboard, useHasPermission makes an async call to determine if the
user has access to somePermission.
Issue:
hasPermission initially evaluates to false, but once the async call has completed hasPermission evaluates to true.
This causes the useQuery, Apollo hook to not be called on the first render, and then called on the second render.
The following error is shown:
"Rendered more hooks than during the previous render."
Question:
Why does this error only happen in example A and not example B?
// Example A: Does not work
const Dashboard = () => {
const hasPermission = useHasPermission([somePermission]);
const getDashboardData = () => {
const { loading, data, error } = useQuery(SOME_QUERY, {
variables: { ...someVars }
});
return <Table ={data} loading={loading} error={error}><Table>
};
return (
{hasPermission ? getDashboardData() : null}
<Announcements></Announcements>
)
}
// Example B: Works
const Dashboard = () => {
const hasPermission = useHasPermission([somePermission]);
const DashboardData = () => {
const { loading, data, error } = useQuery(ACCOUNTS_FOR_CUSTOMER_DASHBOARD, {
variables: { ...someVars }
});
return <Table ={data} loading={loading} error={error}><Table>
};
return (
{hasPermission ? (
<DashboardData></DashboardData>
) : null}
<Announcements></Announcements>
)
}
Hooks aren't meant to be conditionally used.
In the first example, you are conditionally calling a function that uses a new hook and returns JSX, so this breaks the rules of hooks.
In the second example, you are creating a new component DashboardData that mounts conditionally. So because it is a new component it is allowed.
So the difference between the two is in "A" useQuery belongs to the Dashboard component, where in "B" it belongs to DashboardData.

state is not being updated when using React Hooks

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.

How to set a state array with values from TextField using onchange

I am new to react and am trying to add string values in an array. I am using Material-UI objects.
My state has
this.state: {
roles: []
}
A button pushes an undefined element in roles, incrementing its length.
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
So now we have some length to the roles array.
The Textfield is generated with
this.state.roles.map((item, i)=> {
return (
<TextField id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
the onchange event is handled as below
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
The console.log statements generate output like
roles[0]
[undefined]
I expect
roles[0]
["somedata"]
what is going wrong here? The data does not get set in the roles array.
The whole code file is
const styles = theme => ({
error: {
verticalAlign: 'middle'
},
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit,
width: 300
},
submit: {
margin: 'auto',
marginBottom: theme.spacing.unit * 2
}
})
class AddModule extends Component {
constructor() {
super();
this.state = {
roles:[],
open: false,
error: ''
}
}
clickSubmit = () => {
const module = {
roles: this.state.roles || undefined
}
create(module).then((data) => {
if (data.error) {
this.setState({error: data.error})
} else {
this.setState({error: '', 'open': true});
}
})
}
clickAddRole = () => {
this.setState({roles: this.state.roles.concat([undefined]) });
};
handleChange = name => event => {
console.log(name);
this.setState({[name]: event.target.value});
console.log(this.state.roles);
}
render() {
const {classes} = this.props;
return (
<div>
<Button onClick={this.clickAddRole} >Add Role</Button>
{
this.state.roles.map((item, i)=> {
return (
<TextField className={classes.textField} id={'roles['+i+']'} label={'role '+i} key={i} onChange={this.handleChange('roles['+i+']')} />
)
})
}
</div>
)
}
}
I think you're making the whole code a bit overcomplicated creating names for each input field. What I would do is change the handleRolesChange or handleChange (not really sure if you changed its name) method so that it takes the index instead of a name.
handleRolesChange = index => event => {
const { roles } = this.state;
const newRoles = roles.slice(0); // Create a shallow copy of the roles
newRoles[index] = event.target.value; // Set the new value
this.setState({ roles: newRoles });
}
Then change the render method to something like this:
this.state.roles.map((item, index) => (
<TextField
id={`roles[${index}]`}
label={`role ${index}`}
key={index}
onChange={this.handleRolesChange(index)}
/>
))
Guy I have the issue (maybe temporarily).
I an array-element is a child of the array. so changing the data in the array-element does not need setState.
So this is what I did....
handleRolesChange = name => event => {
const i = [name];
this.state.roles[i]=event.target.value;
}
I also change the Textfield onchange parameter to
onChange={this.handleRolesChange(i)}
where i is the index starting from zero in the map function.
All this works perfectly as I needed.
However, if you think that I have mutated the roles array by skipping setState, I will keep the Question unanswered and wait for the correct & legitimate answer.
Thanks a lot for your support guys.
We must try and find the solution for such basic issues. :)
Are you positive it's not being set? From React's docs:
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
Usually logging state in the same block you set the code in will print the previous state, since state has not actually updated at the time the console.log fires.
I would recommend using React Dev Tools to check state, instead of relying on console.log.

Resources